449 lines
18 KiB
Markdown
Raw Normal View History

2021-08-05 19:04:37 +09:00
# ObservableCollections
2021-09-03 18:14:01 +09:00
[![GitHub Actions](https://github.com/Cysharp/ObservableCollections/workflows/Build-Debug/badge.svg)](https://github.com/Cysharp/ObservableCollections/actions) [![Releases](https://img.shields.io/github/release/Cysharp/ObservableCollections.svg)](https://github.com/Cysharp/ObservableCollections/releases)
2024-02-15 17:13:16 +09:00
ObservableCollections is a high performance observable collections(`ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`) with synchronized views and Observe Extension for [R3](https://github.com/Cysharp/R3).
2021-09-03 18:14:01 +09:00
.NET has [`ObservableCollection<T>`](https://docs.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.observablecollection-1), however it has many lacks of features.
It based `INotifyCollectionChanged`, `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`. There are no generics so everything boxed, allocate memory every time. Also `NotifyCollectionChangedEventArgs` holds all values to `IList` even if it is single value, this also causes allocations. `ObservableCollection<T>` has no Range feature so a lot of wastage occurs when adding multiple values, because it is a single value notification. Also, it is not thread-safe is hard to do linkage with the notifier.
ObservableCollections introduces generics version of `NotifyCollectionChangedEventHandler` and `NotifyCollectionChangedEventArgs`, it using latest C# features(`in`, `readonly ref struct`, `ReadOnlySpan<T>`).
```csharp
public delegate void NotifyCollectionChangedEventHandler<T>(in NotifyCollectionChangedEventArgs<T> e);
public readonly ref struct NotifyCollectionChangedEventArgs<T>
{
public readonly NotifyCollectionChangedAction Action;
public readonly bool IsSingleItem;
public readonly T NewItem;
public readonly T OldItem;
public readonly ReadOnlySpan<T> NewItems;
public readonly ReadOnlySpan<T> OldItems;
public readonly int NewStartingIndex;
public readonly int OldStartingIndex;
}
```
Also, use the interface `IObservableCollection<T>` instead of `INotifyCollectionChanged`. This is guaranteed to be thread-safe and can produce a View that is fully synchronized with the collection.
```csharp
public interface IObservableCollection<T> : IReadOnlyCollection<T>
{
event NotifyCollectionChangedEventHandler<T> CollectionChanged;
object SyncRoot { get; }
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
}
// also exists SortedView
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer);
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> viewComparer);
```
SynchronizedView helps to separate between Model and View (ViewModel). We will use ObservableCollections as the Model and generate SynchronizedView as the View (ViewModel). This architecture can be applied not only to WPF, but also to Blazor, Unity, etc.
![image](https://user-images.githubusercontent.com/46207/131979264-2463403b-0fba-474b-8f49-277c2abe1b05.png)
ObservableCollections has not just a simple list, there are many more data structures. `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`. `RingBuffer`, especially `FixedSizeRingBuffer`, can be achieved with efficient performance when there is rotation (e.g., displaying up to 1000 logs, where old ones are deleted when new ones are added). Of course, the AddRange allows for efficient batch processing of large numbers of additions.
2024-02-15 17:13:16 +09:00
If you want to handle each change event with Rx, you can monitor it with the following method by combining it with [R3](https://github.com/Cysharp/R3):
```csharp
Observable<CollectionAddEvent<T>> IObservableCollection<T>.ObserveAdd()
Observable<CollectionRemoveEvent<T>> IObservableCollection<T>.ObserveRemove()
Observable<CollectionReplaceEvent<T>> IObservableCollection<T>.ObserveReplace()
Observable<CollectionMoveEvent<T>> IObservableCollection<T>.ObserveMove()
Observable<CollectionResetEvent<T>> IObservableCollection<T>.ObserveReset()
```
2021-09-03 18:14:01 +09:00
Getting Started
---
For .NET, use NuGet. For Unity, please read [Unity](#unity) section.
PM> Install-Package [ObservableCollections](https://www.nuget.org/packages/ObservableCollections)
create new `ObservableList<T>`, `ObservableDictionary<TKey, TValue>`, `ObservableHashSet<T>`, `ObservableQueue<T>`, `ObservableStack<T>`, `ObservableRingBuffer<T>`, `ObservableFixedSizeRingBuffer<T>`.
2021-09-03 18:26:37 +09:00
```csharp
2021-09-22 20:19:11 +09:00
// Basic sample, use like ObservableCollection<T>.
// CollectionChanged observes all collection modification
var list = new ObservableList<int>();
list.CollectionChanged += List_CollectionChanged;
list.Add(10);
list.Add(20);
list.AddRange(new[] { 10, 20, 30 });
static void List_CollectionChanged(in NotifyCollectionChangedEventArgs<int> e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
if (e.IsSingleItem)
{
Console.WriteLine(e.NewItem);
}
else
{
foreach (var item in e.NewItems)
{
Console.WriteLine(item);
}
}
break;
// Remove, Replace, Move, Reset
default:
break;
}
}
```
2021-09-22 20:22:00 +09:00
Handling all `CollectionChanged` event manually is hard. We recommend to use `SynchronizedView` that transform element and handling all collection changed event for view synchronize.
2021-09-22 20:19:11 +09:00
```csharp
var list = new ObservableList<int>();
var view = list.CreateView(x => x.ToString() + "$");
list.Add(10);
list.Add(20);
list.AddRange(new[] { 30, 40, 50 });
list[1] = 60;
list.RemoveAt(3);
foreach (var (_, v) in view)
{
// 10$, 60$, 30$, 50$
Console.WriteLine(v);
}
// Dispose view is unsubscribe collection changed event.
view.Dispose();
```
2021-10-12 20:55:57 +09:00
The basic idea behind using ObservableCollections is to create a View. In order to automate this pipeline, the view can be sortable, filtered, and have side effects on the values when they are changed.
2024-02-15 17:13:16 +09:00
Reactive Extensions with R3
---
Once the R3 extension package is installed, you can subscribe to `ObserveAdd`, `ObserveRemove`, `ObserveReplace`, `ObserveMove`, and `ObserveReset` events as Rx, allowing you to compose events individually.
PM> Install-Package [ObservableCollections.R3](https://www.nuget.org/packages/ObservableCollections.R3)
```csharp
using R3;
using ObservableCollections;
var list = new ObservableList<int>();
list.ObserveAdd()
.Subscribe(x =>
{
Console.WriteLine(x);
});
list.Add(10);
list.Add(20);
list.AddRange(new[] { 10, 20, 30 });
```
Since it is not supported by dotnet/reactive, please use the Rx library [R3](https://github.com/Cysharp/R3).
2021-09-22 20:19:11 +09:00
Blazor
---
2021-10-12 20:55:57 +09:00
Since Blazor re-renders the whole thing by StateHasChanged, you may think that Observable collections are unnecessary. However, when you split it into Components, it is beneficial for Component confidence to detect the change and change its own State.
2021-09-22 20:19:11 +09:00
2021-10-12 20:55:57 +09:00
The View selector in ObservableCollections is also useful for converting data to a View that represents a Cell, for example, when creating something like a table.
2021-09-22 20:19:11 +09:00
```csharp
public partial class DataTable<T> : ComponentBase, IDisposable
2021-09-03 18:26:37 +09:00
{
2021-09-22 20:19:11 +09:00
[Parameter, EditorRequired]
public IReadOnlyList<T> Items { get; set; } = default!;
[Parameter, EditorRequired]
2021-09-22 20:22:00 +09:00
public Func<T, Cell[]> DataTemplate { get; set; } = default!;
2021-09-22 20:19:11 +09:00
ISynchronizedView<T, Cell[]> view = default!;
2021-09-03 18:26:37 +09:00
protected override void OnInitialized()
{
2021-09-22 20:19:11 +09:00
if (Items is IObservableCollection<T> observableCollection)
{
2021-10-12 20:55:57 +09:00
// Note: If the table has the ability to sort columns, then it will be automatically sorted using SortedView.
2021-09-22 20:19:11 +09:00
view = observableCollection.CreateView(DataTemplate);
}
else
{
2021-10-12 20:55:57 +09:00
// It is often the case that Items is not Observable.
// In that case, FreezedList is provided to create a View with the same API for normal collections.
2021-09-22 20:19:11 +09:00
var freezedList = new FreezedList<T>(Items);
view = freezedList.CreateView(DataTemplate);
}
2021-09-03 18:26:37 +09:00
2021-10-12 20:55:57 +09:00
// View also has a change notification.
2021-09-22 20:19:11 +09:00
view.CollectionStateChanged += async _ =>
2021-09-03 18:26:37 +09:00
{
2021-09-22 20:19:11 +09:00
await InvokeAsync(StateHasChanged);
2021-09-03 18:26:37 +09:00
};
}
2021-09-22 20:19:11 +09:00
2021-09-03 18:26:37 +09:00
public void Dispose()
{
2021-09-22 20:19:11 +09:00
// unsubscribe.
view.Dispose();
2021-09-03 18:26:37 +09:00
}
}
2021-09-22 20:19:11 +09:00
// .razor, iterate view
@foreach (var (row, cells) in view)
{
<tr>
@foreach (var item in cells)
{
<td>
<CellView Item="item" />
</td>
}
</tr>
}
2021-09-03 18:26:37 +09:00
```
2021-09-03 18:14:01 +09:00
2021-09-22 20:19:11 +09:00
WPF
---
2024-02-15 17:13:16 +09:00
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.
2021-09-22 20:19:11 +09:00
2021-09-03 18:14:01 +09:00
```csharp
// WPF simple sample.
ObservableList<int> list;
public INotifyCollectionChangedSynchronizedView<int> ItemsView { get; set; }
2021-09-03 18:14:01 +09:00
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
list = new ObservableList<int>();
2024-02-15 17:13:16 +09:00
ItemsView = list.CreateView(x => x).ToNotifyCollectionChanged();
2021-09-03 18:14:01 +09:00
BindingOperations.EnableCollectionSynchronization(ItemsView, new object()); // for ui synchronization safety of viewmodel
}
protected override void OnClosed(EventArgs e)
{
ItemsView.Dispose();
}
```
2022-12-17 14:16:10 +09:00
> WPF can not use SortedView because SortedView can not provide sort event to INotifyCollectionChanged.
2021-10-12 20:55:57 +09:00
2021-09-22 20:19:11 +09:00
Unity
---
2024-02-15 17:13:16 +09:00
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.
2024-02-02 17:28:37 +09:00
2021-10-12 20:55:57 +09:00
In Unity, ObservableCollections and Views are useful as CollectionManagers, since they need to convert T to Prefab for display.
Since we need to have side effects on GameObjects, we will prepare a filter and apply an action on changes.
2021-09-22 20:19:11 +09:00
2021-09-03 18:26:37 +09:00
```csharp
// Unity, with filter sample.
public class SampleScript : MonoBehaviour
{
public Button prefab;
public GameObject root;
ObservableRingBuffer<int> collection;
ISynchronizedView<GameObject> view;
void Start()
{
collection = new ObservableRingBuffer<int>();
view = collection.CreateView(x =>
{
var item = GameObject.Instantiate(prefab);
item.GetComponentInChildren<Text>().text = x.ToString();
return item.gameObject;
});
view.AttachFilter(new GameObjectFilter(root));
}
void OnDestroy()
{
view.Dispose();
}
public class GameObjectFilter : ISynchronizedViewFilter<int, GameObject>
{
readonly GameObject root;
public GameObjectFilter(GameObject root)
{
this.root = root;
}
2024-02-22 18:24:15 +09:00
public void OnCollectionChanged(in SynchronizedViewChangedEventArgs<int, GameObject> eventArgs)
2021-09-03 18:26:37 +09:00
{
2024-02-22 18:24:15 +09:00
if (eventArgs.Action == NotifyCollectionChangedAction.Add)
2021-09-03 18:26:37 +09:00
{
2024-02-22 18:24:15 +09:00
eventArgs.NewView.transform.SetParent(root.transform);
2021-09-03 18:26:37 +09:00
}
2024-02-22 18:24:15 +09:00
else if (NotifyCollectionChangedAction.Remove)
2021-09-03 18:26:37 +09:00
{
2024-02-22 18:24:15 +09:00
GameObject.Destroy(eventArgs.OldView);
2021-09-03 18:26:37 +09:00
}
}
public bool IsMatch(int value, GameObject view)
{
2021-10-12 20:55:57 +09:00
return true;
2021-09-03 18:26:37 +09:00
}
public void WhenTrue(int value, GameObject view)
{
view.SetActive(true);
}
public void WhenFalse(int value, GameObject view)
{
view.SetActive(false);
}
}
}
```
2021-10-12 20:55:57 +09:00
It is also possible to manage Order by managing indexes inserted from eventArgs, but it is very difficult with many caveats. If you don't have major performance issues, you can foreach the View itself on CollectionStateChanged (like Blazor) and reorder the transforms. If you have such a architecture, you can also use SortedView.
2021-09-03 18:14:01 +09:00
2022-12-17 14:16:10 +09:00
View/SortedView
2021-09-03 18:14:01 +09:00
---
2021-10-12 20:55:57 +09:00
View can create from `IObservableCollection<T>`, it completely synchronized and thread-safe.
```csharp
public interface IObservableCollection<T> : IReadOnlyCollection<T>
{
// snip...
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
}
```
When reverse = true, foreach view as reverse order(Dictionary, etc. are not supported).
`ISynchronizedView<T, TView>` is `IReadOnlyCollection` and hold both value and view(transformed value when added).
```csharp
public interface ISynchronizedView<T, TView> : IReadOnlyCollection<(T Value, TView View)>, IDisposable
{
object SyncRoot { get; }
event NotifyCollectionChangedEventHandler<T>? RoutingCollectionChanged;
event Action<NotifyCollectionChangedAction>? CollectionStateChanged;
void AttachFilter(ISynchronizedViewFilter<T, TView> filter);
void ResetFilter(Action<T, TView>? resetAction);
2024-02-22 18:24:15 +09:00
INotifyCollectionChangedSynchronizedView<T, TView> ToNotifyCollectionChanged();
2021-10-12 20:55:57 +09:00
}
```
see [filter](#filter) section.
```csharp
var view = transform(value);
2022-12-17 14:16:10 +09:00
if (filter.IsMatch(value, view))
2021-10-12 20:55:57 +09:00
{
filter.WhenTrue(value, view);
}
else
{
filter.WhenFalse(value, view);
}
AddToCollectionInnerStructure(value, view);
filter.OnCollectionChanged(ChangeKind.Add, value, view, eventArgs);
RoutingCollectionChanged(eventArgs);
CollectionStateChanged();
```
2021-09-03 18:14:01 +09:00
2021-10-12 20:55:57 +09:00
```csharp
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<T> comparer)
where TKey : notnull
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, IComparer<TView> viewComparer)
where TKey : notnull
public static ISynchronizedView<T, TView> CreateSortedView<T, TKey, TView, TCompare>(this IObservableCollection<T> source, Func<T, TKey> identitySelector, Func<T, TView> transform, Func<T, TCompare> compareSelector, bool ascending = true)
where TKey : notnull
```
2022-12-17 14:16:10 +09:00
> Notice: foreach ObservableCollections and Views are thread-safe but it uses lock at iterating. In other words, the obtained Enumerator must be Dispose. foreach and LINQ are guaranteed to be Dispose, but be careful when you extract the Enumerator by yourself.
2021-09-22 20:19:11 +09:00
2021-09-03 18:26:37 +09:00
Filter
---
2021-09-03 18:14:01 +09:00
2021-10-12 20:55:57 +09:00
```csharp
public interface ISynchronizedViewFilter<T, TView>
{
bool IsMatch(T value, TView view);
void WhenTrue(T value, TView view);
void WhenFalse(T value, TView view);
2024-02-22 18:24:15 +09:00
void OnCollectionChanged(in SynchronizedViewChangedEventArgs<T, TView> eventArgs);
2021-10-12 20:55:57 +09:00
}
2024-02-22 18:24:15 +09:00
public readonly struct SynchronizedViewChangedEventArgs<T, TView>
2021-10-12 20:55:57 +09:00
{
2024-02-22 18:24:15 +09:00
public readonly NotifyCollectionChangedAction Action = action;
public readonly T NewValue = newValue;
public readonly T OldValue = oldValue;
public readonly TView NewView = newView;
public readonly TView OldView = oldView;
public readonly int NewViewIndex = newViewIndex;
public readonly int OldViewIndex = oldViewIndex;
2021-10-12 20:55:57 +09:00
}
```
2021-09-03 18:14:01 +09:00
Collections
---
2021-10-12 20:55:57 +09:00
```csharp
public sealed partial class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, IObservableCollection<KeyValuePair<TKey, TValue>> where TKey : notnull
public sealed partial class ObservableFixedSizeRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T> where T : notnull
public sealed partial class ObservableHashSet<T> : IReadOnlySet<T>, IReadOnlyCollection<T>, IObservableCollection<T>
where T : notnull
public sealed partial class ObservableList<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
public sealed partial class ObservableQueue<T> : IReadOnlyCollection<T>, IObservableCollection<T>
public sealed partial class ObservableRingBuffer<T> : IList<T>, IReadOnlyList<T>, IObservableCollection<T>
public sealed partial class ObservableStack<T> : IReadOnlyCollection<T>, IObservableCollection<T>
public sealed class RingBuffer<T> : IList<T>, IReadOnlyList<T>
```
2021-09-03 19:50:17 +09:00
Freezed
---
2021-09-03 18:14:01 +09:00
2021-10-12 20:55:57 +09:00
```csharp
public sealed class FreezedList<T> : IReadOnlyList<T>, IFreezedCollection<T>
public sealed class FreezedDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>, IFreezedCollection<KeyValuePair<TKey, TValue>> where TKey : notnull
public interface IFreezedCollection<T>
{
ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform, bool reverse = false);
ISortableSynchronizedView<T, TView> CreateSortableView<TView>(Func<T, TView> transform);
}
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<T> initialSort)
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView>(this IFreezedCollection<T> source, Func<T, TView> transform, IComparer<TView> initialViewSort)
public static ISortableSynchronizedView<T, TView> CreateSortableView<T, TView, TCompare>(this IFreezedCollection<T> source, Func<T, TView> transform, Func<T, TCompare> initialCompareSelector, bool ascending = true)
public static void Sort<T, TView, TCompare>(this ISortableSynchronizedView<T, TView> source, Func<T, TCompare> compareSelector, bool ascending = true)
```
2021-09-03 18:14:01 +09:00
License
---
2022-12-17 14:16:10 +09:00
This library is licensed under the MIT License.