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 @@ - + + + + +