Click here to Skip to main content
15,887,135 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 52K   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 
I've been experimenting with your latest version on GitHub. I'm using this to bind to a Data Grid control in WPF and pull entries from a database in a background thread. The goal being, I can have the UI remain responsive and displaying new rows as they are added while the background thread chugs away at getting results from the DB.

To simulate all this, I'm generating 100,000 records in a background thread. So it's not actually getting the records from the DB (yet). Maybe that's my problem - maybe the fact that I'm doing this in a simple background thread in a for loop, that it is adding records too quickly to the collection from the background thread. I'm not sure.

I do know that it seems like the UI freezes because it's probably dealing with too many Collection Changed events on the stack.

I semi-solved it by adding an AddRange method (see below). I basically create a temporary list in the background thread, load that with X number of records, and then call AddRange and pass in the list, then I reset the variable to a new reference of a list and do the next chunk of records. This allows me to do maintain a responsive UI, but load a large number of records quickly.

I also tried to instead of the AddRange, call the Add method via the Dispatcher with a lower priority (Input priority seemed best - anything higher and the UI would freeze). The problem with that though was what used to take a couple of seconds to generate 100,000 records and add to the collection in batches, now barely clears about 5,000 records in the same amount of time.

Here's the AddRange code. Is there a better way of doing things than the AddRange, that still allows a responsive UI?

public virtual void AddRange(IEnumerable<T> collection)
{
    _itemsLocker.EnterWriteLock();

    int index;

    try
    {
        CheckReentrancy();

        index = _items.Count;

        foreach (T item in collection)
        {
            _items.Add(item);
        }
    }
    finally
    {
        _itemsLocker.ExitWriteLock();
    }

    OnNotifyItemAdded(_items[index], index);
}


There's a complex reason (that I won't get into here), where I would prefer the Add() to work without locking up the UI adding a bunch of back to back items (and not needing to go the AddRange batch route).
42! Because it is the answer to everything! Life, the universe, everything!

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.