add writableview
This commit is contained in:
parent
14893136e5
commit
4cdbe8ce34
56
README.md
56
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<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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user