diff --git a/src/ObservableCollections.R3/ObservableCollectionR3Extensions.View.cs b/src/ObservableCollections.R3/ObservableCollectionR3Extensions.View.cs new file mode 100644 index 0000000..3bc84d2 --- /dev/null +++ b/src/ObservableCollections.R3/ObservableCollectionR3Extensions.View.cs @@ -0,0 +1,509 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics.Tracing; +using System.Runtime.InteropServices; +using System.Threading; +using R3; + +namespace ObservableCollections; + +[StructLayout(LayoutKind.Auto)] +public readonly record struct ViewChangedEvent +{ + public readonly NotifyCollectionChangedAction Action; + public readonly (T Value, TView View) NewItem; + public readonly (T Value, TView View) OldItem; + public readonly int NewStartingIndex; + public readonly int OldStartingIndex; + public readonly SortOperation SortOperation; + + public ViewChangedEvent(NotifyCollectionChangedAction action, (T, TView) newItem, (T, TView) oldItem, int newStartingIndex, int oldStartingIndex, SortOperation sortOperation) + { + Action = action; + NewItem = newItem; + OldItem = oldItem; + NewStartingIndex = newStartingIndex; + OldStartingIndex = oldStartingIndex; + SortOperation = sortOperation; + } +} + +[StructLayout(LayoutKind.Auto)] +public readonly record struct RejectedViewChangedEvent +{ + public readonly RejectedViewChangedAction Action; + public readonly int NewIndex; + public readonly int OldIndex; + + public RejectedViewChangedEvent(RejectedViewChangedAction action, int newIndex, int oldIndex) + { + Action = action; + NewIndex = newIndex; + OldIndex = oldIndex; + } +} + +public static partial class ObservableCollectionR3Extensions +{ + public static Observable ObserveRejected(this ISynchronizedView source, CancellationToken cancellationToken = default) + { + return new SynchronizedViewRejected(source, cancellationToken); + } + + public static Observable> ObserveChanged(this ISynchronizedView source, CancellationToken cancellationToken = default) + { + return new SynchronizedViewChanged(source, cancellationToken); + } + + public static Observable> ObserveAdd(this ISynchronizedView source, CancellationToken cancellationToken = default) + { + return new SynchronizedViewAdd(source, cancellationToken); + } + + public static Observable> ObserveRemove(this ISynchronizedView source, CancellationToken cancellationToken = default) + { + return new SynchronizedViewRemove(source, cancellationToken); + } + + public static Observable> ObserveReplace(this ISynchronizedView source, CancellationToken cancellationToken = default) + { + return new SynchronizedViewReplace(source, cancellationToken); + } + + public static Observable> ObserveMove(this ISynchronizedView source, CancellationToken cancellationToken = default) + { + return new SynchronizedViewMove(source, cancellationToken); + } + + public static Observable> ObserveReset(this ISynchronizedView source, CancellationToken cancellationToken = default) + { + return new SynchronizedViewReset(source, cancellationToken); + } + + public static Observable ObserveClear(this ISynchronizedView source, CancellationToken cancellationToken = default) + { + return new SynchronizedViewClear(source, cancellationToken); + } + + public static Observable<(int Index, int Count)> ObserveReverse(this ISynchronizedView source, CancellationToken cancellationToken = default) + { + return new SynchronizedViewReverse(source, cancellationToken); + } + + public static Observable<(int Index, int Count, IComparer? Comparer)> ObserveSort(this ISynchronizedView source, CancellationToken cancellationToken = default) + { + return new SynchronizedViewSort(source, cancellationToken); + } + + public static Observable ObserveCountChanged(this ISynchronizedView source, bool notifyCurrentCount = false, CancellationToken cancellationToken = default) + { + return new SynchronizedViewCountChanged(source, notifyCurrentCount, cancellationToken); + } +} + +sealed class SynchronizedViewChanged(ISynchronizedView source, CancellationToken cancellationToken) + : Observable> +{ + protected override IDisposable SubscribeCore(Observer> observer) + { + return new _SynchronizedViewChanged(source, observer, cancellationToken); + } + + sealed class _SynchronizedViewChanged( + ISynchronizedView source, + Observer> observer, + CancellationToken cancellationToken) + : SynchronizedViewObserverBase>(source, observer, cancellationToken) + { + protected override void Handler(in SynchronizedViewChangedEventArgs eventArgs) + { + if (eventArgs.IsSingleItem) + { + var newArgs = new ViewChangedEvent( + eventArgs.Action, + eventArgs.NewItem, + eventArgs.OldItem, + eventArgs.NewStartingIndex, + eventArgs.OldStartingIndex, + eventArgs.SortOperation); + + observer.OnNext(newArgs); + } + else + { + if (eventArgs.Action == NotifyCollectionChangedAction.Add) + { + var index = eventArgs.NewStartingIndex; + for (int i = 0; i < eventArgs.NewValues.Length; i++) + { + var newItem = (eventArgs.NewValues[i], eventArgs.NewViews[i]); + var newArgs = new ViewChangedEvent( + eventArgs.Action, + newItem, + default, + index++, + eventArgs.OldStartingIndex, + eventArgs.SortOperation); + + observer.OnNext(newArgs); + } + } + else if (eventArgs.Action == NotifyCollectionChangedAction.Remove) + { + + for (int i = 0; i < eventArgs.OldValues.Length; i++) + { + var oldItem = (eventArgs.OldValues[i], eventArgs.OldViews[i]); + var newArgs = new ViewChangedEvent( + eventArgs.Action, + default, + oldItem, + eventArgs.NewStartingIndex, + eventArgs.OldStartingIndex, // removed, uses same index + eventArgs.SortOperation); + + observer.OnNext(newArgs); + } + } + } + } + } +} + +sealed class SynchronizedViewAdd(ISynchronizedView source, CancellationToken cancellationToken) + : Observable> +{ + protected override IDisposable SubscribeCore(Observer> observer) + { + return new _SynchronizedViewAdd(source, observer, cancellationToken); + } + + sealed class _SynchronizedViewAdd( + ISynchronizedView source, + Observer> observer, + CancellationToken cancellationToken) + : SynchronizedViewObserverBase>(source, observer, cancellationToken) + { + protected override void Handler(in SynchronizedViewChangedEventArgs eventArgs) + { + if (eventArgs.Action == NotifyCollectionChangedAction.Add) + { + if (eventArgs.IsSingleItem) + { + observer.OnNext(new CollectionAddEvent<(T, TView)>(eventArgs.NewStartingIndex, eventArgs.NewItem)); + } + else + { + var index = eventArgs.NewStartingIndex; + for (int i = 0; i < eventArgs.NewValues.Length; i++) + { + observer.OnNext(new CollectionAddEvent<(T, TView)>(index++, (eventArgs.NewValues[i], eventArgs.NewViews[i]))); + } + } + } + } + } +} + +sealed class SynchronizedViewRemove(ISynchronizedView source, CancellationToken cancellationToken) + : Observable> +{ + protected override IDisposable SubscribeCore(Observer> observer) + { + return new _SynchronizedViewRemove(source, observer, cancellationToken); + } + + sealed class _SynchronizedViewRemove( + ISynchronizedView source, + Observer> observer, + CancellationToken cancellationToken) + : SynchronizedViewObserverBase>(source, observer, cancellationToken) + { + protected override void Handler(in SynchronizedViewChangedEventArgs eventArgs) + { + if (eventArgs.Action == NotifyCollectionChangedAction.Remove) + { + if (eventArgs.IsSingleItem) + { + observer.OnNext(new CollectionRemoveEvent<(T, TView)>(eventArgs.OldStartingIndex, eventArgs.OldItem)); + } + else + { + for (int i = 0; i < eventArgs.OldValues.Length; i++) + { + observer.OnNext(new CollectionRemoveEvent<(T, TView)>(eventArgs.OldStartingIndex, (eventArgs.OldValues[i], eventArgs.OldViews[i]))); + } + } + } + } + } +} + +sealed class SynchronizedViewReplace(ISynchronizedView source, CancellationToken cancellationToken) + : Observable> +{ + protected override IDisposable SubscribeCore(Observer> observer) + { + return new _SynchronizedViewReplace(source, observer, cancellationToken); + } + + sealed class _SynchronizedViewReplace( + ISynchronizedView source, + Observer> observer, + CancellationToken cancellationToken) + : SynchronizedViewObserverBase>(source, observer, cancellationToken) + { + protected override void Handler(in SynchronizedViewChangedEventArgs eventArgs) + { + if (eventArgs.Action == NotifyCollectionChangedAction.Replace) + { + observer.OnNext(new CollectionReplaceEvent<(T, TView)>(eventArgs.NewStartingIndex, eventArgs.OldItem, eventArgs.NewItem)); + } + } + } +} + +sealed class SynchronizedViewMove(ISynchronizedView source, CancellationToken cancellationToken) + : Observable> +{ + protected override IDisposable SubscribeCore(Observer> observer) + { + return new _SynchronizedViewMove(source, observer, cancellationToken); + } + + sealed class _SynchronizedViewMove( + ISynchronizedView source, + Observer> observer, + CancellationToken cancellationToken) + : SynchronizedViewObserverBase>(source, observer, cancellationToken) + { + protected override void Handler(in SynchronizedViewChangedEventArgs eventArgs) + { + if (eventArgs.Action == NotifyCollectionChangedAction.Move) + { + observer.OnNext(new CollectionMoveEvent<(T, TView)>(eventArgs.OldStartingIndex, eventArgs.NewStartingIndex, eventArgs.NewItem)); + } + } + } +} + +sealed class SynchronizedViewReset(ISynchronizedView source, CancellationToken cancellationToken) + : Observable> +{ + protected override IDisposable SubscribeCore(Observer> observer) + { + return new _SynchronizedViewReset(source, observer, cancellationToken); + } + + sealed class _SynchronizedViewReset( + ISynchronizedView source, + Observer> observer, + CancellationToken cancellationToken) + : SynchronizedViewObserverBase>(source, observer, cancellationToken) + { + protected override void Handler(in SynchronizedViewChangedEventArgs eventArgs) + { + if (eventArgs.Action == NotifyCollectionChangedAction.Reset) + { + observer.OnNext(new CollectionResetEvent(eventArgs.SortOperation)); + } + } + } +} + +sealed class SynchronizedViewClear(ISynchronizedView source, CancellationToken cancellationToken) + : Observable +{ + protected override IDisposable SubscribeCore(Observer observer) + { + return new _SynchronizedViewClear(source, observer, cancellationToken); + } + + sealed class _SynchronizedViewClear( + ISynchronizedView source, + Observer observer, + CancellationToken cancellationToken) + : SynchronizedViewObserverBase(source, observer, cancellationToken) + { + protected override void Handler(in SynchronizedViewChangedEventArgs eventArgs) + { + if (eventArgs.Action == NotifyCollectionChangedAction.Reset && eventArgs.SortOperation.IsClear) + { + observer.OnNext(Unit.Default); + } + } + } +} + +sealed class SynchronizedViewReverse(ISynchronizedView source, CancellationToken cancellationToken) + : Observable<(int Index, int Count)> +{ + protected override IDisposable SubscribeCore(Observer<(int Index, int Count)> observer) + { + return new _SynchronizedViewReverse(source, observer, cancellationToken); + } + + sealed class _SynchronizedViewReverse( + ISynchronizedView source, + Observer<(int Index, int Count)> observer, + CancellationToken cancellationToken) + : SynchronizedViewObserverBase(source, observer, cancellationToken) + { + protected override void Handler(in SynchronizedViewChangedEventArgs eventArgs) + { + if (eventArgs.Action == NotifyCollectionChangedAction.Reset && eventArgs.SortOperation.IsReverse) + { + observer.OnNext((eventArgs.SortOperation.Index, eventArgs.SortOperation.Count)); + } + } + } +} + +sealed class SynchronizedViewSort(ISynchronizedView source, CancellationToken cancellationToken) + : Observable<(int Index, int Count, IComparer? Comparer)> +{ + protected override IDisposable SubscribeCore(Observer<(int Index, int Count, IComparer? Comparer)> observer) + { + return new _SynchronizedViewSort(source, observer, cancellationToken); + } + + sealed class _SynchronizedViewSort( + ISynchronizedView source, + Observer<(int Index, int Count, IComparer? Comparer)> observer, + CancellationToken cancellationToken) + : SynchronizedViewObserverBase? Comparer)>(source, observer, cancellationToken) + { + protected override void Handler(in SynchronizedViewChangedEventArgs eventArgs) + { + if (eventArgs.Action == NotifyCollectionChangedAction.Reset && eventArgs.SortOperation.IsSort) + { + observer.OnNext(eventArgs.SortOperation.AsTuple()); + } + } + } +} + +sealed class SynchronizedViewCountChanged(ISynchronizedView source, bool notifyCurrentCount, CancellationToken cancellationToken) + : Observable +{ + protected override IDisposable SubscribeCore(Observer observer) + { + return new _SynchronizedViewCountChanged(source, notifyCurrentCount, observer, cancellationToken); + } + + sealed class _SynchronizedViewCountChanged : SynchronizedViewObserverBase + { + int countPrev; + + public _SynchronizedViewCountChanged( + ISynchronizedView source, + bool notifyCurrentCount, + Observer observer, + CancellationToken cancellationToken) : base(source, observer, cancellationToken) + { + this.countPrev = source.Count; + if (notifyCurrentCount) + { + observer.OnNext(source.Count); + } + } + + protected override void Handler(in SynchronizedViewChangedEventArgs eventArgs) + { + switch (eventArgs.Action) + { + case NotifyCollectionChangedAction.Add: + case NotifyCollectionChangedAction.Remove: + case NotifyCollectionChangedAction.Reset when countPrev != source.Count: + observer.OnNext(source.Count); + break; + } + countPrev = source.Count; + } + } +} + + + +sealed class SynchronizedViewRejected(ISynchronizedView source, CancellationToken cancellationToken) + : Observable +{ + protected override IDisposable SubscribeCore(Observer observer) + { + return new _SynchronizedViewRejected(source, observer, cancellationToken); + } + + sealed class _SynchronizedViewRejected : IDisposable + { + readonly ISynchronizedView source; + readonly Observer observer; + readonly CancellationTokenRegistration cancellationTokenRegistration; + readonly Action handlerDelegate; + + public _SynchronizedViewRejected(ISynchronizedView source, Observer observer, CancellationToken cancellationToken) + { + this.source = source; + this.observer = observer; + this.handlerDelegate = Handler; + + source.RejectedViewChanged += handlerDelegate; + + if (cancellationToken.CanBeCanceled) + { + cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state => + { + var s = (_SynchronizedViewRejected)state!; + s.observer.OnCompleted(); + s.Dispose(); + }, this); + } + } + + public void Dispose() + { + source.RejectedViewChanged -= handlerDelegate; + cancellationTokenRegistration.Dispose(); + } + + void Handler(RejectedViewChangedAction rejectedViewChangedAction, int newIndex, int oldIndex) + { + observer.OnNext(new RejectedViewChangedEvent(rejectedViewChangedAction, newIndex, oldIndex)); + } + } +} + +abstract class SynchronizedViewObserverBase : IDisposable +{ + protected readonly ISynchronizedView source; + protected readonly Observer observer; + readonly CancellationTokenRegistration cancellationTokenRegistration; + readonly NotifyViewChangedEventHandler handlerDelegate; + + public SynchronizedViewObserverBase(ISynchronizedView source, Observer observer, CancellationToken cancellationToken) + { + this.source = source; + this.observer = observer; + this.handlerDelegate = Handler; + + source.ViewChanged += handlerDelegate; + + if (cancellationToken.CanBeCanceled) + { + cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state => + { + var s = (SynchronizedViewObserverBase)state!; + s.observer.OnCompleted(); + s.Dispose(); + }, this); + } + } + + public void Dispose() + { + source.ViewChanged -= handlerDelegate; + cancellationTokenRegistration.Dispose(); + } + + protected abstract void Handler(in SynchronizedViewChangedEventArgs eventArgs); +} \ No newline at end of file diff --git a/src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs b/src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs index 61f3f2a..21adea2 100644 --- a/src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs +++ b/src/ObservableCollections.R3/ObservableCollectionR3Extensions.cs @@ -55,7 +55,7 @@ public readonly record struct DictionaryRemoveEvent(TKey Key, TVal public readonly record struct DictionaryReplaceEvent(TKey Key, TValue OldValue, TValue NewValue); -public static class ObservableCollectionR3Extensions +public static partial class ObservableCollectionR3Extensions { public static Observable> ObserveChanged(this IObservableCollection source, CancellationToken cancellationToken = default) { @@ -165,7 +165,7 @@ sealed class ObservableCollectionChanged(IObservableCollection collection, { var newArgs = new CollectionChangedEvent( eventArgs.Action, - eventArgs.NewItem, + item, eventArgs.OldItem, i++, eventArgs.OldStartingIndex, @@ -181,7 +181,7 @@ sealed class ObservableCollectionChanged(IObservableCollection collection, var newArgs = new CollectionChangedEvent( eventArgs.Action, eventArgs.NewItem, - eventArgs.OldItem, + item, eventArgs.NewStartingIndex, eventArgs.OldStartingIndex, // removed, uses same index eventArgs.SortOperation);