Click here to Skip to main content
14,106,748 members
Click here to Skip to main content
Add your own
alternative version

Stats

32K views
1.2K downloads
42 bookmarked
Posted 8 Jun 2015
Licenced CPOL

Thread-Safe ObservableCollection<T>

, 10 Dec 2016
Rate this:
Please Sign up or sign in to vote.
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:

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

License

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

Share

About the Author

Cory Charlton
Software Developer (Senior)
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

 
QuestionLarge number of Adds in succession and UI thread responsiveness Pin
B.O.B.14-Jan-18 12:37
memberB.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
memberB.O.B.14-Jan-18 16:53 
QuestionSynchronizationContext.Current / ItemsControl is not consistent with its source element. Pin
Daniel_866-Apr-17 21:14
memberDaniel_866-Apr-17 21:14 
QuestionJust a question Pin
Member 1081178519-Dec-16 1:41
memberMember 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
memberChristoph197225-Jan-16 10:33 
AnswerRe: System.Invalid.OperationException Pin
Christoph197226-Jan-16 4:57
memberChristoph197226-Jan-16 4:57 
GeneralVery good! Pin
Mike Barthold8-Jun-15 21:53
professionalMike Barthold8-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.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web06 | 2.8.190518.1 | Last Updated 10 Dec 2016
Article Copyright 2015 by Cory Charlton
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid