Introduction
The ObservableCollection<T> class is frequently used in WPF applications to bind a set of data to a control and have item updates (adds/moves/removes) automatically represented in the UI. This is handled by the implementation of the INotifyCollectionChanged interface.
The default implementation of the ObservableCollection<T> has three main limitations with regards to thread safety that I have attempted to overcome:
- The CollectionChanged event is often bound to a UI element which can only be updated from the UI thread.
- Internally, items are stored in a List<T> which is not thread-safe. Writes during reads or multiple parallel writes can cause the list to become corrupt.
- The
GetEnumerator()
methods return an enumerator from the working list. This is desired but will cause problems if another thread modifies the list while it is being enumerated.
Background
I spent some time searching the net to see if other people had already solved these issues. Unfortunately, most solutions only attempted to solve issue #1 by storing the Dispatcher.Current value at construction and then using it to Invoke the CollectionChanged
event handler on the UI thread.
This is likely an acceptable solution for naive use cases where the work is light but it wouldn't work for me because it:
- didn't solve issues #2 and #3, and
- wasn't portable [doesn't solve the Windows Forms usage]
Overview of the Code
I started by looking at the source code for Collection<T> and ObservableCollection<T> as I wasn't looking to reinvent the wheel, but rather just polish it up a bit.
Issue #1 - Invoke Event Handlers on the UI Thread
To solve the issue of calling the CollectionChanged
event on the UI thread with a portable solution, I used the SyncronizationContext class. Here's an excellent article that goes into great detail on the SyncronizationContext class.
Long story short, the SyncronizationContext class allows us to queue a delegate on a given context (in this case, the UI thread) without regards for the underlying architecture (Windows Forms / Windows Presentation Foundation). The usage is fairly straightforward:
public SynchronizedObservableCollection()
{
_context = SynchronizationContext.Current;
_items = new List<T>();
}
private void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var collectionChanged = CollectionChanged;
if (collectionChanged == null)
{
return;
}
using (BlockReentrancy())
{
_context.Send(state => collectionChanged(this, e), null);
}
}
Issue #2 - Add Thread Safety Around the Underlying List<T> that Contains the Items
To make the underlying List<T> thread-safe, I needed to ensure that only one thread was writing at a time and that no thread was in the process of a read while a write occurred.
To accomplish this, I used a ReaderWriterLockSlim which manages this for us if implemented correctly.
private readonly ReaderWriterLockSlim _itemsLock = new ReaderWriterLockSlim();
I needed to ensure that all reads from the List<T> were encapsulated in a read lock as so:
public bool Contains(T item)
{
_itemsLock.EnterReadLock();
try
{
return _items.Contains(item);
}
finally
{
_itemsLock.ExitReadLock();
}
}
And that all writes were encapsulated in a write lock:
public void Add(T item)
{
_itemsLock.EnterWriteLock();
var index = _items.Count;
try
{
CheckIsReadOnly();
CheckReentrancy();
_items.Insert(index, item);
}
finally
{
_itemsLock.ExitWriteLock();
}
OnPropertyChanged("Count");
OnPropertyChanged("Item[]");
OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
}
It's very important that we always exit our locks so that we do not end up in a deadlock situation.
Issue #3 - Protect the Enumerator from Being Changed by Another Thread
This was a trivial change but I had to make a trade off here. In order to prevent another thread from breaking the enumerator, I need to work off a copy of the list. Due to this, the enumerator will not always represent the current state, but in most cases this will be ok.
public IEnumerator<T> GetEnumerator()
{
_itemsLock.EnterReadLock();
try
{
return _items.ToList().GetEnumerator();
}
finally
{
_itemsLock.ExitReadLock();
}
}
Points of Interest
This is the first time I've worked with both the SyncronizationContext and ReaderWriterLockSlim, both of which will come in very handy.
In the past, I would have used a concrete implementation (i.e., Dispatcher
) to invoke a delegate on the UI thread but the SyncronizationContext makes much more sense in a situation like this where the implementation may be used across different technologies.
As far as the ReaderWriterLockSlim is concerned, it made more sense in this situation than a Monitor.Enter() / Monitor.Exit() pattern as it should give me better read performance while still guaranteeing thread-safety.
History
- 2016-12-10 - Updated link to renamed repository
- 2015-06-14 - Added link to repository on GitHub
- 2015-06-07 - Initial version