Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / WPF
Tip/Trick

Thread-Safe ObservableCollection<T>

Rate me:
Please Sign up or sign in to vote.
4.97/5 (23 votes)
10 Dec 2016CPOL3 min read 51.7K   1.5K   42   10
A thread-safe implementation of the ObservableCollection<T> class

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:

  1. The CollectionChanged event is often bound to a UI element which can only be updated from the UI thread.
  2. 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.
  3. 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:

  1. didn't solve issues #2 and #3, and
  2. 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:

C#
public SynchronizedObservableCollection()
{
    _context = SynchronizationContext.Current;
    _items = new List<T>();
}
C#
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.

C#
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:

C#
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:

C#
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.

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionThanks for sharing but the code is full of bugs Pin
sparrow5820-Apr-20 11:21
sparrow5820-Apr-20 11:21 
AnswerRe: Thanks for sharing but the code is full of bugs - reports would help, I'm sure. Pin
OriginalGriff20-Apr-20 11:26
mveOriginalGriff20-Apr-20 11:26 
QuestionLarge number of Adds in succession and UI thread responsiveness Pin
B.O.B.14-Jan-18 12:37
B.O.B.14-Jan-18 12:37 
AnswerRe: Large number of Adds in succession and UI thread responsiveness Pin
B.O.B.14-Jan-18 16:53
B.O.B.14-Jan-18 16:53 
QuestionSynchronizationContext.Current / ItemsControl is not consistent with its source element. Pin
Daniel_866-Apr-17 21:14
Daniel_866-Apr-17 21:14 
QuestionJust a question Pin
Member 1081178519-Dec-16 1:41
Member 1081178519-Dec-16 1:41 
AnswerRe: Just a question Pin
Cory Charlton30-Jan-17 9:24
professionalCory Charlton30-Jan-17 9:24 
QuestionSystem.Invalid.OperationException Pin
Christoph197225-Jan-16 10:33
Christoph197225-Jan-16 10:33 
AnswerRe: System.Invalid.OperationException Pin
Christoph197226-Jan-16 4:57
Christoph197226-Jan-16 4:57 
GeneralVery good! Pin
Mike (Prof. Chuck)8-Jun-15 21:53
professionalMike (Prof. Chuck)8-Jun-15 21:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.