From 4cdbe8ce346925b5859b2963c04e2a8c030d97c4 Mon Sep 17 00:00:00 2001 From: neuecc Date: Thu, 3 Oct 2024 19:29:51 +0900 Subject: [PATCH] add writableview --- README.md | 56 +++++++++ sandbox/ConsoleApp/Program.cs | 27 ++-- .../AlternateIndexList.cs | 5 + .../IObservableCollection.cs | 19 ++- .../ObservableList.Views.cs | 75 ++++++++++- .../SynchronizedViewList.cs | 116 ++++++++++++++++-- 6 files changed, 279 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 94ff256..b12f64f 100644 --- a/README.md +++ b/README.md @@ -359,6 +359,31 @@ public class WpfDispatcherCollection(Dispatcher dispatcher) : ICollectionEventDi Views and ToNotifyCollectionChanged are internally connected by events, so they need to be `Dispose` to release those connections. +Standard Views are readonly. If you want to reflect the results of binding back to the original collection, use `CreateWritableView` to generate an `IWritableSynchronizedView`, and then use `ToWritableNotifyCollectionChanged` to create an `INotifyCollectionChanged` collection from it. + +```csharp +public delegate T WritableViewChangedEventHandler(TView newView, T originalValue, ref bool setValue); + +public interface IWritableSynchronizedView : ISynchronizedView +{ + INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter); + INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter, ICollectionEventDispatcher? collectionEventDispatcher); +} +``` + +`ToWritableNotifyCollectionChanged` accepts a delegate called `WritableViewChangedEventHandler`. `newView` receives the newly bound value. If `setValue` is true, it sets a new value to the original collection, triggering notification propagation. The View is also regenerated. If `T originalValue` is a reference type, you can prevent such propagation by setting `setValue` to `false`. + +```csharp +var list = new ObservableList(); +var view = list.CreateWritableView(x => x.ToString()); +view.AttachFilter(x => x % 2 == 0); +IList notify = view.ToWritableNotifyCollectionChanged((string newView, int originalValue, ref bool setValue) => +{ + setValue = true; // or false + return int.Parse(newView); +}); +``` + Unity --- In Unity projects, you can installing `ObservableCollections` with [NugetForUnity](https://github.com/GlitchEnzo/NuGetForUnity). If R3 integration is required, similarly install `ObservableCollections.R3` via NuGetForUnity. @@ -553,6 +578,37 @@ public static class SynchronizedViewExtensions } ``` +`ObservableList` has writable view. + +```csharp +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 delegate T WritableViewChangedEventHandler(TView newView, T originalValue, ref bool setValue); + +public interface IWritableSynchronizedView : ISynchronizedView +{ + (T Value, TView View) GetAt(int index); + void SetViewAt(int index, TView view); + void SetToSourceCollection(int index, T value); + IWritableSynchronizedViewList ToWritableViewList(WritableViewChangedEventHandler converter); + INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter); + INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter, ICollectionEventDispatcher? collectionEventDispatcher); +} + +public interface IWritableSynchronizedViewList : ISynchronizedViewList +{ + new TView this[int index] { get; set; } +} +``` + Here are definitions for other collections: ```csharp diff --git a/sandbox/ConsoleApp/Program.cs b/sandbox/ConsoleApp/Program.cs index 727c228..5e05fb5 100644 --- a/sandbox/ConsoleApp/Program.cs +++ b/sandbox/ConsoleApp/Program.cs @@ -5,23 +5,30 @@ using System.Linq; using ObservableCollections; using System.Collections; using System.Collections.Generic; +using System.Threading.Tasks.Sources; +var l = new ObservableList(); +var view = l.CreateWritableView(x => x.ToString()); +view.AttachFilter(x => x % 2 == 0); +IList notify = view.ToWritableNotifyCollectionChanged((string newView, int originalValue, ref bool setValue) => +{ + setValue = false; + return int.Parse(newView); +}); -var dict = new ObservableDictionary(); -var view = dict.CreateView(x => x); -view.AttachFilter(x => x.Key == 1); +l.Add(0); +l.Add(1); +l.Add(2); +l.Add(3); +l.Add(4); +l.Add(5); -var view2 = view.ToNotifyCollectionChanged(); -dict.Add(key: 1, value: "foo"); -dict.Add(key: 2, value: "bar"); +notify[1] = "99999"; -foreach (var item in view2) +foreach (var item in view) { Console.WriteLine(item); } -Console.WriteLine("---"); - - //var buffer = new ObservableFixedSizeRingBuffer(5); diff --git a/src/ObservableCollections/AlternateIndexList.cs b/src/ObservableCollections/AlternateIndexList.cs index 621191b..ec3db35 100644 --- a/src/ObservableCollections/AlternateIndexList.cs +++ b/src/ObservableCollections/AlternateIndexList.cs @@ -35,8 +35,11 @@ public class AlternateIndexList : IEnumerable public T this[int index] { get => list[index].Value; + set => CollectionsMarshal.AsSpan(list)[index].Value = value; } + public int GetAlternateIndex(int index) => list[index].AlternateIndex; + public int Count => list.Count; public int Insert(int alternateIndex, T value) @@ -127,6 +130,7 @@ public class AlternateIndexList : IEnumerable return true; } + /// NOTE: when replace successfully, list has been sorted. public bool TryReplaceAlternateIndex(int getAlternateIndex, int setAlternateIndex) { var index = list.BinarySearch(getAlternateIndex); @@ -137,6 +141,7 @@ public class AlternateIndexList : IEnumerable var span = CollectionsMarshal.AsSpan(list); span[index].AlternateIndex = setAlternateIndex; + list.Sort(); // needs sort to keep order return true; } diff --git a/src/ObservableCollections/IObservableCollection.cs b/src/ObservableCollections/IObservableCollection.cs index 680c020..1372d12 100644 --- a/src/ObservableCollections/IObservableCollection.cs +++ b/src/ObservableCollections/IObservableCollection.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; @@ -7,6 +8,7 @@ namespace ObservableCollections { public delegate void NotifyCollectionChangedEventHandler(in NotifyCollectionChangedEventArgs e); public delegate void NotifyViewChangedEventHandler(in SynchronizedViewChangedEventArgs e); + public delegate T WritableViewChangedEventHandler(TView newView, T originalValue, ref bool setValue); public interface IObservableCollection : IReadOnlyCollection { @@ -49,11 +51,26 @@ namespace ObservableCollections INotifyCollectionChangedSynchronizedViewList ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher); } + public interface IWritableSynchronizedView : ISynchronizedView + { + (T Value, TView View) GetAt(int index); + void SetViewAt(int index, TView view); + void SetToSourceCollection(int index, T value); + IWritableSynchronizedViewList ToWritableViewList(WritableViewChangedEventHandler converter); + INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter); + INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter, ICollectionEventDispatcher? collectionEventDispatcher); + } + public interface ISynchronizedViewList : IReadOnlyList, IDisposable { } - public interface INotifyCollectionChangedSynchronizedViewList : ISynchronizedViewList, INotifyCollectionChanged, INotifyPropertyChanged + public interface IWritableSynchronizedViewList : ISynchronizedViewList + { + new TView this[int index] { get; set; } + } + + public interface INotifyCollectionChangedSynchronizedViewList : IList, IList, ISynchronizedViewList, INotifyCollectionChanged, INotifyPropertyChanged { } diff --git a/src/ObservableCollections/ObservableList.Views.cs b/src/ObservableCollections/ObservableList.Views.cs index 5b84818..f37d86d 100644 --- a/src/ObservableCollections/ObservableList.Views.cs +++ b/src/ObservableCollections/ObservableList.Views.cs @@ -14,7 +14,36 @@ namespace ObservableCollections return new View(this, transform); } - internal sealed class View : ISynchronizedView + public IWritableSynchronizedView CreateWritableView(Func transform) + { + return new View(this, transform); + } + + public INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged() + { + return ToWritableNotifyCollectionChanged(null); + } + + public INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher) + { + return ToWritableNotifyCollectionChanged(static x => x, collectionEventDispatcher, static (T newView, T originalValue, ref bool setValue) => + { + setValue = true; + return newView; + }); + } + + public INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(Func transform, WritableViewChangedEventHandler? converter) + { + return ToWritableNotifyCollectionChanged(transform, null!); + } + + public INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(Func transform, ICollectionEventDispatcher? collectionEventDispatcher, WritableViewChangedEventHandler? converter) + { + return new NonFilteredNotifyCollectionChangedSynchronizedViewList(CreateView(transform), collectionEventDispatcher, converter); + } + + internal sealed class View : ISynchronizedView, IWritableSynchronizedView { public ISynchronizedViewFilter Filter { @@ -301,6 +330,50 @@ namespace ObservableCollections } } + #region Writable + + public (T Value, TView View) GetAt(int index) + { + lock (SyncRoot) + { + return list[index]; + } + } + + public void SetViewAt(int index, TView view) + { + lock (SyncRoot) + { + var v = list[index]; + list[index] = (v.Item1, view); + } + } + + public void SetToSourceCollection(int index, T value) + { + lock (SyncRoot) + { + source[index] = value; + } + } + + public IWritableSynchronizedViewList ToWritableViewList(WritableViewChangedEventHandler converter) + { + return new FiltableWritableSynchronizedViewList(this, converter); + } + + public INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter) + { + return new NotifyCollectionChangedSynchronizedViewList(this, null, converter); + } + + public INotifyCollectionChangedSynchronizedViewList ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler converter, ICollectionEventDispatcher? collectionEventDispatcher) + { + return new NotifyCollectionChangedSynchronizedViewList(this, null, converter); + } + + #endregion + sealed class IgnoreViewComparer : IComparer<(T, TView)> { readonly IComparer comparer; diff --git a/src/ObservableCollections/SynchronizedViewList.cs b/src/ObservableCollections/SynchronizedViewList.cs index 4a55eeb..c134034 100644 --- a/src/ObservableCollections/SynchronizedViewList.cs +++ b/src/ObservableCollections/SynchronizedViewList.cs @@ -13,7 +13,7 @@ namespace ObservableCollections; internal class FiltableSynchronizedViewList : ISynchronizedViewList { - readonly ISynchronizedView parent; + protected readonly ISynchronizedView parent; protected readonly AlternateIndexList listView; protected readonly object gate = new object(); @@ -211,7 +211,11 @@ internal class FiltableSynchronizedViewList : ISynchronizedViewList(NotifyCollectionChangedAction.Reset, true)); + } break; default: break; @@ -270,7 +274,7 @@ internal class FiltableSynchronizedViewList : ISynchronizedViewList : ISynchronizedViewList { - readonly ISynchronizedView parent; + protected readonly ISynchronizedView parent; protected readonly List listView; // no filter can be faster protected readonly object gate = new object(); @@ -523,6 +527,43 @@ internal class NonFilteredSynchronizedViewList : ISynchronizedViewList } } +internal class FiltableWritableSynchronizedViewList : FiltableSynchronizedViewList, IWritableSynchronizedViewList +{ + IWritableSynchronizedView writableView; + WritableViewChangedEventHandler converter; + + public FiltableWritableSynchronizedViewList(IWritableSynchronizedView parent, WritableViewChangedEventHandler converter) : base(parent) + { + this.writableView = parent; + this.converter = converter; + } + + public new TView this[int index] + { + get => base[index]; + set + { + lock (gate) + { + var originalIndex = listView.GetAlternateIndex(index); + var (originalValue, _) = writableView.GetAt(originalIndex); + + // update view + writableView.SetViewAt(originalIndex, value); + listView[index] = value; + + var setValue = true; + var newOriginal = converter(value, originalValue, ref setValue); + + if (setValue) + { + writableView.SetToSourceCollection(index, newOriginal); + } + } + } + } +} + internal class NotifyCollectionChangedSynchronizedViewList : FiltableSynchronizedViewList, INotifyCollectionChangedSynchronizedViewList, @@ -532,6 +573,7 @@ internal class NotifyCollectionChangedSynchronizedViewList : static readonly Action raiseChangedEventInvoke = RaiseChangedEvent; readonly ICollectionEventDispatcher eventDispatcher; + WritableViewChangedEventHandler? converter; // null = readonly protected override bool IsSupportRangeFeature => false; // WPF, Avalonia etc does not support range notification @@ -544,6 +586,13 @@ internal class NotifyCollectionChangedSynchronizedViewList : this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance; } + public NotifyCollectionChangedSynchronizedViewList(ISynchronizedView parent, ICollectionEventDispatcher? eventDispatcher, WritableViewChangedEventHandler? converter) + : base(parent) + { + this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance; + this.converter = converter; + } + protected override void OnCollectionChanged(in SynchronizedViewChangedEventArgs args) { if (CollectionChanged == null && PropertyChanged == null) return; @@ -644,7 +693,30 @@ internal class NotifyCollectionChangedSynchronizedViewList : TView IList.this[int index] { get => ((IReadOnlyList)this)[index]; - set => throw new NotSupportedException(); + set + { + if (converter == null || parent is not IWritableSynchronizedView writableView) + { + throw new NotSupportedException("This CollectionView does not support set. If base type is ObservableList, you can use ToWritableSynchronizedView and ToWritableNotifyCollectionChanged."); + } + else + { + var originalIndex = listView.GetAlternateIndex(index); + var (originalValue, _) = writableView.GetAt(originalIndex); + + // update view + writableView.SetViewAt(originalIndex, value); + listView[index] = value; + + var setValue = true; + var newOriginal = converter(value, originalValue, ref setValue); + + if (setValue) + { + writableView.SetToSourceCollection(index, newOriginal); + } + } + } } object? IList.this[int index] @@ -653,7 +725,7 @@ internal class NotifyCollectionChangedSynchronizedViewList : { return this[index]; } - set => throw new NotSupportedException(); + set => ((IList)this)[index] = (TView)value!; } static bool IsCompatibleObject(object? value) @@ -779,6 +851,7 @@ internal class NonFilteredNotifyCollectionChangedSynchronizedViewList static readonly Action raiseChangedEventInvoke = RaiseChangedEvent; readonly ICollectionEventDispatcher eventDispatcher; + readonly WritableViewChangedEventHandler? converter; // null = readonly public event NotifyCollectionChangedEventHandler? CollectionChanged; public event PropertyChangedEventHandler? PropertyChanged; @@ -791,6 +864,13 @@ internal class NonFilteredNotifyCollectionChangedSynchronizedViewList this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance; } + public NonFilteredNotifyCollectionChangedSynchronizedViewList(ISynchronizedView parent, ICollectionEventDispatcher? eventDispatcher, WritableViewChangedEventHandler? converter) + : base(parent) + { + this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance; + this.converter = converter; + } + protected override void OnCollectionChanged(in SynchronizedViewChangedEventArgs args) { if (CollectionChanged == null && PropertyChanged == null) return; @@ -891,7 +971,29 @@ internal class NonFilteredNotifyCollectionChangedSynchronizedViewList TView IList.this[int index] { get => ((IReadOnlyList)this)[index]; - set => throw new NotSupportedException(); + set + { + if (converter == null || parent is not IWritableSynchronizedView writableView) + { + throw new NotSupportedException("This CollectionView does not support set. If base type is ObservableList, you can use ToWritableSynchronizedView and ToWritableNotifyCollectionChanged."); + } + else + { + var (originalValue, _) = writableView.GetAt(index); + + // update view + writableView.SetViewAt(index, value); + listView[index] = value; + + var setValue = true; + var newOriginal = converter(value, originalValue, ref setValue); + + if (setValue) + { + writableView.SetToSourceCollection(index, newOriginal); + } + } + } } object? IList.this[int index] @@ -900,7 +1002,7 @@ internal class NonFilteredNotifyCollectionChangedSynchronizedViewList { return this[index]; } - set => throw new NotSupportedException(); + set => ((IList)this)[index] = (TView)value!; } static bool IsCompatibleObject(object? value)