Add ICollectionEventDispatcher, ToNotifyCollectionChanged(ICollectionEventDispatcher)

This commit is contained in:
neuecc 2024-08-08 18:42:03 +09:00
parent 7e139dc57a
commit a99b10ca49
16 changed files with 327 additions and 40 deletions

View File

@ -206,9 +206,9 @@ public partial class DataTable<T> : ComponentBase, IDisposable
}
```
WPF
WPF/Avalonia
---
Because of data binding in WPF, it is important that the collection is Observable. ObservableCollections high-performance `IObservableCollection<T>` cannot be bind to WPF. Call `ToNotifyCollectionChanged()` to convert it to `INotifyCollectionChanged`. Also, although ObservableCollections and Views are thread-safe, the WPF UI does not support change notifications from different threads. `BindingOperations.EnableCollectionSynchronization` to work safely with change notifications from different threads.
Because of data binding in WPF, it is important that the collection is Observable. ObservableCollections high-performance `IObservableCollection<T>` cannot be bind to WPF. Call `ToNotifyCollectionChanged()` to convert it to `INotifyCollectionChanged`. Also, although ObservableCollections and Views are thread-safe, the WPF UI does not support change notifications from different threads. To`ToNotifyCollectionChanged(IColllectionEventDispatcher)` allows multi thread changed.
```csharp
// WPF simple sample.
@ -222,9 +222,12 @@ public MainWindow()
this.DataContext = this;
list = new ObservableList<int>();
ItemsView = list.CreateView(x => x).ToNotifyCollectionChanged();
BindingOperations.EnableCollectionSynchronization(ItemsView, new object()); // for ui synchronization safety of viewmodel
// for ui synchronization safety of viewmodel
ItemsView = list.CreateView(x => x).ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
// if collection is changed only from ui-thread, can use this overload
// ItemsView = list.CreateView(x => x).ToNotifyCollectionChanged();
}
protected override void OnClosed(EventArgs e)
@ -235,6 +238,22 @@ protected override void OnClosed(EventArgs e)
> WPF can not use SortedView because SortedView can not provide sort event to INotifyCollectionChanged.
`SynchronizationContextCollectionEventDispatcher.Current` is default implementation of `IColllectionEventDispatcher`, it is used `SynchronizationContext.Current` for dispatche ui thread. You can create custom `ICollectionEventDispatcher` to use custom dispatcher object. For example use WPF Dispatcher:
```csharp
public class WpfDispatcherCollection(Dispatcher dispatcher) : ICollectionEventDispatcher
{
public void Post(CollectionEventDispatcherEventArgs ev)
{
dispatcher.InvokeAsync(() =>
{
// notify in dispatcher
ev.Invoke();
});
}
}
```
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.

View File

@ -19,6 +19,7 @@
</Grid>-->
<StackPanel>
<ListBox ItemsSource="{Binding ItemsView}" />
<Button Content="Add" Command="{Binding AddCommand}" />
<Button Content="Clear" Command="{Binding ClearCommand}" />
</StackPanel>
</Window>

View File

@ -2,6 +2,7 @@
using R3;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Text;
@ -16,6 +17,7 @@ using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace WpfApp
{
@ -42,7 +44,7 @@ namespace WpfApp
this.DataContext = new ViewModel();
// Dispatcher.BeginInvoke(
//list = new ObservableList<int>();
@ -73,6 +75,7 @@ namespace WpfApp
{
private ObservableList<int> observableList { get; } = new ObservableList<int>();
public INotifyCollectionChangedSynchronizedView<int> ItemsView { get; }
public ReactiveCommand<Unit> AddCommand { get; } = new ReactiveCommand<Unit>();
public ReactiveCommand<Unit> ClearCommand { get; } = new ReactiveCommand<Unit>();
public ViewModel()
@ -80,9 +83,20 @@ namespace WpfApp
observableList.Add(1);
observableList.Add(2);
ItemsView = observableList.CreateView(x => x).ToNotifyCollectionChanged();
ItemsView = observableList.CreateView(x => x).ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
BindingOperations.EnableCollectionSynchronization(ItemsView, new object());
// ItemsView = observableList.CreateView(x => x).ToNotifyCollectionChanged();
// BindingOperations.EnableCollectionSynchronization(ItemsView, new object());
AddCommand.Subscribe(_ =>
{
ThreadPool.QueueUserWorkItem(_ =>
{
observableList.Add(Random.Shared.Next());
});
});
// var iii = 10;
ClearCommand.Subscribe(_ =>
@ -92,4 +106,15 @@ namespace WpfApp
});
}
}
public class WpfDispatcherCollection(Dispatcher dispatcher) : ICollectionEventDispatcher
{
public void Post(CollectionEventDispatcherEventArgs ev)
{
dispatcher.InvokeAsync(() =>
{
ev.Invoke();
});
}
}
}

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<IsPackable>false</IsPackable>
@ -10,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="R3Extensions.WPF" Version="1.0.4" />
<PackageReference Include="R3Extensions.WPF" Version="1.0.4" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,119 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Threading;
namespace ObservableCollections
{
public interface ICollectionEventDispatcher
{
void Post(CollectionEventDispatcherEventArgs ev);
}
public class SynchronizationContextCollectionEventDispatcher : ICollectionEventDispatcher
{
static readonly Lazy<ICollectionEventDispatcher> current = new Lazy<ICollectionEventDispatcher>(() =>
{
var current = SynchronizationContext.Current;
if (current == null)
{
throw new InvalidOperationException("SynchronizationContext.Current is null");
}
return new SynchronizationContextCollectionEventDispatcher(current);
});
public static readonly ICollectionEventDispatcher Current = current.Value;
readonly SynchronizationContext synchronizationContext;
static readonly SendOrPostCallback callback = SendOrPostCallback;
public SynchronizationContextCollectionEventDispatcher(SynchronizationContext synchronizationContext)
{
this.synchronizationContext = synchronizationContext;
}
public void Post(CollectionEventDispatcherEventArgs ev)
{
synchronizationContext.Post(callback, ev);
}
static void SendOrPostCallback(object? state)
{
var ev = (CollectionEventDispatcherEventArgs)state!;
ev.Invoke();
}
}
internal class DirectCollectionEventDispatcher : ICollectionEventDispatcher
{
public static readonly ICollectionEventDispatcher Instance = new DirectCollectionEventDispatcher();
DirectCollectionEventDispatcher()
{
}
public void Post(CollectionEventDispatcherEventArgs ev)
{
ev.Invoke();
}
}
public class CollectionEventDispatcherEventArgs : NotifyCollectionChangedEventArgs
{
// +state, init;
public object Collection { get; set; } = default!;
public bool IsInvokeCollectionChanged { get; set; }
public bool IsInvokePropertyChanged { get; set; }
internal Action<CollectionEventDispatcherEventArgs> Invoker { get; set; } = default!;
public void Invoke()
{
Invoker.Invoke(this);
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action) : base(action)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, IList? changedItems) : base(action, changedItems)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, object? changedItem) : base(action, changedItem)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, IList newItems, IList oldItems) : base(action, newItems, oldItems)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, IList? changedItems, int startingIndex) : base(action, changedItems, startingIndex)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, object? changedItem, int index) : base(action, changedItem, index)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, object? newItem, object? oldItem) : base(action, newItem, oldItem)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, IList newItems, IList oldItems, int startingIndex) : base(action, newItems, oldItems, startingIndex)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, IList? changedItems, int index, int oldIndex) : base(action, changedItems, index, oldIndex)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, object? changedItem, int index, int oldIndex) : base(action, changedItem, index, oldIndex)
{
}
public CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction action, object? newItem, object? oldItem, int index) : base(action, newItem, oldItem, index)
{
}
}
}

View File

@ -37,6 +37,7 @@ namespace ObservableCollections
void AttachFilter(ISynchronizedViewFilter<T, TView> filter, bool invokeAddEventForInitialElements = false);
void ResetFilter(Action<T, TView>? resetAction);
INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged();
INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher);
}
public interface ISortableSynchronizedView<T, TView> : ISynchronizedView<T, TView>

View File

@ -114,7 +114,12 @@ namespace ObservableCollections.Internal
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
}
}
@ -218,7 +223,12 @@ namespace ObservableCollections.Internal
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged()
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
}
class TComparer : IComparer<(T, TView)>

View File

@ -11,13 +11,16 @@ namespace ObservableCollections.Internal
ISynchronizedViewFilter<T, TView>
{
static readonly PropertyChangedEventArgs CountPropertyChangedEventArgs = new("Count");
static readonly Action<NotifyCollectionChangedEventArgs> raiseChangedEventInvoke = RaiseChangedEvent;
readonly ISynchronizedView<T, TView> parent;
readonly ISynchronizedViewFilter<T, TView> currentFilter;
readonly ICollectionEventDispatcher eventDispatcher;
public NotifyCollectionChangedSynchronizedView(ISynchronizedView<T, TView> parent)
public NotifyCollectionChangedSynchronizedView(ISynchronizedView<T, TView> parent, ICollectionEventDispatcher? eventDispatcher)
{
this.parent = parent;
this.eventDispatcher = eventDispatcher ?? DirectCollectionEventDispatcher.Instance;
currentFilter = parent.CurrentFilter;
parent.AttachFilter(this);
}
@ -62,27 +65,71 @@ namespace ObservableCollections.Internal
{
currentFilter.OnCollectionChanged(args);
if (CollectionChanged == null && PropertyChanged == null) return;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, args.NewView, args.NewViewIndex));
PropertyChanged?.Invoke(this, CountPropertyChangedEventArgs);
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Add, args.NewView, args.NewViewIndex)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = true
});
break;
case NotifyCollectionChangedAction.Remove:
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, args.OldView, args.OldViewIndex));
PropertyChanged?.Invoke(this, CountPropertyChangedEventArgs);
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Remove, args.OldView, args.OldViewIndex)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = true
});
break;
case NotifyCollectionChangedAction.Reset:
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
PropertyChanged?.Invoke(this, CountPropertyChangedEventArgs);
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Reset)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = true
});
break;
case NotifyCollectionChangedAction.Replace:
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, args.NewView, args.OldView, args.NewViewIndex));
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Replace, args.NewView, args.OldView, args.NewViewIndex)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = false
});
break;
case NotifyCollectionChangedAction.Move:
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, args.NewView, args.NewViewIndex, args.OldViewIndex));
eventDispatcher.Post(new CollectionEventDispatcherEventArgs(NotifyCollectionChangedAction.Move, args.NewView, args.NewViewIndex, args.OldViewIndex)
{
Collection = this,
Invoker = raiseChangedEventInvoke,
IsInvokeCollectionChanged = true,
IsInvokePropertyChanged = false
});
break;
}
}
static void RaiseChangedEvent(NotifyCollectionChangedEventArgs e)
{
var e2 = (CollectionEventDispatcherEventArgs)e;
var self = (NotifyCollectionChangedSynchronizedView<T, TView>)e2.Collection;
if (e2.IsInvokeCollectionChanged)
{
self.CollectionChanged?.Invoke(self, e);
}
if (e2.IsInvokePropertyChanged)
{
self.PropertyChanged?.Invoke(self, CountPropertyChangedEventArgs);
}
}
}
}

View File

@ -94,7 +94,15 @@ namespace ObservableCollections.Internal
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
}
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
}
}

View File

@ -99,7 +99,15 @@ namespace ObservableCollections.Internal
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
}
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
}
}

View File

@ -100,7 +100,15 @@ namespace ObservableCollections
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<KeyValuePair<TKey, TValue>, TView>(this);
return new NotifyCollectionChangedSynchronizedView<KeyValuePair<TKey, TValue>, TView>(this, null);
}
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<KeyValuePair<TKey, TValue>, TView>(this, collectionEventDispatcher);
}
}

View File

@ -94,7 +94,15 @@ namespace ObservableCollections
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
}
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
}
}

View File

@ -100,7 +100,15 @@ namespace ObservableCollections
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
}
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
}
}
@ -210,21 +218,21 @@ namespace ObservableCollections
break;
case NotifyCollectionChangedAction.Replace:
// ObservableList does not support replace range
{
var v = (e.NewItem, selector(e.NewItem));
var ov = (e.OldItem, list[e.OldStartingIndex].Item2);
list[e.NewStartingIndex] = v;
filter.InvokeOnReplace(v, ov, e.NewStartingIndex);
break;
}
{
var v = (e.NewItem, selector(e.NewItem));
var ov = (e.OldItem, list[e.OldStartingIndex].Item2);
list[e.NewStartingIndex] = v;
filter.InvokeOnReplace(v, ov, e.NewStartingIndex);
break;
}
case NotifyCollectionChangedAction.Move:
{
var removeItem = list[e.OldStartingIndex];
list.RemoveAt(e.OldStartingIndex);
list.Insert(e.NewStartingIndex, removeItem);
{
var removeItem = list[e.OldStartingIndex];
list.RemoveAt(e.OldStartingIndex);
list.Insert(e.NewStartingIndex, removeItem);
filter.InvokeOnMove(removeItem, e.NewStartingIndex, e.OldStartingIndex);
}
filter.InvokeOnMove(removeItem, e.NewStartingIndex, e.OldStartingIndex);
}
break;
case NotifyCollectionChangedAction.Reset:
list.Clear();

View File

@ -97,7 +97,15 @@ namespace ObservableCollections
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
}
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
}
}

View File

@ -98,7 +98,15 @@ namespace ObservableCollections
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
}
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
}
}

View File

@ -96,7 +96,15 @@ namespace ObservableCollections
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this);
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, null);
}
}
public INotifyCollectionChangedSynchronizedView<TView> ToNotifyCollectionChanged(ICollectionEventDispatcher? collectionEventDispatcher)
{
lock (SyncRoot)
{
return new NotifyCollectionChangedSynchronizedView<T, TView>(this, collectionEventDispatcher);
}
}