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 public interface ISynchronizedView<T, TView> : IReadOnlyCollection<(T Value, TView View)>, IDisposable
{ {
object SyncRoot { get; } object SyncRoot { get; }
ISynchronizedViewFilter<T, TView> CurrentFilter { get; }
event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged; event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
event Action<NotifyCollectionChangedAction>? CollectionStateChanged; event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForInitialElements = false); void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForInitialElements = false);
void ResetFilter(Action<T, TView>? resetAction); void ResetFilter(Action<T, TView>? resetAction);
INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged(); INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged();
} }
public interface ISortableSynchronizedView<T, TView> : ISynchronizedView<T, TView> 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; ISynchronizedViewFilter<T, TView> filter;
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged; public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged; 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); return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
} }
@ -119,6 +124,11 @@ namespace ObservableCollections.Internal
ISynchronizedViewFilter<T, TView> filter; ISynchronizedViewFilter<T, TView> filter;
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
public event Action<NotifyCollectionChangedAction>? CollectionStateChanged; public event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged; public event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
@ -206,7 +216,7 @@ namespace ObservableCollections.Internal
Array.Sort(array, new TViewComparer(viewComparer)); Array.Sort(array, new TViewComparer(viewComparer));
} }
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged() public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{ {
return new NotifyCollectionChangedSynchronizedView<T, TView>(this); return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
} }

View File

@ -6,38 +6,22 @@ using System.ComponentModel;
namespace ObservableCollections.Internal 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; readonly ISynchronizedView<T, TView> parent;
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new PropertyChangedEventArgs("Count"); readonly ISynchronizedViewFilter<T, TView> currentFilter;
public NotifyCollectionChangedSynchronizedView(ISynchronizedView<T, TView> parent) public NotifyCollectionChangedSynchronizedView(ISynchronizedView<T, TView> parent)
{ {
this.parent = 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 int Count => parent.Count;
public event NotifyCollectionChangedEventHandler? CollectionChanged; public event NotifyCollectionChangedEventHandler? CollectionChanged;
@ -55,16 +39,43 @@ namespace ObservableCollections.Internal
remove { parent.RoutingCollectionChanged -= value; } 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() public void Dispose()
{ {
this.parent.RoutingCollectionChanged -= Parent_RoutingCollectionChanged;
parent.Dispose(); 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(); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
namespace ObservableCollections.Internal namespace ObservableCollections.Internal
{ {
internal class SortedView<T, TKey, TView> : ISynchronizedView<T, TView> internal class SortedView<T, TKey, TView> : ISynchronizedView<T, TView>
where TKey : notnull where TKey : notnull
{ {
public ISynchronizedViewFilter<T, TView> CurrentFilter
{
get { lock (SyncRoot) return filter; }
}
readonly IObservableCollection<T> source; readonly IObservableCollection<T> source;
readonly Func<T, TView> transform; readonly Func<T, TView> transform;
readonly Func<T, TKey> identitySelector; readonly Func<T, TKey> identitySelector;
@ -85,7 +89,7 @@ namespace ObservableCollections.Internal
} }
} }
public INotifyCollectionChangedSynchronizedView<T, TView> WithINotifyCollectionChanged() public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{ {
lock (SyncRoot) lock (SyncRoot)
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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