The ability to register for changes to a collection isn't something that all languages allow. Indeed, it's not something we might require for every project but it's arguably a very useful pattern. Often collections are used as data sources, backing a user interface such that the UI must refresh when the data source changes. When collections are not observable, we can always have callers manually update the UI but a degree of automation is lost and when we litter the responsibility for UI updates around, it becomes error-prone. With observable collections, the underlying data source can be 'bound' to specific UI elements. It can be altered from anywhere, allowing the owner of the data source to automatically update any associated UI with no additional refresh calls – a core structural component of the increasingly popular MVVM software design pattern.
Introducing ObservableCollection
ObservableCollection is a C# collection that offers this kind of functionality.
For those readers who may be unfamiliar with this versatile collection class, you can think of it much like a mutable array or list. Usually written as ObservableCollection<T>
, it conforms to the standard protocols expected of a collection, such as IEnumerable<T>
. So common iterative and reductive operations can be performed upon it as usual. However, it has an additional, somewhat magical property: interested observers can be notified of changes to the items stored within, through the Event Handler pattern.
Observing Change - The Event Handler Pattern
The Event Handler pattern in C#, with its powerful and familiar +=
and -=
syntax for adding and removing subscribers, is strict and safe way to manage a list of subscribers. Those interested in receiving events can 'add' a handler and later 'remove' it when they no longer require updates. Below is a basic example of delegate subscription with an ObservableCollection for integers.
using System.Collections.ObjectModel
using System.Collections.Specialized;
class MyDataSource {
public ObservableCollection<int> MyData { get; set; }
public MyDataSource() {
MyData = new ObservableCollection<int>();
MyData.CollectionChanged += OnCollectionChanged;
}
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) {
Console.WriteLine("Collection Changed");
}
void TearDown() {
MyData.CollectionChanged -= OnCollectionChanged;
}
}
Here, we create a new ObservableCollection and immediately delegate the local method OnCollectionChanged
to receive updates. As a delegate it's signature must match the expected signature for this event, which typically includes a sender and a set of arguments. When we consider that, for each operation on the collection, NotifyCollectionChangedEventArgs
provides not only an action of type NotifyCollectionChangedAction
from which we can determine what kind of operation occured, but also all the objects affected, we begin to see the power and flexibility at our disposal here.
Event Handling In Practice
Putting aside the ability to query what changed in the array for a moment, consider a very basic case of a view that should change colour when a user has selected some items from a list, but less than ten items in total. Perhaps this could be used to provide some visual feedback to the user about the validity of the data source contents. Of course we can use the event handler and do some checks in our callback so let's first examine what that might look like by extending our earlier example.
N.B. With C# now being used across many platforms, each employing their own unique UI frameworks, all user interface components here are intentionally purely notational.
public View SuccessIndicatorView { get; }
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) => {
SuccessIndicatorView.Color = MyData.Count > 0 && MyData.Count < 10 ? Color.Green : Color.Red;
}
Separating Concerns using Rx
That's quite neat but of course we're omitting all the boilerplate here. We're also ignoring the fact that typically in many software design patterns, like MVVM, the button will exist on a view, distinctly separated from the data source. So, how do we get the information out without exposing the data source's collection or requiring that the data source take a reference to the button? One simple way is to expose the event as an observable sequence on the data source as shown below.
using System.Reactive.Linq;
class MyDataSource {
private ObservableCollection<int> myData;
public IObservable<int[]> DataChanged { get; private set; }
public IObservable<Color> SuccessColor { get; private set; }
public MyDataSource() {
DataChanged = Observable
.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs> (
handler => (sender, args) => handler(args),
handler => myData.CollectionChanged += handler,
handler => myData.CollectionChanged -= handler)
.Select(args => myData.ToArray());
SuccessColor = DataChanged
.Select(args => myData.Count > 0 && myData.Count < 10 ? Color.Green : Color.Red)
.StartWith(Color.Red);
}
}
We're not in Kansas Any More
This may seem a somewhat disarming – the familiar simplicity of the event handler pattern is seemingly cast aside for this more complex, voluminous alternative. So what gives? As we will see, there are some significant benefits for a small increase in localised complexity.
Let's start by dissecting it a little.
Firstly, notice that we've crossed over to the world of Reactive Extensions (Rx), which deals with asynchronous observable sequences. The Observable.FromEvent<TDelegate, TEventArgs>
operator found in System.Reactive.Linq
can transform the event into an observable sequence using some simple rules:
handler => (sender, e) => handler(e)
converts given theNotifyCollectionChangedEventHandler
to a delegate, which is used in the following two steps.handler => myData.CollectionChanged += handler
is responsible for attaching the given event handler to the underlying delegate.handler => myData.CollectionChanged -= handler
removes it as subscribers are disposed.
Once the transformation is complete, we then further transform the result into an immutable array that can be publicly observed using Rx's LINQ-style operator Select
. Notice that the ObservableCollection is now private and so observers to DataChanged
get a completely immutable version of the data, allowing the data source complete internal control over mutations. Other than that, the output hasn't really changed significantly: the subscription model has changed from an event to an observable but that's about it. However, now that we have an IObservable instance, we can easily operate on it internally and expose convenient and accurate observable streams about expected, internally-defined conditions.
On a basic level, we can now transform the observable stream of data changes to a stream of Color
values for our 'sucess/fail' conditions again using the Select
operator. It should be clear to see then how a view model subscribing to this property, could automatically toggle our externally managed view's colour as the collection changes and the conditions for the user to continue are met or violated. We can even specify the initial conditions using the StartWith
operator – a state which may otherwise be left to pure assumption for subscribers.
Furthermore, we can easily define new conditions, change existing conditions or even allow external sources to define their own conditions in-situ if they so desire. Of course this is all still possible with the event pattern, but part of the beauty of this approach is how it simplifies alterations to conditions. It provides quick, easy access to brand new publicly observable sequences on the data source that meet specific criteria. For example, a stream that emits true
when items are added and false
when items are removed. It's not immediately clear why you might need such a sequence, but the fact remains that it's trivial once the basic stream is established.
Conclusion
Savvy C# developers might notice of course that this is achievable through the use of INotifyPropertyChanged
or possibly with popular MVVM frameworks such as ReactiveUI or WPF. However, we may not always be in a position to include our favourite frameworks and it could be argued that INotifyPropertyChanged
can introduce wieldy boilerplate, so it is prudent to examine this approach also. This is where the event handler pattern meets Rx in way that can tangibly improve the consistency, repeatability and stability of observable data sources, however we choose to use them.