add writableview

This commit is contained in:
neuecc 2024-10-03 19:29:51 +09:00
parent 14893136e5
commit 4cdbe8ce34
6 changed files with 279 additions and 19 deletions

View File

@ -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<T, TView>(TView newView, T originalValue, ref bool setValue);
public interface IWritableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
{
INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter);
INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> 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<int>();
var view = list.CreateWritableView(x => x.ToString());
view.AttachFilter(x => x % 2 == 0);
IList<string> 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<T>` has writable view.
```csharp
public sealed partial class ObservableList<T>
{
public IWritableSynchronizedView<T, TView> CreateWritableView<TView>(Func<T, TView> transform);
public INotifyCollectionChangedSynchronizedViewList<T> ToWritableNotifyCollectionChanged();
public INotifyCollectionChangedSynchronizedViewList<T> ToWritableNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
public INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged<TView>(Func<T, TView> transform, WritableViewChangedEventHandler<T, TView>? converter);
public INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged<TView>(Func<T, TView> transform, ICollectionEventDispatcher? collectionEventDispatcher, WritableViewChangedEventHandler<T, TView>? converter);
}
public delegate T WritableViewChangedEventHandler<T, TView>(TView newView, T originalValue, ref bool setValue);
public interface IWritableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
{
(T Value, TView View) GetAt(int index);
void SetViewAt(int index, TView view);
void SetToSourceCollection(int index, T value);
IWritableSynchronizedViewList<TView> ToWritableViewList(WritableViewChangedEventHandler<T, TView> converter);
INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter);
INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter, ICollectionEventDispatcher? collectionEventDispatcher);
}
public interface IWritableSynchronizedViewList<TView> : ISynchronizedViewList<TView>
{
new TView this[int index] { get; set; }
}
```
Here are definitions for other collections:
```csharp

View File

@ -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<int>();
var view = l.CreateWritableView(x => x.ToString());
view.AttachFilter(x => x % 2 == 0);
IList<string> notify = view.ToWritableNotifyCollectionChanged((string newView, int originalValue, ref bool setValue) =>
{
setValue = false;
return int.Parse(newView);
});
var dict = new ObservableDictionary<int, string>();
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<int>(5);

View File

@ -35,8 +35,11 @@ public class AlternateIndexList<T> : IEnumerable<T>
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<T> : IEnumerable<T>
return true;
}
/// <summary>NOTE: when replace successfully, list has been sorted.</summary>
public bool TryReplaceAlternateIndex(int getAlternateIndex, int setAlternateIndex)
{
var index = list.BinarySearch(getAlternateIndex);
@ -137,6 +141,7 @@ public class AlternateIndexList<T> : IEnumerable<T>
var span = CollectionsMarshal.AsSpan(list);
span[index].AlternateIndex = setAlternateIndex;
list.Sort(); // needs sort to keep order
return true;
}

View File

@ -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<T>(in NotifyCollectionChangedEventArgs<T> e);
public delegate void NotifyViewChangedEventHandler<T, TView>(in SynchronizedViewChangedEventArgs<T, TView> e);
public delegate T WritableViewChangedEventHandler<T, TView>(TView newView, T originalValue, ref bool setValue);
public interface IObservableCollection<T> : IReadOnlyCollection<T>
{
@ -49,11 +51,26 @@ namespace ObservableCollections
INotifyCollectionChangedSynchronizedViewList<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
}
public interface IWritableSynchronizedView<T, TView> : ISynchronizedView<T, TView>
{
(T Value, TView View) GetAt(int index);
void SetViewAt(int index, TView view);
void SetToSourceCollection(int index, T value);
IWritableSynchronizedViewList<TView> ToWritableViewList(WritableViewChangedEventHandler<T, TView> converter);
INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter);
INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter, ICollectionEventDispatcher? collectionEventDispatcher);
}
public interface ISynchronizedViewList<out TView> : IReadOnlyList<TView>, IDisposable
{
}
public interface INotifyCollectionChangedSynchronizedViewList<out TView> : ISynchronizedViewList<TView>, INotifyCollectionChanged, INotifyPropertyChanged
public interface IWritableSynchronizedViewList<TView> : ISynchronizedViewList<TView>
{
new TView this[int index] { get; set; }
}
public interface INotifyCollectionChangedSynchronizedViewList<TView> : IList<TView>, IList, ISynchronizedViewList<TView>, INotifyCollectionChanged, INotifyPropertyChanged
{
}

View File

@ -14,7 +14,36 @@ namespace ObservableCollections
return new View<TView>(this, transform);
}
internal sealed class View<TView> : ISynchronizedView<T, TView>
public IWritableSynchronizedView<T, TView> CreateWritableView<TView>(Func<T, TView> transform)
{
return new View<TView>(this, transform);
}
public INotifyCollectionChangedSynchronizedViewList<T> ToWritableNotifyCollectionChanged()
{
return ToWritableNotifyCollectionChanged(null);
}
public INotifyCollectionChangedSynchronizedViewList<T> ToWritableNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
return ToWritableNotifyCollectionChanged(static x => x, collectionEventDispatcher, static (T newView, T originalValue, ref bool setValue) =>
{
setValue = true;
return newView;
});
}
public INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged<TView>(Func<T, TView> transform, WritableViewChangedEventHandler<T, TView>? converter)
{
return ToWritableNotifyCollectionChanged(transform, null!);
}
public INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged<TView>(Func<T, TView> transform, ICollectionEventDispatcher? collectionEventDispatcher, WritableViewChangedEventHandler<T, TView>? converter)
{
return new NonFilteredNotifyCollectionChangedSynchronizedViewList<T, TView>(CreateView(transform), collectionEventDispatcher, converter);
}
internal sealed class View<TView> : ISynchronizedView<T, TView>, IWritableSynchronizedView<T, TView>
{
public ISynchronizedViewFilter<T> 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<TView> ToWritableViewList(WritableViewChangedEventHandler<T, TView> converter)
{
return new FiltableWritableSynchronizedViewList<T, TView>(this, converter);
}
public INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter)
{
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, null, converter);
}
public INotifyCollectionChangedSynchronizedViewList<TView> ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T, TView> converter, ICollectionEventDispatcher? collectionEventDispatcher)
{
return new NotifyCollectionChangedSynchronizedViewList<T, TView>(this, null, converter);
}
#endregion
sealed class IgnoreViewComparer : IComparer<(T, TView)>
{
readonly IComparer<T> comparer;

View File

@ -13,7 +13,7 @@ namespace ObservableCollections;
internal class FiltableSynchronizedViewList<T, TView> : ISynchronizedViewList<TView>
{
readonly ISynchronizedView<T, TView> parent;
protected readonly ISynchronizedView<T, TView> parent;
protected readonly AlternateIndexList<TView> listView;
protected readonly object gate = new object();
@ -211,7 +211,11 @@ internal class FiltableSynchronizedViewList<T, TView> : ISynchronizedViewList<TV
break;
case RejectedViewChangedAction.Move:
if (oldIndex == -1) return;
listView.TryReplaceAlternateIndex(oldIndex, index);
if (listView.TryReplaceAlternateIndex(oldIndex, index))
{
// replace alternate-index changes order so needs Reset
OnCollectionChanged(new SynchronizedViewChangedEventArgs<T, TView>(NotifyCollectionChangedAction.Reset, true));
}
break;
default:
break;
@ -270,7 +274,7 @@ internal class FiltableSynchronizedViewList<T, TView> : ISynchronizedViewList<TV
internal class NonFilteredSynchronizedViewList<T, TView> : ISynchronizedViewList<TView>
{
readonly ISynchronizedView<T, TView> parent;
protected readonly ISynchronizedView<T, TView> parent;
protected readonly List<TView> listView; // no filter can be faster
protected readonly object gate = new object();
@ -523,6 +527,43 @@ internal class NonFilteredSynchronizedViewList<T, TView> : ISynchronizedViewList
}
}
internal class FiltableWritableSynchronizedViewList<T, TView> : FiltableSynchronizedViewList<T, TView>, IWritableSynchronizedViewList<TView>
{
IWritableSynchronizedView<T, TView> writableView;
WritableViewChangedEventHandler<T, TView> converter;
public FiltableWritableSynchronizedViewList(IWritableSynchronizedView<T, TView> parent, WritableViewChangedEventHandler<T, TView> 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<T, TView> :
FiltableSynchronizedViewList<T, TView>,
INotifyCollectionChangedSynchronizedViewList<TView>,
@ -532,6 +573,7 @@ internal class NotifyCollectionChangedSynchronizedViewList<T, TView> :
static readonly Action<NotifyCollectionChangedEventArgs> raiseChangedEventInvoke = RaiseChangedEvent;
readonly ICollectionEventDispatcher eventDispatcher;
WritableViewChangedEventHandler<T, TView>? converter; // null = readonly
protected override bool IsSupportRangeFeature => false; // WPF, Avalonia etc does not support range notification
@ -544,6 +586,13 @@ internal class NotifyCollectionChangedSynchronizedViewList<T, TView> :
this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance;
}
public NotifyCollectionChangedSynchronizedViewList(ISynchronizedView<T, TView> parent, ICollectionEventDispatcher? eventDispatcher, WritableViewChangedEventHandler<T, TView>? converter)
: base(parent)
{
this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance;
this.converter = converter;
}
protected override void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, TView> args)
{
if (CollectionChanged == null && PropertyChanged == null) return;
@ -644,7 +693,30 @@ internal class NotifyCollectionChangedSynchronizedViewList<T, TView> :
TView IList<TView>.this[int index]
{
get => ((IReadOnlyList<TView>)this)[index];
set => throw new NotSupportedException();
set
{
if (converter == null || parent is not IWritableSynchronizedView<T,TView> writableView)
{
throw new NotSupportedException("This CollectionView does not support set. If base type is ObservableList<T>, 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<T, TView> :
{
return this[index];
}
set => throw new NotSupportedException();
set => ((IList<TView>)this)[index] = (TView)value!;
}
static bool IsCompatibleObject(object? value)
@ -779,6 +851,7 @@ internal class NonFilteredNotifyCollectionChangedSynchronizedViewList<T, TView>
static readonly Action<NotifyCollectionChangedEventArgs> raiseChangedEventInvoke = RaiseChangedEvent;
readonly ICollectionEventDispatcher eventDispatcher;
readonly WritableViewChangedEventHandler<T, TView>? converter; // null = readonly
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public event PropertyChangedEventHandler? PropertyChanged;
@ -791,6 +864,13 @@ internal class NonFilteredNotifyCollectionChangedSynchronizedViewList<T, TView>
this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance;
}
public NonFilteredNotifyCollectionChangedSynchronizedViewList(ISynchronizedView<T, TView> parent, ICollectionEventDispatcher? eventDispatcher, WritableViewChangedEventHandler<T, TView>? converter)
: base(parent)
{
this.eventDispatcher = eventDispatcher ?? InlineCollectionEventDispatcher.Instance;
this.converter = converter;
}
protected override void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, TView> args)
{
if (CollectionChanged == null && PropertyChanged == null) return;
@ -891,7 +971,29 @@ internal class NonFilteredNotifyCollectionChangedSynchronizedViewList<T, TView>
TView IList<TView>.this[int index]
{
get => ((IReadOnlyList<TView>)this)[index];
set => throw new NotSupportedException();
set
{
if (converter == null || parent is not IWritableSynchronizedView<T, TView> writableView)
{
throw new NotSupportedException("This CollectionView does not support set. If base type is ObservableList<T>, 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<T, TView>
{
return this[index];
}
set => throw new NotSupportedException();
set => ((IList<TView>)this)[index] = (TView)value!;
}
static bool IsCompatibleObject(object? value)