ObservableCollections

repository·master·Indexed 21 days ago

https://github.com/cysharp/observablecollections

A high-performance, thread-safe library of observable data structures for .NET. It provides optimized alternatives to the standard ObservableCollection<T>, including ObservableList<T>, ObservableDictionary<TKey, TValue>, ObservableHashSet<T>, ObservableQueue<T>, ObservableStack<T>, and RingBuffers. Key features include generic-based notifications to avoid boxing, range operations, a SynchronizedView system for Model-View separation, and integration with R3 for Reactive Extensions.

Tokens
5.1K
Snippets
17
Records
22
Agent score
77%

What's inside ObservableCollections

  1. Overview of ObservableCollections

    master

    ObservableCollections is a high-performance collection library providing thread-safe, generic observable data structures. Unlike the standard .NET ObservableCollection<T>, it uses generics to avoid boxing and allocations, supports range operations (like AddRange), and provides a SynchronizedView mechanism to separate Model from View (ViewModel).

    Supported collection types include:

    • ObservableList<T>
    • ObservableDictionary<TKey, TValue>
    • ObservableHashSet<T>
    • ObservableQueue<T>
    • ObservableStack<T>
    • ObservableRingBuffer<T>
    • ObservableFixedSizeRingBuffer<T>
  2. Use SynchronizedView to separate Model and View

    master

    You can use CreateView to generate a ISynchronizedView<T, TView>. The view holds transformed values (e.g., converting a Model object to a ViewModel or a UI element) and stays synchronized with the underlying collection. The transform function is called only once during an Add operation, making it efficient for costly object instantiation.

    Key features of a View:

    • Filtering: Use AttachFilter to dynamically show/hide items. Count returns the filtered count.
    • Synchronization: Operations like Sort and Reverse on the source collection are reflected in the view.
    • Lifecycle: Views must be Dispose()'d to unsubscribe from the collection's change events.
    var list = new ObservableList<int>();
    // Transform int to string with a suffix
    var view = list.CreateView(x => x.ToString() + "$");
    
    list.Add(10);
    list.AddRange(new[] { 30, 40, 50 });
    
    // Attach a filter to show only even numbers
    view.AttachFilter(x => x % 2 == 0);
    
    foreach (var v in view)
    {
        // Output: 10$, 30$, 40$, 50$
        Console.WriteLine(v);
    }
    
    view.Dispose();
  3. Install ObservableCollections via NuGet

    master

    For .NET projects, install the core package using the dotnet CLI:

    dotnet add package ObservableCollections

    If you require Reactive Extensions (Rx) integration with R3, install the R3 extension package:

    dotnet add package ObservableCollections.R3
    dotnet add package ObservableCollections
  4. Use Writable Views to bind UI changes back to the Model

    master

    Standard views are read-only. If you need to reflect changes made in a UI (like a text box binding) back to the original collection, use CreateWritableView to generate an IWritableSynchronizedView<T, TView>.

    Use ToWritableNotifyCollectionChanged with a WritableViewChangedEventHandler to define how changes to the view should be applied to the source collection.

    In the handler:

    • Set setValue = true to propagate the change to the original collection.
    • Set setValue = false to prevent propagation (useful for mutable reference types where you only want to update the view's local state).
    var list = new ObservableList<Person> { new() { Age = 10, Name = "John" } };
    var view = list.CreateWritableView(x => x.Name);
    
    // Convert to a bindable collection for XAML
    var bindable = view.ToWritableNotifyCollectionChanged((string? newName, Person original, ref bool setValue) =>
    {
        if (setValue) 
        {
            original.Name = newName;
            // You can choose to set setValue to false to avoid redundant notifications
            setValue = false;
            return original;
        }
        return new Person { Name = newName };
    });
    
    bindable[0] = "Bob"; // Updates the original list's Person name to "Bob"
  5. Use ObservableCollections in Unity

    master

    In Unity, install the package via [NuGetForUnity].

    ObservableCollections are ideal for CollectionManagers where you need to transform data into Prefabs. Because CreateView is only called once per item, you can instantiate GameObjects and link them to the collection items efficiently.

    Use the ViewChanged event on the view to handle the lifecycle of the instantiated GameObjects (e.g., destroying the GameObject when an item is removed from the collection).

    // Inside a MonoBehaviour
    void Start()
    {
        collection = new ObservableRingBuffer<int>();
        view = collection.CreateView(x => {
            var item = GameObject.Instantiate(prefab);
            return item.gameObject;
        });
    
        view.ViewChanged += (in SynchronizedViewChangedEventArgs<int, GameObject> e) => {
            if (e.Action == NotifyCollectionChangedAction.Remove) {
                GameObject.Destroy(e.OldItem.View);
            }
        };
    }
  6. Convert ObservableCollections for XAML binding (WPF/Avalonia/WinUI)

    master

    Since IObservableCollection<T> does not implement INotifyCollectionChanged, it cannot be directly bound to XAML. Use ToNotifyCollectionChanged() to convert it into a collection suitable for binding.

    Thread Safety: While the collections are thread-safe, XAML platforms require notifications to occur on the UI thread. Use the ICollectionEventDispatcher overload to handle this:

    1. Automatic Dispatching: Use ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current) to use the current SynchronizationContext.
    2. Custom Dispatching: Implement ICollectionEventDispatcher to use a specific dispatcher (e.g., WPF Dispatcher).

    Performance Options:

    • ToNotifyCollectionChanged(): Standard conversion.
    • ToNotifyCollectionChangedSlim(): The fastest and most memory-efficient option as it shares the actual data, but it does not support range operations (AddRange, InsertRange, RemoveRange), which will cause runtime exceptions in XAML platforms.
    // WPF example with automatic synchronization context dispatching
    ObservableList<int> list = new ObservableList<int>();
    ItemsView = list.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);
  7. Integrate with R3 (Reactive Extensions)

    master

    By installing ObservableCollections.R3, you can treat collection changes as observable streams. This allows you to compose complex logic using Rx operators.

    Available observation methods on IObservableCollection<T>:

    • ObserveChanged(): All changes.
    • ObserveAdd(), ObserveRemove(), ObserveReplace(), ObserveMove(), ObserveReset(), ObserveClear(): Specific change types.
    • ObserveReverse(): Notifies when the collection is reversed.
    • ObserveSort(): Notifies when the collection is sorted.
    • ObserveCountChanged(): Notifies when the count changes.
    using R3;
    using ObservableCollections;
    
    var list = new ObservableList<int>();
    list.ObserveAdd()
        .Subscribe(x => Console.WriteLine($"Added: {x}"));
    
    list.Add(10);
  8. Reference: NotifyCollectionChangedEventArgs<T> structure

    master

    The NotifyCollectionChangedEventArgs<T> is a readonly ref struct used in the CollectionChanged event. It provides high-performance access to change details without allocations.

    Data Contract:

    • If IsSingleItem is true: Use NewItem and OldItem.
    • If IsSingleItem is false: Use NewItems and OldItems (as ReadOnlySpan<T>).

    Action-specific fields:

    • Add: NewItem/NewItems, NewStartingIndex.
    • Remove: OldItem/OldItems, OldStartingIndex.
    • Replace: NewItem/NewItems, OldItem/OldItems, NewStartingIndex, OldStartingIndex (indices are the same).
    • Move: NewStartingIndex, OldStartingIndex.
    • Reset: Uses SortOperation<T> to indicate if the reset was due to IsClear, IsReverse, or IsSort.
  9. Dispose an ObservableDictionary view

    master

    Because a view created via CreateView subscribes to the CollectionChanged event of the source ObservableDictionary, it is important to call Dispose() on the view when it is no longer needed. This prevents memory leaks by unsubscribing from the source dictionary.

    using (var view = dictionary.CreateView(x => x.Value.Name)) 
    {
        // Use the view
    }
    // View is automatically unsubscribed from dictionary here
  10. Use SynchronizedViewFilter<T, TView> for quick filter creation

    master

    Instead of implementing the ISynchronizedViewFilter<T, TView> interface manually, you can use the SynchronizedViewFilter<T, TView> class to create a filter using a lambda expression or delegate. This class accepts a Func<T, TView, bool> in its constructor.

    It also provides a Null static property which acts as a filter that always returns true (effectively disabling filtering).

    // Create a filter using a lambda
    var filter = new SynchronizedViewFilter<MyItem, MyView>((item, view) => item.Id > 10);
    
    // Use the Null filter to always match everything
    var noFilter = SynchronizedViewFilter<MyItem, MyView>.Null;
  11. Convert an ObservableStack view to a list

    master

    An ISynchronizedView<T, TView> can be converted into specialized list types for different consumption patterns:

    • ToViewList(): Returns an ISynchronizedViewList<TView>. This is useful when you need list-like access to the projected view items.
    • ToNotifyCollectionChanged(): Returns a NotifyCollectionChangedSynchronizedViewList<TView>. This is specifically designed for UI frameworks that require INotifyCollectionChanged support (like WPF or Avalonia) to react to changes in the view.
    // Get a list that supports UI notifications
    var notifyList = view.ToNotifyCollectionChanged();
  12. Create a synchronized view of an ObservableQueue

    master

    You can create a synchronized view of an ObservableQueue<T> using the CreateView method. This allows you to project the underlying data into a different type (TView) and apply filtering while staying synchronized with the source queue.

    To use it:

    1. Call CreateView on your ObservableQueue<T> instance, providing a transformation function Func<T, TView>.
    2. Use the resulting ISynchronizedView<T, TView> to access filtered or unfiltered data.
    3. Apply filters using AttachFilter or clear them with ResetFilter.
    4. Convert the view into specialized list types like ISynchronizedViewList<TView> or NotifyCollectionChangedSynchronizedViewList<TView> for UI binding or iteration.
    // Assuming queue is an ObservableQueue<MyModel>
    var view = queue.CreateView(model => model.ToString());
    
    // Apply a filter
    view.AttachFilter(new MyCustomFilter());
    
    // Get the count of items matching the filter
    int count = view.Count;
    
    // Convert to a list for easier consumption
    var viewList = view.ToViewList();