SkillAgentSearch skills...

ObservableCollections

High performance observable collections and synchronized views, for WPF, Blazor, Unity.

Install / Use

/learn @Cysharp/ObservableCollections
About this skill

Quality Score

0/100

Supported Platforms

Zed

README

ObservableCollections

GitHub Actions Releases

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.

.NET has ObservableCollection<T>, 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 there generics version, NotifyCollectionChangedEventHandler<T> and NotifyCollectionChangedEventArgs<T>, it using latest C# features(in, readonly ref struct, ReadOnlySpan<T>). Also, Sort and Reverse will now be notified.

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;
    public readonly SortOperation<T> SortOperation;
}

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.

public interface IObservableCollection<T> : IReadOnlyCollection<T>
{
    event NotifyCollectionChangedEventHandler<T>? CollectionChanged;
    object SyncRoot { get; }
    ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform);
}

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

The View retains the transformed values. The transform function is called only once during Add, so costly objects that are linked can also be instantiated. Additionally, it has a feature to dynamically show or hide values using filters.

Observable Collections themselves do not implement INotifyCollectionChanged, so they cannot be bound on XAML platforms and the like. However, they can be converted to collections that implement INotifyCollectionChanged using ToNotifyCollectionChanged(), making them suitable for binding.

image

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.

If you want to handle each change event with Rx, you can monitor it with the following method by combining it with R3:

Observable<CollectionChangedEvent<T>> IObservableCollection<T>.ObserveChanged()
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()
Observable<Unit> IObservableCollection<T>.ObserveClear<T>()
Observable<(int Index, int Count)> IObservableCollection<T>.ObserveReverse<T>()
Observable<(int Index, int Count, IComparer<T>? Comparer)> IObservableCollection<T>.ObserveSort<T>()
Observable<int> IObservableCollection<T>.ObserveCountChanged<T>()

Getting Started

For .NET, use NuGet. For Unity, please read Unity section.

dotnet add package ObservableCollections

create new ObservableList<T>, ObservableDictionary<TKey, TValue>, ObservableHashSet<T>, ObservableQueue<T>, ObservableStack<T>, ObservableRingBuffer<T>, ObservableFixedSizeRingBuffer<T>.

// 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;
    }
}

While it is possible to manually handle the CollectionChanged event as shown in the example above, you can also create a SynchronizedView as a collection that holds a separate synchronized value.

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();

The view can modify the objects being enumerated by attaching a Filter.

var list = new ObservableList<int>();
using var view = list.CreateView(x => x.ToString() + "$");

list.Add(1);
list.Add(20);
list.AddRange(new[] { 30, 31, 32 });

// attach filter
view.AttachFilter(x => x % 2 == 0);

foreach (var v in view)
{
    // 20$, 30$, 32$
    Console.WriteLine(v);
}

// attach other filter(removed previous filter)
view.AttachFilter(x => x % 2 == 1);

foreach (var v in view)
{
    // 1$, 31$
    Console.WriteLine(v);
}

// Count shows filtered length
Console.WriteLine(view.Count); // 2

The View only allows iteration and Count; it cannot be accessed via an indexer. If indexer access is required, you need to convert it using ToViewList(). Additionally, ToNotifyCollectionChanged() converts it to a synchronized view that implements INotifyCollectionChanged, which is necessary for XAML binding, in addition to providing indexer access.

// Queue <-> List Synchronization
var queue = new ObservableQueue<int>();

queue.Enqueue(1);
queue.Enqueue(10);
queue.Enqueue(100);
queue.Enqueue(1000);
queue.Enqueue(10000);

using var view = queue.CreateView(x => x.ToString() + "$");

using var viewList = view.ToViewList();

Console.WriteLine(viewList[2]); // 100$

In the case of ObservableList, calls to Sort and Reverse can also be synchronized with the view.

var list = new ObservableList<int> { 1, 301, 20, 50001, 4000 };
using var view = list.CreateView(x => x.ToString() + "$");

view.AttachFilter(x => x % 2 == 0);

foreach (var v in view)
{
    // 20$, 4000$
    Console.WriteLine(v);
}

// Reverse operations on the list will affect the view
list.Reverse();

foreach (var v in view)
{
    // 4000$, 20$
    Console.WriteLine(v);
}

// remove filter
view.ResetFilter();

// The reverse operation is also reflected in the values hidden by the filter
foreach (var v in view)
{
    // 4000$, 50001$, 20$, 301$, 1$
    Console.WriteLine(v);
}

// also affect Sort Operations    
list.Sort();
foreach (var v in view)
{
    // 1$, 20$, 301$, 4000$, 50001$
    Console.WriteLine(v);
}

// you can use custom comparer
list.Sort(new DescendantComaprer());
foreach (var v in view)
{
    // 50001$, 4000$, 301$, 20$, 1$
    Console.WriteLine(v);
}

class DescendantComaprer : IComparer<int>
{
    public int Compare(int x, int y)
    {
        return y.CompareTo(x);
    }
}

Reactive Extensions with R3

Once the R3 extension package is installed, you can subscribe to ObserveChanged, ObserveAdd, ObserveRemove, ObserveReplace, ObserveMove, ObserveReset, ObserveClear, ObserveReverse, ObserveSort, ObserveCounteChanged events as Rx, allowing you to compose events individually.

dotnet add package ObservableCollections.R3

using R3;
using ObservableCollections;

var list = new ObservableList<int>();
list.ObserveAdd()
    .Subscribe(x =>
    {
        Console.WriteLine(x);
    });

list.Add(10);
list.Add(2
View on GitHub
GitHub Stars946
CategoryDevelopment
Updated18h ago
Forks66

Languages

C#

Security Score

95/100

Audited on Apr 6, 2026

No findings