From 891319245946203744198d481d817ea6697bc63a Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 16 Oct 2024 14:22:59 +0900 Subject: [PATCH] Add R3 ObserveChanged --- README.md | 30 ++-- sandbox/ConsoleApp/Program.cs | 5 + .../ObservableCollectionR3Extensions.cs | 97 ++++++++++++- .../ObservableList.OptimizeView.cs | 128 +++--------------- .../SynchronizedViewChangedEventArgs.cs | 2 +- 5 files changed, 140 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 01eab90..e02d888 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ ObservableCollections has not just a simple list, there are many more data struc If you want to handle each change event with Rx, you can monitor it with the following method by combining it with [R3](https://github.com/Cysharp/R3): ```csharp +Observable> IObservableCollection.ObserveChanged() Observable> IObservableCollection.ObserveAdd() Observable> IObservableCollection.ObserveRemove() Observable> IObservableCollection.ObserveReplace() @@ -237,7 +238,7 @@ class DescendantComaprer : IComparer Reactive Extensions with R3 --- -Once the R3 extension package is installed, you can subscribe to `ObserveAdd`, `ObserveRemove`, `ObserveReplace`, `ObserveMove`, `ObserveReset`, `ObserveClear`, `ObserveReverse`, `ObserveSort` events as Rx, allowing you to compose events individually. +Once the R3 extension package is installed, you can subscribe to `ObserveChanged`, `ObserveAdd`, `ObserveRemove`, `ObserveReplace`, `ObserveMove`, `ObserveReset`, `ObserveClear`, `ObserveReverse`, `ObserveSort` events as Rx, allowing you to compose events individually. > dotnet add package [ObservableCollections.R3](https://www.nuget.org/packages/ObservableCollections.R3) @@ -317,7 +318,7 @@ Because of data binding in WPF, it is important that the collection is Observabl // WPF simple sample. ObservableList list; -public INotifyCollectionChangedSynchronizedViewList ItemsView { get; set; } +public NotifyCollectionChangedSynchronizedViewList ItemsView { get; set; } public MainWindow() { @@ -366,8 +367,8 @@ public delegate T WritableViewChangedEventHandler(TView newView, T ori public interface IWritableSynchronizedView : ISynchronizedView { - INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter); - INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter, ICollectionEventDispatcher? collectionEventDispatcher); + NotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter); + NotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter, ICollectionEventDispatcher? collectionEventDispatcher); } ``` @@ -564,8 +565,8 @@ public interface ISynchronizedView : IReadOnlyCollection, IDisp void AttachFilter(ISynchronizedViewFilter filter); void ResetFilter(); ISynchronizedViewList ToViewList(); - INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChanged(); - INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher); + NotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChanged(); + NotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher); } ``` @@ -630,10 +631,10 @@ public sealed partial class ObservableList { public IWritableSynchronizedView CreateWritableView(Func transform); - public INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(); - public INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher); - public INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(Func transform, WritableViewChangedEventHandler? converter); - public INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(Func transform, ICollectionEventDispatcher? collectionEventDispatcher, WritableViewChangedEventHandler? converter); + public NotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(); + public NotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher); + public NotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(Func transform, WritableViewChangedEventHandler? converter); + public NotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(Func transform, ICollectionEventDispatcher? collectionEventDispatcher, WritableViewChangedEventHandler? converter); } public delegate T WritableViewChangedEventHandler(TView newView, T originalValue, ref bool setValue); @@ -671,9 +672,18 @@ public interface ISynchronizedViewList : IReadOnlyList, IDispo { } +// Obsolete for public use public interface INotifyCollectionChangedSynchronizedViewList : ISynchronizedViewList, INotifyCollectionChanged, INotifyPropertyChanged { } + +public abstract class NotifyCollectionChangedSynchronizedViewList : + INotifyCollectionChangedSynchronizedViewList, + IWritableSynchronizedViewList, + IList, + IList + { + } ``` License diff --git a/sandbox/ConsoleApp/Program.cs b/sandbox/ConsoleApp/Program.cs index 873c5a4..d506925 100644 --- a/sandbox/ConsoleApp/Program.cs +++ b/sandbox/ConsoleApp/Program.cs @@ -39,6 +39,11 @@ } }); + +list.Clear(); + +list.Add(new() { Age = 99, Name = "tako" }); + // bindable[0] = "takoyaki"; foreach (var item in view) diff --git a/src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs b/src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs index c62a02e..61f3f2a 100644 --- a/src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs +++ b/src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Runtime.InteropServices; using System.Threading; using R3; @@ -27,15 +28,40 @@ public CollectionResetEvent(SortOperation sortOperation) } } +[StructLayout(LayoutKind.Auto)] +public readonly record struct CollectionChangedEvent +{ + public readonly NotifyCollectionChangedAction Action; + public readonly T NewItem; + public readonly T OldItem; + public readonly int NewStartingIndex; + public readonly int OldStartingIndex; + public readonly SortOperation SortOperation; + + public CollectionChangedEvent(NotifyCollectionChangedAction action, T newItem, T oldItem, int newStartingIndex, int oldStartingIndex, SortOperation sortOperation) + { + Action = action; + NewItem = newItem; + OldItem = oldItem; + NewStartingIndex = newStartingIndex; + OldStartingIndex = oldStartingIndex; + SortOperation = sortOperation; + } +} + public readonly record struct DictionaryAddEvent(TKey Key, TValue Value); public readonly record struct DictionaryRemoveEvent(TKey Key, TValue Value); public readonly record struct DictionaryReplaceEvent(TKey Key, TValue OldValue, TValue NewValue); - public static class ObservableCollectionR3Extensions { + public static Observable> ObserveChanged(this IObservableCollection source, CancellationToken cancellationToken = default) + { + return new ObservableCollectionChanged(source, cancellationToken); + } + public static Observable> ObserveAdd(this IObservableCollection source, CancellationToken cancellationToken = default) { return new ObservableCollectionAdd(source, cancellationToken); @@ -102,6 +128,72 @@ public static Observable> ObserveDictionary } } +sealed class ObservableCollectionChanged(IObservableCollection collection, CancellationToken cancellationToken) + : Observable> +{ + protected override IDisposable SubscribeCore(Observer> observer) + { + return new _ObservableCollectionAdd(collection, observer, cancellationToken); + } + + sealed class _ObservableCollectionAdd( + IObservableCollection collection, + Observer> observer, + CancellationToken cancellationToken) + : ObservableCollectionObserverBase>(collection, observer, cancellationToken) + { + protected override void Handler(in NotifyCollectionChangedEventArgs eventArgs) + { + if (eventArgs.IsSingleItem) + { + var newArgs = new CollectionChangedEvent( + eventArgs.Action, + eventArgs.NewItem, + eventArgs.OldItem, + eventArgs.NewStartingIndex, + eventArgs.OldStartingIndex, + eventArgs.SortOperation); + + observer.OnNext(newArgs); + } + else + { + if (eventArgs.Action == NotifyCollectionChangedAction.Add) + { + var i = eventArgs.NewStartingIndex; + foreach (var item in eventArgs.NewItems) + { + var newArgs = new CollectionChangedEvent( + eventArgs.Action, + eventArgs.NewItem, + eventArgs.OldItem, + i++, + eventArgs.OldStartingIndex, + eventArgs.SortOperation); + + observer.OnNext(newArgs); + } + } + else if (eventArgs.Action == NotifyCollectionChangedAction.Remove) + { + foreach (var item in eventArgs.OldItems) + { + var newArgs = new CollectionChangedEvent( + eventArgs.Action, + eventArgs.NewItem, + eventArgs.OldItem, + eventArgs.NewStartingIndex, + eventArgs.OldStartingIndex, // removed, uses same index + eventArgs.SortOperation); + + observer.OnNext(newArgs); + } + } + } + } + } +} + sealed class ObservableCollectionAdd(IObservableCollection collection, CancellationToken cancellationToken) : Observable> { @@ -161,10 +253,9 @@ protected override void Handler(in NotifyCollectionChangedEventArgs eventArgs } else { - var i = eventArgs.OldStartingIndex; foreach (var item in eventArgs.OldItems) { - observer.OnNext(new CollectionRemoveEvent(i++, item)); + observer.OnNext(new CollectionRemoveEvent(eventArgs.OldStartingIndex, item)); // remove uses same index } } } diff --git a/src/ObservableCollections/ObservableList.OptimizeView.cs b/src/ObservableCollections/ObservableList.OptimizeView.cs index 453b7c4..b2a6df6 100644 --- a/src/ObservableCollections/ObservableList.OptimizeView.cs +++ b/src/ObservableCollections/ObservableList.OptimizeView.cs @@ -24,7 +24,7 @@ public partial class ObservableList : IList, IReadOnlyObservableList /// /// Create faster, compact INotifyCollectionChanged view, however it does not support ***Range. /// - public INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChangedSlim() + public NotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChangedSlim() { return new ObservableListSynchronizedViewList(this, null); } @@ -32,7 +32,7 @@ public INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChanged /// /// Create faster, compact INotifyCollectionChanged view, however it does not support ***Range. /// - public INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChangedSlim(ICollectionEventDispatcher? collectionEventDispatcher) + public NotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChangedSlim(ICollectionEventDispatcher? collectionEventDispatcher) { return new ObservableListSynchronizedViewList(this, collectionEventDispatcher); } @@ -48,7 +48,7 @@ public INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChanged //} } -internal sealed class ObservableListSynchronizedViewList : INotifyCollectionChangedSynchronizedViewList, IList, IList +internal sealed class ObservableListSynchronizedViewList : NotifyCollectionChangedSynchronizedViewList { static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count"); static readonly Action raiseChangedEventInvoke = RaiseChangedEvent; @@ -56,8 +56,8 @@ internal sealed class ObservableListSynchronizedViewList : INotifyCollectionC readonly ObservableList parent; readonly ICollectionEventDispatcher eventDispatcher; - public event NotifyCollectionChangedEventHandler? CollectionChanged; - public event PropertyChangedEventHandler? PropertyChanged; + public override event NotifyCollectionChangedEventHandler? CollectionChanged; + public override event PropertyChangedEventHandler? PropertyChanged; public ObservableListSynchronizedViewList(ObservableList parent, ICollectionEventDispatcher? eventDispatcher) { @@ -161,130 +161,42 @@ static void RaiseChangedEvent(NotifyCollectionChangedEventArgs e) } } - public T this[int index] => parent[index]; - - public int Count => parent.Count; - - public IEnumerator GetEnumerator() - { - return parent.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return parent.GetEnumerator(); - } - - public void Dispose() - { - parent.CollectionChanged -= Parent_CollectionChanged; - } - - // IList, IList implementation - - T IList.this[int index] - { - get => ((IReadOnlyList)this)[index]; - set => throw new NotSupportedException(); - } - - object? IList.this[int index] + public override T this[int index] { get { - return this[index]; + return parent[index]; } - set => throw new NotSupportedException(); - } - - static bool IsCompatibleObject(object? value) - { - return value is T || value == null && default(T) == null; - } - - public bool IsReadOnly => true; - - public bool IsFixedSize => false; - - public bool IsSynchronized => true; - - public object SyncRoot => parent.SyncRoot; - - public void Add(T item) - { - throw new NotSupportedException(); - } - - public int Add(object? value) - { - throw new NotImplementedException(); - } - - public void Clear() - { - throw new NotSupportedException(); - } - - public bool Contains(T item) - { - return parent.Contains(item); - } - - public bool Contains(object? value) - { - if (IsCompatibleObject(value)) + set { - return Contains((T)value!); + parent[index] = value; } - return false; } - public void CopyTo(T[] array, int arrayIndex) - { - throw new NotSupportedException(); - } + public override int Count => parent.Count; - public void CopyTo(Array array, int index) + public override IEnumerator GetEnumerator() { - throw new NotImplementedException(); - } - - public int IndexOf(T item) - { - return parent.IndexOf(item); - } - - public int IndexOf(object? item) - { - if (IsCompatibleObject(item)) - { - return IndexOf((T)item!); - } - return -1; - } - - public void Insert(int index, T item) - { - throw new NotSupportedException(); + return parent.GetEnumerator(); } - public void Insert(int index, object? value) + public override void Dispose() { - throw new NotImplementedException(); + parent.CollectionChanged -= Parent_CollectionChanged; } - public bool Remove(T item) + public override void Add(T item) { - throw new NotSupportedException(); + parent.Add(item); } - public void Remove(object? value) + public override bool Contains(T item) { - throw new NotImplementedException(); + return parent.Contains(item); } - public void RemoveAt(int index) + public override int IndexOf(T item) { - throw new NotSupportedException(); + return parent.IndexOf(item); } } \ No newline at end of file diff --git a/src/ObservableCollections/SynchronizedViewChangedEventArgs.cs b/src/ObservableCollections/SynchronizedViewChangedEventArgs.cs index 42aae20..a36cb43 100644 --- a/src/ObservableCollections/SynchronizedViewChangedEventArgs.cs +++ b/src/ObservableCollections/SynchronizedViewChangedEventArgs.cs @@ -174,7 +174,7 @@ internal static void InvokeOnRemoveRange(this ISynchronizedView