diff --git a/samples/Directory.build.props b/samples/Directory.build.props
index db19600a..723ae204 100644
--- a/samples/Directory.build.props
+++ b/samples/Directory.build.props
@@ -2,7 +2,6 @@
enable
11.0.10
- $(MSBuildThisFileDirectory)..\src\analyzers.ruleset
CS8600;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8623;CS8624;CS8625
diff --git a/src/Directory.build.props b/src/Directory.build.props
index c162a511..b821a33b 100644
--- a/src/Directory.build.props
+++ b/src/Directory.build.props
@@ -27,6 +27,7 @@
preview
True
latest
+ $(NoWarn);IDE1006
@@ -35,17 +36,16 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
\ No newline at end of file
diff --git a/src/ReactiveUI.Validation.Tests/MemoryLeakTests.cs b/src/ReactiveUI.Validation.Tests/MemoryLeakTests.cs
index ba48c583..664e9f20 100644
--- a/src/ReactiveUI.Validation.Tests/MemoryLeakTests.cs
+++ b/src/ReactiveUI.Validation.Tests/MemoryLeakTests.cs
@@ -37,13 +37,13 @@ public void Instance_Released_IsGarbageCollected()
new Action(
() =>
{
- var sut = new TestClassMemory();
+ var memTest = new TestClassMemory();
- reference = new WeakReference(sut, true);
- sut.Dispose();
+ reference = new WeakReference(memTest, true);
+ memTest.Dispose();
})();
- // Sut should have gone out of scope about now, so the garbage collector can clean it up
+ // memTest should have gone out of scope about now, so the garbage collector can clean it up
dotMemory.Check(
memory => memory.GetObjects(
where => where.Type.Is()).ObjectsCount.Should().Be(0, "it is out of scope"));
@@ -51,14 +51,6 @@ public void Instance_Released_IsGarbageCollected()
GC.Collect();
GC.WaitForPendingFinalizers();
- if (reference.Target is TestClassMemory sut)
- {
- // ReactiveObject does not inherit from IDisposable, so we need to check ValidationContext
- sut.ValidationContext.Should().BeNull("it is garbage collected");
- }
- else
- {
- reference.Target.Should().BeNull("it is garbage collected");
- }
+ reference.IsAlive.Should().BeFalse("it is garbage collected");
}
}
diff --git a/src/ReactiveUI.Validation.Tests/Models/TestClassMemory.cs b/src/ReactiveUI.Validation.Tests/Models/TestClassMemory.cs
index fccca40f..58423e62 100644
--- a/src/ReactiveUI.Validation.Tests/Models/TestClassMemory.cs
+++ b/src/ReactiveUI.Validation.Tests/Models/TestClassMemory.cs
@@ -31,7 +31,7 @@ public TestClassMemory()
.DisposeWith(_disposable);
// commenting out the following statement makes the test green
- ValidationContext.ValidationStatusChange
+ ValidationContext?.ValidationStatusChange
.Subscribe(/* you do something here, but this does not matter for now. */)
.DisposeWith(_disposable);
}
diff --git a/src/ReactiveUI.Validation.Tests/ValidationContextTests.cs b/src/ReactiveUI.Validation.Tests/ValidationContextTests.cs
index de6f525f..7dd5bbd6 100644
--- a/src/ReactiveUI.Validation.Tests/ValidationContextTests.cs
+++ b/src/ReactiveUI.Validation.Tests/ValidationContextTests.cs
@@ -125,7 +125,7 @@ public void IsValidShouldNotifyOfValidityChange()
viewModel.ValidationContext.Add(nameValidation);
var latestValidity = false;
- viewModel.IsValid().Subscribe(isValid => latestValidity = isValid);
+ var d = viewModel.IsValid().Subscribe(isValid => latestValidity = isValid);
Assert.False(latestValidity);
viewModel.Name = "Jonathan";
@@ -133,6 +133,7 @@ public void IsValidShouldNotifyOfValidityChange()
viewModel.Name = string.Empty;
Assert.False(latestValidity);
+ d.Dispose();
}
///
@@ -220,4 +221,4 @@ public void ShouldClearAttachedValidationRulesForTheGivenProperty()
Assert.NotEmpty(viewModel.ValidationContext.Text);
Assert.Equal(name2ErrorMessage, viewModel.ValidationContext.Text.ToSingleLine());
}
-}
\ No newline at end of file
+}
diff --git a/src/ReactiveUI.Validation/Collections/ReadOnlyCollectionPooled.cs b/src/ReactiveUI.Validation/Collections/ReadOnlyCollectionPooled.cs
deleted file mode 100644
index 3803d05d..00000000
--- a/src/ReactiveUI.Validation/Collections/ReadOnlyCollectionPooled.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-using System;
-using System.Buffers;
-using System.Collections;
-using System.Collections.Generic;
-
-using ReactiveUI.Validation.Extensions;
-
-namespace ReactiveUI.Validation.Collections;
-
-internal sealed class ReadOnlyCollectionPooled : IReadOnlyCollection, IDisposable
-{
- private readonly T[] _items;
-
- public ReadOnlyCollectionPooled(IEnumerable items)
- {
- var array = ArrayPool.Shared.Rent(16);
- var index = 0;
-
- foreach (var item in items)
- {
- if (array.Length == index)
- {
- ArrayPool.Shared.Resize(ref array, array.Length * 2, true);
- }
-
- array![index] = item;
- index++;
- }
-
- Count = index;
- _items = array;
- }
-
- public int Count { get; }
-
- void IDisposable.Dispose() => ArrayPool.Shared.Return(_items);
-
- IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
-
- IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
-
- public Enumerator GetEnumerator() => new(this);
-
- public struct Enumerator : IEnumerator
- {
- private readonly ReadOnlyCollectionPooled _readOnlyCollectionPooled;
- private int _index;
- private T? _current;
-
- internal Enumerator(ReadOnlyCollectionPooled readOnlyCollectionPooled)
- {
- _readOnlyCollectionPooled = readOnlyCollectionPooled;
- _index = 0;
- _current = default;
- }
-
- public readonly T Current => _current!;
-
- readonly object? IEnumerator.Current
- {
- get
- {
- if (_index == 0 || _index == _readOnlyCollectionPooled.Count + 1)
- {
- ThrowInvalidOperationException();
- }
-
- return Current;
- }
- }
-
- public void Dispose()
- {
- }
-
- public bool MoveNext()
- {
- var readOnlyCollectionPooled = _readOnlyCollectionPooled;
-
- if ((uint)_index < (uint)readOnlyCollectionPooled.Count)
- {
- _current = readOnlyCollectionPooled._items[_index];
- _index++;
-
- return true;
- }
-
- return MoveNextRare();
- }
-
- public void Reset()
- {
- _index = 0;
- _current = default;
- }
-
- private static void ThrowInvalidOperationException() => throw new InvalidOperationException();
-
- private bool MoveNextRare()
- {
- _index = _readOnlyCollectionPooled.Count + 1;
- _current = default;
-
- return false;
- }
- }
-}
diff --git a/src/ReactiveUI.Validation/Collections/ReadOnlyDisposableCollection{T}.cs b/src/ReactiveUI.Validation/Collections/ReadOnlyDisposableCollection{T}.cs
new file mode 100644
index 00000000..1a532a12
--- /dev/null
+++ b/src/ReactiveUI.Validation/Collections/ReadOnlyDisposableCollection{T}.cs
@@ -0,0 +1,60 @@
+// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+
+namespace ReactiveUI.Validation.Collections;
+
+internal sealed class ReadOnlyDisposableCollection(IEnumerable items) : IReadOnlyCollection, IDisposable
+{
+ private readonly ImmutableList _immutableList = ImmutableList.CreateRange(items);
+ private bool _disposedValue;
+
+ ///
+ /// Gets the number of elements in the collection.
+ ///
+ public int Count => _immutableList.Count;
+
+ ///
+ /// Returns an enumerator that iterates through the collection.
+ ///
+ ///
+ /// An enumerator that can be used to iterate through the collection.
+ ///
+ public IEnumerator GetEnumerator() => _immutableList.GetEnumerator();
+
+ ///
+ /// Returns an enumerator that iterates through a collection.
+ ///
+ ///
+ /// An object that can be used to iterate through the collection.
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => _immutableList.GetEnumerator();
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ _immutableList.Clear();
+ }
+
+ _disposedValue = true;
+ }
+ }
+}
diff --git a/src/ReactiveUI.Validation/Contexts/ValidationContext.cs b/src/ReactiveUI.Validation/Contexts/ValidationContext.cs
index c3597866..ed2f8820 100755
--- a/src/ReactiveUI.Validation/Contexts/ValidationContext.cs
+++ b/src/ReactiveUI.Validation/Contexts/ValidationContext.cs
@@ -6,7 +6,6 @@
using System;
using System.Buffers;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
@@ -31,15 +30,16 @@ namespace ReactiveUI.Validation.Contexts;
///
public class ValidationContext : ReactiveObject, IValidationContext
{
+ private readonly CompositeDisposable _disposables = [];
+
private readonly ReplaySubject _validationStatusChange = new(1);
private readonly ReplaySubject _validSubject = new(1);
- private readonly IConnectableObservable _validationConnectable;
+ private readonly IObservable _validationObservable;
private readonly ObservableAsPropertyHelper _validationText;
private readonly ObservableAsPropertyHelper _isValid;
- private readonly CompositeDisposable _disposables = [];
- private SourceList _validationSource = new();
+ private readonly SourceList _validationSource = new();
private bool _isActive;
///
@@ -52,15 +52,14 @@ public ValidationContext(IScheduler? scheduler = null)
var changeSets = _validationSource.Connect().ObserveOn(scheduler);
Validations = changeSets.AsObservableList();
- _validationConnectable = changeSets
+ _validationObservable = changeSets
.StartWithEmpty()
.AutoRefreshOnObservable(x => x.ValidationStatusChange)
.QueryWhenChanged(static x =>
{
- using ReadOnlyCollectionPooled validationComponents = new(x);
+ using ReadOnlyDisposableCollection validationComponents = new(x);
return validationComponents.Count is 0 || validationComponents.All(v => v.IsValid);
- })
- .Multicast(_validSubject);
+ });
_isValid = _validSubject
.StartWith(true)
@@ -95,7 +94,7 @@ public IObservable Valid
///
/// Gets get the list of validations.
///
- public IObservableList Validations { get; private set; }
+ public IObservableList Validations { get; }
///
public bool IsValid
@@ -162,10 +161,7 @@ public IValidationText Text
///
public void Dispose()
{
- // Dispose of unmanaged resources.
Dispose(true);
-
- // Suppress finalization.
GC.SuppressFinalize(this);
}
@@ -183,11 +179,8 @@ protected virtual void Dispose(bool disposing)
_validationStatusChange.Dispose();
_validSubject.Dispose();
_validationSource.Clear();
- Validations.Dispose();
_validationSource.Dispose();
-
- Validations = null!;
- _validationSource = null!;
+ Validations.Dispose();
}
}
@@ -199,7 +192,7 @@ private void Activate()
}
_isActive = true;
- _disposables.Add(_validationConnectable.Connect());
+ _disposables.Add(_validationObservable.Subscribe(_validSubject));
}
///
diff --git a/src/ReactiveUI.Validation/Helpers/ReactiveValidationObject.cs b/src/ReactiveUI.Validation/Helpers/ReactiveValidationObject.cs
index aa9179a8..a40be4d0 100644
--- a/src/ReactiveUI.Validation/Helpers/ReactiveValidationObject.cs
+++ b/src/ReactiveUI.Validation/Helpers/ReactiveValidationObject.cs
@@ -27,9 +27,9 @@ namespace ReactiveUI.Validation.Helpers;
///
public abstract class ReactiveValidationObject : ReactiveObject, IValidatableViewModel, INotifyDataErrorInfo, IDisposable
{
- private CompositeDisposable _disposables = [];
- private IValidationTextFormatter _formatter;
- private HashSet _mentionedPropertyNames = [];
+ private readonly CompositeDisposable _disposables = [];
+ private readonly IValidationTextFormatter _formatter;
+ private readonly HashSet _mentionedPropertyNames = [];
private bool _hasErrors;
///
@@ -77,7 +77,7 @@ public bool HasErrors
}
///
- public IValidationContext ValidationContext { get; private set; }
+ public IValidationContext ValidationContext { get; }
///
/// Returns a collection of error messages, required by the INotifyDataErrorInfo interface.
@@ -121,11 +121,8 @@ protected virtual void Dispose(bool disposing)
if (!_disposables.IsDisposed && disposing)
{
_disposables.Dispose();
+ ValidationContext.Dispose();
_mentionedPropertyNames.Clear();
- _formatter = null!;
- _mentionedPropertyNames = null!;
- _disposables = null!;
- ValidationContext = null!;
}
}
diff --git a/src/ReactiveUI.Validation/ReactiveUI.Validation.csproj b/src/ReactiveUI.Validation/ReactiveUI.Validation.csproj
index 4e4c20ff..3b88bcf1 100644
--- a/src/ReactiveUI.Validation/ReactiveUI.Validation.csproj
+++ b/src/ReactiveUI.Validation/ReactiveUI.Validation.csproj
@@ -8,6 +8,10 @@
-
+
+
+
+
+