This repository contains a modified version of Cysharp/ObservableCollections, adapted for compatibility with CsWinRT.
To support CsWinRT projection, several changes have been made to the original implementation. These include the use of a Source Generator-like tool to automatically generate the required projection code, as well as making certain types partial where necessary. When using this code, please ensure that the accompanying Source Generator is included in your project.
We have also incorporated some modifications from CommunityToolkit to add support for incremental loading.
The original ObservableCollections project and the modifications in this repository are both licensed under the MIT License. We gratefully acknowledge and appreciate the Cysharp team and CommunityToolkit contributors for their excellent work and contributions.
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.
publicdelegatevoidNotifyCollectionChangedEventHandler<T>(inNotifyCollectionChangedEventArgs<T>e);publicreadonlyrefstructNotifyCollectionChangedEventArgs<T>{publicreadonlyNotifyCollectionChangedActionAction;publicreadonlyboolIsSingleItem;publicreadonlyTNewItem;publicreadonlyTOldItem;publicreadonlyReadOnlySpan<T>NewItems;publicreadonlyReadOnlySpan<T>OldItems;publicreadonlyintNewStartingIndex;publicreadonlyintOldStartingIndex;publicreadonlySortOperation<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.
publicinterfaceIObservableCollection<T>:IReadOnlyCollection<T>{eventNotifyCollectionChangedEventHandler<T>?CollectionChanged;objectSyncRoot{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.
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.
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<(intIndex,intCount)>IObservableCollection<T>.ObserveReverse<T>()
Observable<(intIndex,intCount,IComparer<T>?Comparer)>IObservableCollection<T>.ObserveSort<T>()Observable<int>IObservableCollection<T>.ObserveCountChanged<T>()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 modificationvarlist=newObservableList<int>();list.CollectionChanged+=List_CollectionChanged;list.Add(10);list.Add(20);list.AddRange(new[]{10,20,30});staticvoidList_CollectionChanged(inNotifyCollectionChangedEventArgs<int>e){switch(e.Action){caseNotifyCollectionChangedAction.Add:if(e.IsSingleItem){Console.WriteLine(e.NewItem);}else{foreach(varitemine.NewItems){Console.WriteLine(item);}}break;// Remove, Replace, Move, Resetdefault: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.
varlist=newObservableList<int>();varview=list.CreateView(x =>x.ToString()+"$");list.Add(10);list.Add(20);list.AddRange(new[]{30,40,50});list[1]=60;list.RemoveAt(3);foreach(varvinview){// 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.
varlist=newObservableList<int>();usingvarview=list.CreateView(x =>x.ToString()+"$");list.Add(1);list.Add(20);list.AddRange(new[]{30,31,32});// attach filterview.AttachFilter(x =>x%2==0);foreach(varvinview){// 20$, 30$, 32$Console.WriteLine(v);}// attach other filter(removed previous filter)view.AttachFilter(x =>x%2==1);foreach(varvinview){// 1$, 31$Console.WriteLine(v);}// Count shows filtered lengthConsole.WriteLine(view.Count);// 2The 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 Synchronizationvarqueue=newObservableQueue<int>();queue.Enqueue(1);queue.Enqueue(10);queue.Enqueue(100);queue.Enqueue(1000);queue.Enqueue(10000);usingvarview=queue.CreateView(x =>x.ToString()+"$");usingvarviewList=view.ToViewList();Console.WriteLine(viewList[2]);// 100$In the case of ObservableList, calls to Sort and Reverse can also be synchronized with the view.
varlist=newObservableList<int>{1,301,20,50001,4000};usingvarview=list.CreateView(x =>x.ToString()+"$");view.AttachFilter(x =>x%2==0);foreach(varvinview){// 20$, 4000$Console.WriteLine(v);}// Reverse operations on the list will affect the viewlist.Reverse();foreach(varvinview){// 4000$, 20$Console.WriteLine(v);}// remove filterview.ResetFilter();// The reverse operation is also reflected in the values hidden by the filterforeach(varvinview){// 4000$, 50001$, 20$, 301$, 1$Console.WriteLine(v);}// also affect Sort Operations list.Sort();foreach(varvinview){// 1$, 20$, 301$, 4000$, 50001$Console.WriteLine(v);}// you can use custom comparerlist.Sort(newDescendantComaprer());foreach(varvinview){// 50001$, 4000$, 301$, 20$, 1$Console.WriteLine(v);}classDescendantComaprer:IComparer<int>{publicintCompare(intx,inty){returny.CompareTo(x);}}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
usingR3;usingObservableCollections;varlist=newObservableList<int>();list.ObserveAdd().Subscribe(x =>{Console.WriteLine(x);});list.Add(10);list.Add(20);list.AddRange(new[]{10,20,30});Note that ObserveReset is used to subscribe to Clear, Reverse, and Sort operations in bulk.
In addition to IObservableCollection<T>, there is also a subscription event for ISynchronizedView<T, TView>. In the case of View, ObserveRejected is also added.
Since it is not supported by dotnet/reactive, please use the Rx library R3.
In the case of Blazor, StateHasChanged is called and re-enumeration occurs in response to changes in the collection. It's advisable to use the CollectionStateChanged event for this purpose.
publicpartialclassIndex:IDisposable{ObservableList<int>list;publicISynchronizedView<int,int>ItemsView{get;set;}intcount=0;protectedoverridevoidOnInitialized(){list=newObservableList<int>();ItemsView=list.CreateView(x =>x);ItemsView.CollectionStateChanged+= action =>{InvokeAsync(StateHasChanged);};}voidOnClick(){list.Add(count++);}publicvoidDispose(){ItemsView.Dispose();}}// .razor, iterate view
@page "/"<button @onclick=OnClick>button</button><table>@foreach(variteminItemsView){<tr><td>@item</td></tr>}</table>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. ToToNotifyCollectionChanged(IColllectionEventDispatcher) allows multi thread changed.
// WPF simple sample.ObservableList<int>list;publicNotifyCollectionChangedSynchronizedViewList<int>ItemsView{get;set;}publicMainWindow(){InitializeComponent();this.DataContext=this;list=newObservableList<int>();// for ui synchronization safety of viewmodelItemsView=list.ToNotifyCollectionChanged(SynchronizationContextCollectionEventDispatcher.Current);// if collection is changed only from ui-thread, can use this overload// ItemsView = list.ToNotifyCollectionChanged();}protectedoverridevoidOnClosed(EventArgse){ItemsView.Dispose();}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:
publicclassWpfDispatcherCollection(Dispatcherdispatcher):ICollectionEventDispatcher{publicvoidPost(CollectionEventDispatcherEventArgsev){dispatcher.InvokeAsync(()=>{// notify in dispatcherev.Invoke();});}}ToNotifyCollectionChanged() can also be called without going through a View. In this case, it's guaranteed that no filters will be applied, making it faster. If you want to apply filters, please generate a View before calling it. Additionally, ObservableList has a variation called ToNotifyCollectionChangedSlim(). This option doesn't generate a list for the View and shares the actual data, making it the fastest and most memory-efficient option. However, range operations such as AddRange, InsertRange and RemoveRange are not supported by WPF (or Avalonia), so they will throw runtime exceptions.
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.
publicdelegateTWritableViewChangedEventHandler<T,TView>(TViewnewView,ToriginalValue,refboolsetValue);publicinterfaceIWritableSynchronizedView<T,TView>:ISynchronizedView<T,TView>{NotifyCollectionChangedSynchronizedViewList<TView>ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T,TView>converter);NotifyCollectionChangedSynchronizedViewList<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.
varlist=newObservableList<Person>(){new(){Age=10,Name="John"},new(){Age=22,Name="Jeyne"},new(){Age=30,Name="Mike"},};varview=list.CreateWritableView(x =>x.Name);view.AttachFilter(x =>x.Age>=20);IList<string?>bindable=view.ToWritableNotifyCollectionChanged((string?newView,Personoriginal,refboolsetValue)=>{if(setValue){// default setValue == true is Set operationoriginal.Name=newView;// You can modify setValue to false, it does not set original collection to new value.// For mutable reference types, when there is only a single,// bound View and to avoid recreating the View, setting false is effective.// Otherwise, keeping it true will set the value in the original collection as well,// and change notifications will be sent to lower-level Views(the delegate for View generation will also be called anew).setValue=false;returnoriginal;}else{// default setValue == false is Add operationreturnnewPerson{Age=null,Name=newView};}});bindable[1]="Bob";// change Mike(filtered view's [1]) to Bob.bindable.Add("Ken");// Show Viewsforeach(variteminview){Console.WriteLine(item);}Console.WriteLine("---");// Show Originalsforeach(variteminlist){Console.WriteLine((item.Age,item.Name));}publicclassPerson{publicint?Age{get;set;}publicstring?Name{get;set;}}In Unity projects, you can installing ObservableCollections with NugetForUnity. If R3 integration is required, similarly install ObservableCollections.R3 via NuGetForUnity.
In Unity, ObservableCollections and Views are useful as CollectionManagers, since they need to convert T to Prefab for display. Since View objects are generated only once, it's possible to complement GameObjects tied to the collection.
publicclassSampleScript:MonoBehaviour{publicButtonprefab;publicGameObjectroot;ObservableRingBuffer<int>collection;ISynchronizedView<int,GameObject>view;voidStart(){collection=newObservableRingBuffer<int>();view=collection.CreateView(x =>{varitem=GameObject.Instantiate(prefab);item.GetComponentInChildren<Text>().text=x.ToString();// add to rootitem.transform.SetParent(root.transform);returnitem.gameObject;});view.ViewChanged+=View_ViewChanged;}voidView_ViewChanged(inSynchronizedViewChangedEventArgs<int,GameObject>eventArgs){// hook remove eventif(eventArgs.Action==NotifyCollectionChangedAction.Remove){GameObject.Destroy(eventArgs.OldItem.View);}// hook for Filter attached, clear, etc...// if (NotifyCollectionChangedAction.Reset) { }}voidOnDestroy(){view.Dispose();}}ObservableCollections provides these collections.
classObservableList<T>:IList<T>,IReadOnlyList<T>,IObservableCollection<T>,IReadOnlyObservableList<T>
class ObservableDictionary<TKey,TValue>:IDictionary<TKey,TValue>,IReadOnlyDictionary<TKey,TValue>,IObservableCollection<KeyValuePair<TKey,TValue>>,IReadOnlyObservableDictionary<TKey,TValue>whereTKey:notnull
class ObservableHashSet<T>:IReadOnlySet<T>,IReadOnlyCollection<T>,IObservableCollection<T>whereT:notnull
class ObservableQueue<T>:IReadOnlyCollection<T>,IObservableCollection<T>
class ObservableStack<T>:IReadOnlyCollection<T>,IObservableCollection<T>
class ObservableRingBuffer<T>:IList<T>,IReadOnlyList<T>,IObservableCollection<T>
class RingBuffer<T>:IList<T>,IReadOnlyList<T>
class ObservableFixedSizeRingBuffer<T>:IList<T>,IReadOnlyList<T>,IObservableCollection<T>
class AlternateIndexList<T>:IEnumerable<T>The IObservableCollection<T> is the base interface for all, containing the CollectionChanged event and the CreateView method.
publicdelegatevoidNotifyCollectionChangedEventHandler<T>(inNotifyCollectionChangedEventArgs<T>e);publicinterfaceIObservableCollection<T>:IReadOnlyCollection<T>{objectSyncRoot{get;}eventNotifyCollectionChangedEventHandler<T>?CollectionChanged;ISynchronizedView<T,TView>CreateView<TView>(Func<T,TView>transform);}The notification event NotifyCollectionChangedEventArgs<T> has the following definition:
/// <summary>/// Contract:/// IsSingleItem ? (NewItem, OldItem) : (NewItems, OldItems)/// Action.Add/// NewItem, NewItems, NewStartingIndex/// Action.Remove/// OldItem, OldItems, OldStartingIndex/// Action.Replace/// NewItem, NewItems, OldItem, OldItems, (NewStartingIndex, OldStartingIndex = samevalue)/// Action.Move/// NewStartingIndex, OldStartingIndex/// Action.Reset/// SortOperation(IsClear, IsReverse, IsSort)/// </summary>[StructLayout(LayoutKind.Auto)]publicreadonlyrefstructNotifyCollectionChangedEventArgs<T>{publicreadonlyNotifyCollectionChangedActionAction;publicreadonlyboolIsSingleItem;publicreadonlyTNewItem;publicreadonlyTOldItem;publicreadonlyReadOnlySpan<T>NewItems;publicreadonlyReadOnlySpan<T>OldItems;publicreadonlyintNewStartingIndex;publicreadonlyintOldStartingIndex;publicreadonlySortOperation<T>SortOperation;}This is the interface for View:
publicdelegatevoidNotifyViewChangedEventHandler<T,TView>(inSynchronizedViewChangedEventArgs<T,TView>e);publicenumRejectedViewChangedAction{Add,Remove,Move}publicinterfaceISynchronizedView<T,TView>:IReadOnlyCollection<TView>,IDisposable{objectSyncRoot{get;}ISynchronizedViewFilter<T,TView>Filter{get;}IEnumerable<(TValue,TViewView)>Filtered{get;}IEnumerable<(TValue,TViewView)>Unfiltered{get;}intUnfilteredCount{get;}eventNotifyViewChangedEventHandler<T,TView>?ViewChanged;eventAction<RejectedViewChangedAction,int,int>?RejectedViewChanged;// int index, int oldIndex(when RejectedViewChangedAction is Move)eventAction<NotifyCollectionChangedAction>?CollectionStateChanged;voidAttachFilter(ISynchronizedViewFilter<T,TView>filter);voidResetFilter();ISynchronizedViewList<TView>ToViewList();NotifyCollectionChangedSynchronizedViewList<TView>ToNotifyCollectionChanged();NotifyCollectionChangedSynchronizedViewList<TView>ToNotifyCollectionChanged(ICollectionEventDispatcher?collectionEventDispatcher);}The Count of the View returns the filtered value, but if you need the unfiltered value, use UnfilteredCount. Also, normal enumeration returns only TView, but if you need T or want to enumerate pre-filtered values, you can get them with Filtered and Unfiltered.
The View's notification event SynchronizedViewChangedEventArgs<T> has the following definition:
publicreadonlyrefstructSynchronizedViewChangedEventArgs<T,TView>{publicreadonlyNotifyCollectionChangedActionAction;publicreadonlyboolIsSingleItem;publicreadonly(TValue,TViewView)NewItem;publicreadonly(TValue,TViewView)OldItem;publicreadonlyReadOnlySpan<T>NewValues;publicreadonlyReadOnlySpan<TView>NewViews;publicreadonlyReadOnlySpan<T>OldValues;publicreadonlyReadOnlySpan<TView>OldViews;publicreadonlyintNewStartingIndex;publicreadonlyintOldStartingIndex;publicreadonlySortOperation<T>SortOperation;}When NotifyCollectionChangedAction is Reset, additional determination can be made with SortOperation<T>.
publicreadonlystructSortOperation<T>{publicreadonlyintIndex;publicreadonlyintCount;publicreadonlyIComparer<T>?Comparer;publicboolIsReverse{get;}publicboolIsClear{get;}publicboolIsSort{get;}}When IsReverse is true, you need to use Index and Count. When IsSort is true, you need to use Index, Count, and Comparer values.
For Filter, you can either create one that implements this interface or generate one from a lambda expression using extension methods.
publicinterfaceISynchronizedViewFilter<T,TView>{boolIsMatch(Tvalue,TViewview);}publicstaticclassSynchronizedViewExtensions{publicstaticvoidAttachFilter<T,TView>(thisISynchronizedView<T,TView>source,Func<T,bool>filter){}publicstaticvoidAttachFilter<T,TView>(thisISynchronizedView<T,TView>source,Func<T,TView,bool>filter){}}ObservableList<T> has writable view.
publicsealedpartialclassObservableList<T>{publicIWritableSynchronizedView<T,TView>CreateWritableView<TView>(Func<T,TView>transform);publicNotifyCollectionChangedSynchronizedViewList<T>ToWritableNotifyCollectionChanged();publicNotifyCollectionChangedSynchronizedViewList<T>ToWritableNotifyCollectionChanged(ICollectionEventDispatcher?collectionEventDispatcher);publicNotifyCollectionChangedSynchronizedViewList<TView>ToWritableNotifyCollectionChanged<TView>(Func<T,TView>transform,WritableViewChangedEventHandler<T,TView>?converter);publicNotifyCollectionChangedSynchronizedViewList<TView>ToWritableNotifyCollectionChanged<TView>(Func<T,TView>transform,ICollectionEventDispatcher?collectionEventDispatcher,WritableViewChangedEventHandler<T,TView>?converter);}publicdelegateTWritableViewChangedEventHandler<T,TView>(TViewnewView,ToriginalValue,refboolsetValue);publicinterfaceIWritableSynchronizedView<T,TView>:ISynchronizedView<T,TView>{(TValue,TViewView)GetAt(intindex);voidSetViewAt(intindex,TViewview);voidSetToSourceCollection(intindex,Tvalue);voidAddToSourceCollection(Tvalue);voidInsertIntoSourceCollection(intindex,Tvalue);boolRemoveFromSourceCollection(Tvalue);voidRemoveAtSourceCollection(intindex);voidClearSourceCollection();IWritableSynchronizedViewList<TView>ToWritableViewList(WritableViewChangedEventHandler<T,TView>converter);INotifyCollectionChangedSynchronizedViewList<TView>ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T,TView>converter);INotifyCollectionChangedSynchronizedViewList<TView>ToWritableNotifyCollectionChanged(WritableViewChangedEventHandler<T,TView>converter,ICollectionEventDispatcher?collectionEventDispatcher);}publicinterfaceIWritableSynchronizedViewList<TView>:ISynchronizedViewList<TView>{newTViewthis[intindex]{get;set;}}Here are definitions for other collections:
publicinterfaceIReadOnlyObservableList<T>:IReadOnlyList<T>,IObservableCollection<T>{}publicinterfaceIReadOnlyObservableDictionary<TKey,TValue>:IReadOnlyDictionary<TKey,TValue>,IObservableCollection<KeyValuePair<TKey,TValue>>{}publicinterfaceISynchronizedViewList<outTView>:IReadOnlyList<TView>,IDisposable{}// Obsolete for public usepublicinterfaceINotifyCollectionChangedSynchronizedViewList<TView>:IList<TView>,IList,ISynchronizedViewList<TView>,INotifyCollectionChanged,INotifyPropertyChanged{}publicabstractclassNotifyCollectionChangedSynchronizedViewList<TView>:INotifyCollectionChangedSynchronizedViewList<TView>,IWritableSynchronizedViewList<TView>,IList<TView>,IList{}This library is licensed under the MIT License.

