diff --git a/src/ObservableCollections/IObservableCollection.cs b/src/ObservableCollections/IObservableCollection.cs index abc38b5..ca9d4de 100644 --- a/src/ObservableCollections/IObservableCollection.cs +++ b/src/ObservableCollections/IObservableCollection.cs @@ -24,13 +24,14 @@ namespace ObservableCollections public interface ISynchronizedView : IReadOnlyCollection<(T Value, TView View)>, IDisposable { object SyncRoot { get; } + ISynchronizedViewFilter CurrentFilter { get; } event NotifyCollectionChangedEventHandler? RoutingCollectionChanged; event Action? CollectionStateChanged; void AttachFilter(ISynchronizedViewFilter filter, bool invokeAddEventForInitialElements = false); void ResetFilter(Action? resetAction); - INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged(); + INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged(); } public interface ISortableSynchronizedView : ISynchronizedView @@ -44,7 +45,7 @@ namespace ObservableCollections //{ //} - public interface INotifyCollectionChangedSynchronizedView : ISynchronizedView, INotifyCollectionChanged, INotifyPropertyChanged + public interface INotifyCollectionChangedSynchronizedView : IReadOnlyCollection, INotifyCollectionChanged, INotifyPropertyChanged { } diff --git a/src/ObservableCollections/Internal/FreezedView.cs b/src/ObservableCollections/Internal/FreezedView.cs index f9cd98a..1fc1cfc 100644 --- a/src/ObservableCollections/Internal/FreezedView.cs +++ b/src/ObservableCollections/Internal/FreezedView.cs @@ -15,6 +15,11 @@ namespace ObservableCollections.Internal ISynchronizedViewFilter filter; + public ISynchronizedViewFilter CurrentFilter + { + get { lock (SyncRoot) return filter; } + } + public event Action? CollectionStateChanged; public event NotifyCollectionChangedEventHandler? RoutingCollectionChanged; @@ -107,7 +112,7 @@ namespace ObservableCollections.Internal } - public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() + public INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged() { return new NotifyCollectionChangedSynchronizedView(this); } @@ -119,6 +124,11 @@ namespace ObservableCollections.Internal ISynchronizedViewFilter filter; + public ISynchronizedViewFilter CurrentFilter + { + get { lock (SyncRoot) return filter; } + } + public event Action? CollectionStateChanged; public event NotifyCollectionChangedEventHandler? RoutingCollectionChanged; @@ -206,7 +216,7 @@ namespace ObservableCollections.Internal Array.Sort(array, new TViewComparer(viewComparer)); } - public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() + public INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged() { return new NotifyCollectionChangedSynchronizedView(this); } diff --git a/src/ObservableCollections/Internal/NotifyCollectionChangedSynchronizedView.cs b/src/ObservableCollections/Internal/NotifyCollectionChangedSynchronizedView.cs index 25efb63..46234db 100644 --- a/src/ObservableCollections/Internal/NotifyCollectionChangedSynchronizedView.cs +++ b/src/ObservableCollections/Internal/NotifyCollectionChangedSynchronizedView.cs @@ -6,38 +6,22 @@ using System.ComponentModel; namespace ObservableCollections.Internal { - internal class NotifyCollectionChangedSynchronizedView : INotifyCollectionChangedSynchronizedView + internal class NotifyCollectionChangedSynchronizedView : + INotifyCollectionChangedSynchronizedView, + ISynchronizedViewFilter { + static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count"); + readonly ISynchronizedView parent; - static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new PropertyChangedEventArgs("Count"); + readonly ISynchronizedViewFilter currentFilter; public NotifyCollectionChangedSynchronizedView(ISynchronizedView parent) { this.parent = parent; - this.parent.RoutingCollectionChanged += Parent_RoutingCollectionChanged; + currentFilter = parent.CurrentFilter; + parent.AttachFilter(this); } - private void Parent_RoutingCollectionChanged(in NotifyCollectionChangedEventArgs e) - { - CollectionChanged?.Invoke(this, e.ToStandardEventArgs()); - - switch (e.Action) - { - // add, remove, reset will change the count. - case NotifyCollectionChangedAction.Add: - case NotifyCollectionChangedAction.Remove: - case NotifyCollectionChangedAction.Reset: - PropertyChanged?.Invoke(this, CountPropertyChangedEventArgs); - break; - case NotifyCollectionChangedAction.Replace: - case NotifyCollectionChangedAction.Move: - default: - break; - } - } - - public object SyncRoot => parent.SyncRoot; - public int Count => parent.Count; public event NotifyCollectionChangedEventHandler? CollectionChanged; @@ -55,16 +39,43 @@ namespace ObservableCollections.Internal remove { parent.RoutingCollectionChanged -= value; } } - public void AttachFilter(ISynchronizedViewFilter filter, bool invokeAddEventForCurrentElements = false) => parent.AttachFilter(filter, invokeAddEventForCurrentElements); - public void ResetFilter(Action? resetAction) => parent.ResetFilter(resetAction); - public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() => this; public void Dispose() { - this.parent.RoutingCollectionChanged -= Parent_RoutingCollectionChanged; parent.Dispose(); } - public IEnumerator<(T, TView)> GetEnumerator() => parent.GetEnumerator(); + public IEnumerator GetEnumerator() + { + foreach (var (value, view) in parent) + { + yield return view; + } + } + IEnumerator IEnumerable.GetEnumerator() => parent.GetEnumerator(); + + public bool IsMatch(T value, TView view) => currentFilter.IsMatch(value, view); + public void WhenTrue(T value, TView view) => currentFilter.WhenTrue(value, view); + public void WhenFalse(T value, TView view) => currentFilter.WhenFalse(value, view); + + public void OnCollectionChanged(ChangedKind changedKind, T value, TView view, in NotifyCollectionChangedEventArgs eventArgs) + { + currentFilter.OnCollectionChanged(changedKind, value, view, in eventArgs); + + switch (changedKind) + { + case ChangedKind.Add: + CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, view, eventArgs.NewStartingIndex)); + return; + case ChangedKind.Remove: + CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, view, eventArgs.OldStartingIndex)); + break; + case ChangedKind.Move: + CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, view, eventArgs.NewStartingIndex, eventArgs.OldStartingIndex)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(changedKind), changedKind, null); + } + } } } \ No newline at end of file diff --git a/src/ObservableCollections/Internal/SortedView.cs b/src/ObservableCollections/Internal/SortedView.cs index 435e483..747d822 100644 --- a/src/ObservableCollections/Internal/SortedView.cs +++ b/src/ObservableCollections/Internal/SortedView.cs @@ -2,13 +2,17 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; namespace ObservableCollections.Internal { internal class SortedView : ISynchronizedView where TKey : notnull { + public ISynchronizedViewFilter CurrentFilter + { + get { lock (SyncRoot) return filter; } + } + readonly IObservableCollection source; readonly Func transform; readonly Func identitySelector; @@ -85,7 +89,7 @@ namespace ObservableCollections.Internal } } - public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() + public INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged() { lock (SyncRoot) { @@ -192,7 +196,7 @@ namespace ObservableCollections.Internal var view = transform(value); var id = identitySelector(value); list.Add((value, id), (value, view)); - var newIndex = list.IndexOfKey((value, id)); + var newIndex = list.IndexOfKey((value, id)); filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs.Add(value, newIndex)); } diff --git a/src/ObservableCollections/Internal/SortedViewViewComparer.cs b/src/ObservableCollections/Internal/SortedViewViewComparer.cs index 5f91980..ab47dc9 100644 --- a/src/ObservableCollections/Internal/SortedViewViewComparer.cs +++ b/src/ObservableCollections/Internal/SortedViewViewComparer.cs @@ -22,6 +22,11 @@ namespace ObservableCollections.Internal public object SyncRoot { get; } = new object(); + public ISynchronizedViewFilter CurrentFilter + { + get { lock (SyncRoot) return filter; } + } + public SortedViewViewComparer(IObservableCollection source, Func identitySelector, Func transform, IComparer comparer) { this.source = source; @@ -89,7 +94,7 @@ namespace ObservableCollections.Internal } } - public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() + public INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged() { lock (SyncRoot) { @@ -146,7 +151,7 @@ namespace ObservableCollections.Internal var id = identitySelector(value); list.Add((view, id), (value, view)); viewMap.Add(id, view); - var index = list.IndexOfKey((view, id)); + var index = list.IndexOfKey((view, id)); filter.InvokeOnAdd(value, view, NotifyCollectionChangedEventArgs.Add(value, index)); } } diff --git a/src/ObservableCollections/ObservableCollections.csproj b/src/ObservableCollections/ObservableCollections.csproj index 8f698d0..b6404b1 100644 --- a/src/ObservableCollections/ObservableCollections.csproj +++ b/src/ObservableCollections/ObservableCollections.csproj @@ -1,9 +1,9 @@  - netstandard2.0;netstandard2.1;net5.0;net6.0 + netstandard2.0;netstandard2.1;net6.0;net8.0 enable - 10.0 + 12.0 disable diff --git a/src/ObservableCollections/ObservableDictionary.Views.cs b/src/ObservableCollections/ObservableDictionary.Views.cs index 0dd794a..ed250e1 100644 --- a/src/ObservableCollections/ObservableDictionary.Views.cs +++ b/src/ObservableCollections/ObservableDictionary.Views.cs @@ -14,7 +14,7 @@ namespace ObservableCollections // reverse is no used. return new View(this, transform); } - + class View : ISynchronizedView, TView> { readonly ObservableDictionary source; @@ -39,6 +39,11 @@ namespace ObservableCollections public event NotifyCollectionChangedEventHandler>? RoutingCollectionChanged; public event Action? CollectionStateChanged; + public ISynchronizedViewFilter, TView> CurrentFilter + { + get { lock (SyncRoot) return filter; } + } + public int Count { get @@ -91,7 +96,7 @@ namespace ObservableCollections } } - public INotifyCollectionChangedSynchronizedView, TView> WithINotifyCollectionChanged() + public INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged() { lock (SyncRoot) { diff --git a/src/ObservableCollections/ObservableHashSet.Views.cs b/src/ObservableCollections/ObservableHashSet.Views.cs index 8223634..3039bef 100644 --- a/src/ObservableCollections/ObservableHashSet.Views.cs +++ b/src/ObservableCollections/ObservableHashSet.Views.cs @@ -16,6 +16,11 @@ namespace ObservableCollections sealed class View : ISynchronizedView { + public ISynchronizedViewFilter CurrentFilter + { + get { lock (SyncRoot) return filter; } + } + readonly ObservableHashSet source; readonly Func selector; readonly Dictionary dict; @@ -85,7 +90,7 @@ namespace ObservableCollections } } - public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() + public INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged() { lock (SyncRoot) { diff --git a/src/ObservableCollections/ObservableList.Views.cs b/src/ObservableCollections/ObservableList.Views.cs index ed95711..b699171 100644 --- a/src/ObservableCollections/ObservableList.Views.cs +++ b/src/ObservableCollections/ObservableList.Views.cs @@ -16,6 +16,14 @@ namespace ObservableCollections sealed class View : ISynchronizedView { + public ISynchronizedViewFilter CurrentFilter + { + get + { + lock (SyncRoot) { return filter; } + } + } + readonly ObservableList source; readonly Func selector; readonly bool reverse; @@ -89,7 +97,7 @@ namespace ObservableCollections } } - public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() + public INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged() { lock (SyncRoot) { diff --git a/src/ObservableCollections/ObservableList.cs b/src/ObservableCollections/ObservableList.cs index cc024c4..dc33723 100644 --- a/src/ObservableCollections/ObservableList.cs +++ b/src/ObservableCollections/ObservableList.cs @@ -62,7 +62,6 @@ namespace ObservableCollections public event NotifyCollectionChangedEventHandler? CollectionChanged; - public void Add(T item) { lock (SyncRoot) diff --git a/src/ObservableCollections/ObservableQueue.Views.cs b/src/ObservableCollections/ObservableQueue.Views.cs index 5f551b6..bcb9e93 100644 --- a/src/ObservableCollections/ObservableQueue.Views.cs +++ b/src/ObservableCollections/ObservableQueue.Views.cs @@ -28,6 +28,11 @@ namespace ObservableCollections public object SyncRoot { get; } + public ISynchronizedViewFilter CurrentFilter + { + get { lock (SyncRoot) return filter; } + } + public View(ObservableQueue source, Func selector, bool reverse) { this.source = source; @@ -89,7 +94,7 @@ namespace ObservableCollections } } - public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() + public INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged() { lock (SyncRoot) { diff --git a/src/ObservableCollections/ObservableRingBuffer.Views.cs b/src/ObservableCollections/ObservableRingBuffer.Views.cs index 3562549..a0866b4 100644 --- a/src/ObservableCollections/ObservableRingBuffer.Views.cs +++ b/src/ObservableCollections/ObservableRingBuffer.Views.cs @@ -17,6 +17,11 @@ namespace ObservableCollections // used with ObservableFixedSizeRingBuffer internal sealed class View : ISynchronizedView { + public ISynchronizedViewFilter CurrentFilter + { + get { lock (SyncRoot) return filter; } + } + readonly IObservableCollection source; readonly Func selector; readonly bool reverse; @@ -89,7 +94,7 @@ namespace ObservableCollections } } - public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() + public INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged() { lock (SyncRoot) { diff --git a/src/ObservableCollections/ObservableStack.Views.cs b/src/ObservableCollections/ObservableStack.Views.cs index 5175732..a21c1e0 100644 --- a/src/ObservableCollections/ObservableStack.Views.cs +++ b/src/ObservableCollections/ObservableStack.Views.cs @@ -28,6 +28,11 @@ namespace ObservableCollections public object SyncRoot { get; } + public ISynchronizedViewFilter CurrentFilter + { + get { lock (SyncRoot) return filter; } + } + public View(ObservableStack source, Func selector, bool reverse) { this.source = source; @@ -89,7 +94,7 @@ namespace ObservableCollections } } - public INotifyCollectionChangedSynchronizedView WithINotifyCollectionChanged() + public INotifyCollectionChangedSynchronizedView ToNotifyCollectionChanged() { lock (SyncRoot) { diff --git a/tests/ObservableCollections.Tests/ToNotifyCollectionChangedTest.cs b/tests/ObservableCollections.Tests/ToNotifyCollectionChangedTest.cs new file mode 100644 index 0000000..106b6ec --- /dev/null +++ b/tests/ObservableCollections.Tests/ToNotifyCollectionChangedTest.cs @@ -0,0 +1,57 @@ +namespace ObservableCollections.Tests; + +public class ToNotifyCollectionChangedTest +{ + [Fact] + public void ToNotifyCollectionChanged() + { + var list = new ObservableList(); + + list.Add(10); + list.Add(20); + list.Add(30); + + var notify = list.CreateView(x => $"${x}").ToNotifyCollectionChanged(); + + list.Add(40); + list.Add(50); + + using var e = notify.GetEnumerator(); + e.MoveNext().Should().BeTrue(); + e.Current.Should().Be("$10"); + e.MoveNext().Should().BeTrue(); + e.Current.Should().Be("$20"); + e.MoveNext().Should().BeTrue(); + e.Current.Should().Be("$30"); + e.MoveNext().Should().BeTrue(); + e.Current.Should().Be("$40"); + e.MoveNext().Should().BeTrue(); + e.Current.Should().Be("$50"); + e.MoveNext().Should().BeFalse(); + } + + [Fact] + public void ToNotifyCollectionChanged_Filter() + { + var list = new ObservableList(); + + list.Add(1); + list.Add(2); + list.Add(5); + list.Add(3); + + var view = list.CreateView(x => $"${x}"); + var notify = view.ToNotifyCollectionChanged(); + + view.AttachFilter((value, view) => value % 2 == 0); + + list.Add(4); + + using var e = notify.GetEnumerator(); + e.MoveNext().Should().BeTrue(); + e.Current.Should().Be("$2"); + e.MoveNext().Should().BeTrue(); + e.Current.Should().Be("$4"); + e.MoveNext().Should().BeFalse(); + } +} \ No newline at end of file