Merge pull request #25 from Cysharp/hadashiA/fix-notify-collection-changed

Change the INotifyCollectionChaneged to IE<TView> from IE<(T, View)>
This commit is contained in:
Yoshifumi Kawai 2024-02-15 15:05:06 +09:00 committed by GitHub
commit 4023a28e4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 168 additions and 48 deletions

View File

@ -24,13 +24,14 @@ namespace ObservableCollections
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<(T Value, TView View)>, IDisposable
{
object SyncRoot { get; }
ISynchronizedViewFilter<T, TView> CurrentFilter { get; }
event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForInitialElements = false);
void ResetFilter(Action<T, TView>? resetAction);
INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged();
INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged();
}
public interface ISortableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
@ -44,7 +45,7 @@ namespace ObservableCollections
//{
//}
public interface INotifyCollectionChangedSynchronizedView<T, TView> : ISynchronizedView<T, TView>, INotifyCollectionChanged, INotifyPropertyChanged
public interface INotifyCollectionChangedSynchronizedView<out TView> : IReadOnlyCollection<TView>, INotifyCollectionChanged, INotifyPropertyChanged
{
}

View File

@ -15,6 +15,11 @@ namespace ObservableCollections.Internal
ISynchronizedViewFilter<T, TView> filter;
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
@ -107,7 +112,7 @@ namespace ObservableCollections.Internal
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}
@ -119,6 +124,11 @@ namespace ObservableCollections.Internal
ISynchronizedViewFilter<T, TView> filter;
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
@ -206,7 +216,7 @@ namespace ObservableCollections.Internal
Array.Sort(array, new TViewComparer(viewComparer));
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
}

View File

@ -6,38 +6,22 @@ using System.ComponentModel;
namespace ObservableCollections.Internal
{
internal class NotifyCollectionChangedSynchronizedView<T, TView> : INotifyCollectionChangedSynchronizedView<T, TView>
internal class NotifyCollectionChangedSynchronizedView<T, TView> :
INotifyCollectionChangedSynchronizedView<TView>,
ISynchronizedViewFilter<T, TView>
{
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count");
readonly ISynchronizedView<T, TView> parent;
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new PropertyChangedEventArgs("Count");
readonly ISynchronizedViewFilter<T, TView> currentFilter;
public NotifyCollectionChangedSynchronizedView(ISynchronizedView<T, TView> parent)
{
this.parent = parent;
this.parent.RoutingCollectionChanged += Parent_RoutingCollectionChanged;
currentFilter = parent.CurrentFilter;
parent.AttachFilter(this);
}
private void Parent_RoutingCollectionChanged(in NotifyCollectionChangedEventArgs<T> 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<T, TView> filter, bool invokeAddEventForCurrentElements = false) => parent.AttachFilter(filter, invokeAddEventForCurrentElements);
public void ResetFilter(Action<T, TView>? resetAction) => parent.ResetFilter(resetAction);
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged() => this;
public void Dispose()
{
this.parent.RoutingCollectionChanged -= Parent_RoutingCollectionChanged;
parent.Dispose();
}
public IEnumerator<(T, TView)> GetEnumerator() => parent.GetEnumerator();
public IEnumerator<TView> 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<T> 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);
}
}
}
}

View File

@ -2,13 +2,17 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace ObservableCollections.Internal
{
internal class SortedView<T, TKey, TView> : ISynchronizedView<T, TView>
where TKey : notnull
{
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
readonly IObservableCollection<T> source;
readonly Func<T, TView> transform;
readonly Func<T, TKey> identitySelector;
@ -85,7 +89,7 @@ namespace ObservableCollections.Internal
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{

View File

@ -22,6 +22,11 @@ namespace ObservableCollections.Internal
public object SyncRoot { get; } = new object();
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public SortedViewViewComparer(IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> comparer)
{
this.source = source;
@ -89,7 +94,7 @@ namespace ObservableCollections.Internal
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net5.0;net6.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
<LangVersion>12.0</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<!-- NuGet Packaging -->

View File

@ -39,6 +39,11 @@ namespace ObservableCollections
public event NotifyCollectionChangedEventHandler<KeyValuePair<TKey, TValue>>? RoutingCollectionChanged;
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public ISynchronizedViewFilter<KeyValuePair<TKey, TValue>, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public int Count
{
get
@ -91,7 +96,7 @@ namespace ObservableCollections
}
}
public INotifyCollectionChangedSynchronizedView<KeyValuePair<TKey, TValue>, TView> WithINotifyCollectionChanged()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{

View File

@ -16,6 +16,11 @@ namespace ObservableCollections
sealed class View<TView> : ISynchronizedView<T, TView>
{
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
readonly ObservableHashSet<T> source;
readonly Func<T, TView> selector;
readonly Dictionary<T, (T, TView)> dict;
@ -85,7 +90,7 @@ namespace ObservableCollections
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{

View File

@ -16,6 +16,14 @@ namespace ObservableCollections
sealed class View<TView> : ISynchronizedView<T, TView>
{
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get
{
lock (SyncRoot) { return filter; }
}
}
readonly ObservableList<T> source;
readonly Func<T, TView> selector;
readonly bool reverse;
@ -89,7 +97,7 @@ namespace ObservableCollections
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{

View File

@ -62,7 +62,6 @@ namespace ObservableCollections
public event NotifyCollectionChangedEventHandler<T>? CollectionChanged;
public void Add(T item)
{
lock (SyncRoot)

View File

@ -28,6 +28,11 @@ namespace ObservableCollections
public object SyncRoot { get; }
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public View(ObservableQueue<T> source, Func<T, TView> selector, bool reverse)
{
this.source = source;
@ -89,7 +94,7 @@ namespace ObservableCollections
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{

View File

@ -17,6 +17,11 @@ namespace ObservableCollections
// used with ObservableFixedSizeRingBuffer
internal sealed class View<TView> : ISynchronizedView<T, TView>
{
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
readonly IObservableCollection<T> source;
readonly Func<T, TView> selector;
readonly bool reverse;
@ -89,7 +94,7 @@ namespace ObservableCollections
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{

View File

@ -28,6 +28,11 @@ namespace ObservableCollections
public object SyncRoot { get; }
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public View(ObservableStack<T> source, Func<T, TView> selector, bool reverse)
{
this.source = source;
@ -89,7 +94,7 @@ namespace ObservableCollections
}
}
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged()
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
lock (SyncRoot)
{

View File

@ -0,0 +1,57 @@
namespace ObservableCollections.Tests;
public class ToNotifyCollectionChangedTest
{
[Fact]
public void ToNotifyCollectionChanged()
{
var list = new ObservableList<int>();
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<int>();
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();
}
}