Introduction
Yesterday I ran across a web-page on the internet where people were voting for enhancements to the next .NET Framework. On the page, I noticed there were a lot of people voting for a safe multi-threaded way of dispatching an INotifyCollectionChanged event, mostly regarding the ObservableCollection. That made me remember the initial pains I had with this issue and the many articles devoted to trying to remedy it, one of the earliest if not first solutions coming from Bea Stollnitz (love her blog). I have long since put this behind me since I solved it on my own a few years ago but completely forgot that it's still very much the pain in the neck it was back then, so I've decided to pull the relevant classes out of my library and share it with everyone.
The good thing about my solution is that it's simple, performant, and doesn't have the limitations of most of the other solutions I've seen online such as the inability to modify the collection, or the need to keep two collections internally and keep them in sync, or the necessity of passing in a Dispatcher.
I've included two classes for use: the NotifyCollectionChangedWrapper and MTObservableCollection classes. The NotifyCollectionChangedWrapper class should be the only class you use since it wraps any existing object that derives from INotifyCollectionChanged, including ObservableCollection. The MTObservableCollection class is a multi-threaded version of the ObservableCollection but there should really be no need to use it; I just included it in case anyone needed it for some ungodly reason.
Background
There are already numerous articles out there detailing the issue in depth, so I'll only give a brief synopsis here.
The problem, in a nutshell, is that the INotifyCollectionChanged event, when triggered from a thread other than the originating thread, will give you the infamous "This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread." You can test this yourself by creating an ObservableCollection and binding it to an ItemsControl/ListBox/ListView; note that when you do this, you are doing it on the UI thread. Then, add an item to it via a worker thread, such as through a Timer event or ThreadPool.QueueUserWorkItem and you'll get an exception. The problem that occurs is not that you can't add or remove items from different threads; it's that the NotifyCollectionChanged event (the one your ItemsControl/ListBox/ListView automagically subscribes to in order to determine if something has been added or removed from the collection) that gets triggered is running from the worker thread, *not* from the UI thread, and since the worker thread doesn't have ANY mechanism for updating the UI, it throws an exception. This explanation is a tad-bit simplified, but you get the idea.
Using the Code
The magic to solving this problem in a simple manner happens when you realize that the problem only occurs at the notification side. If I want to be notified of an event, what would I have to do? I would have to subscribe to it, of course. This is where the magic happens; it happens during the subscription process.
What I've done is override the CollectionChanged subscription mechanism so that when (given our scenario above) the UI control subscribes to the underlying collection's CollectionChanged event, I save the Dispatcher associated with the UI control.
#region INotifyCollectionChanged Members
private Dictionary<NotifyCollectionChangedEventHandler, Dispatcher>
collectionChangedHandlers;
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add
{
Dispatcher dispatcher =
Dispatcher.FromThread(Thread.CurrentThread); collectionChangedHandlers.Add(value, dispatcher);
}
remove
{
collectionChangedHandlers.Remove(value);
}
}
#endregion
Now, whenever any changes occur to the collection, it will trigger it from the relevant thread (the UI thread in our scenario).
private void internalOC_CollectionChanged
(object sender, NotifyCollectionChangedEventArgs e)
{
using (BlockReentrancy())
{
KeyValuePair<NotifyCollectionChangedEventHandler, Dispatcher>[] handlers =
collectionChangedHandlers.ToArray();
if (handlers.Length > 0)
{
foreach (KeyValuePair<NotifyCollectionChangedEventHandler, Dispatcher>
kvp in handlers)
{
if (kvp.Value == null)
{
kvp.Key(this, e);
}
else
{
kvp.Value.Invoke(kvp.Key, DispatcherPriority.DataBind, this, e);
}
}
}
}
}
Here's how you would use the NotifyCollectionChangedWrapper class:
private ObservableCollection<string> items;
private NotifyCollectionChangedWrapper<string> ItemsWrapper;
public void Test()
{
items = new ObservableCollection<string>();
ItemsWrapper = new NotifyCollectionChangedWrapper<string>(items);
MyListView.ItemsSource = ItemsWrapper;
}
That's all you have to do to make your existing ObservableCollection or INotifyCollectionChanged classes work problem free! If for some reason, you don't want to use the wrapper and absolutely *must* replace your existing ObservableCollection, however, then you can use the MTObservableCollection class I created for this explicit purpose.
Attached you will find a Visual Studio 2008 solution that contains a test project so that you can see the class at work. When you run the test solution, the main window will appear.
The main window consists of an ObservableCollection that can hold strings wrapped by my NotifyCollectionChangedWrapper, an ItemsControl bound to the wrapper, and a button for spawning separate views of the same data. If you look at the window title, you will see the thread ID associated with the window (i.e., the UI Thread ID). The main window has an internal timer that triggers every two seconds; every time it gets triggered, a worker thread adds a string to the ObservableCollection containing the worker's thread ID and the time. In the above snapshot, you can see that even though the ItemsControl is bound to the wrapper on Thread1, worker threads 5 and 6 are both able to add items to ObservableCollection without a problem.
Another benefit of using the NotifyCollectionChangedWrapper is that you're not limited to just one UI control; you can bind *multiple* UI controls to it. You can even get your own custom ICollectionView instance from it using the CollectionView class! For those of you that don't know about views, the concept is simple: each view is simply another way of looking at the exact same data. For example, imagine that you have an address book containing the names, phone numbers, and addresses of all your friends. Now imagine that you want to have three windows showing your address book: one window to display them sorted by last name, another window to group them by state, and another window to edit the address book. Wouldn't it be a pain to pass the first window a copy of your address book sorted by last name, the second window a copy grouped by state, and the third window an editable instance of your address book? There's really no need for that with views since each view abstracts the presentation from the source. However, experimenting with all the great things you can accomplish with views is a topic for another article and not the point of my demo; I simply want to let you know that my NotifyCollectionChangedWrapper supports it and affords it the same multi-threaded protection.
If you click on the "Open View On Separate Thread" button, you will open another window that is on a completely different thread. Click on this button twice and you will get the following:
What you're seeing are three windows, each on completely different threads, each displaying a different view of the same data. If you click on both "Add Item To List" buttons, you will get the following:
As you can see, not only is the timer able to add items to the ObservableCollection from its own worker threads, but you can also add them manually on completely separate UI threads as well, with no exceptions whatsoever.
Please feel free to dissect the code any which way you like; I hope this has saved you many months of nail-biting agony. If you have any questions, please feel free to ask, and don't forget to rate this article below!
Points of Interest
As I wrote this article, I came across another solution very similar to mine by Muljadi Budiman at geekswithblogs; essentially the same approach, except that he gets the dispatcher from the invocation list while I save mine manually. I have to give it to him, the thought of getting the dispatcher from the invocation list never occurred to me. You can find his article here.
History
- March 11, 2010: Initial post
- March 12, 2010: Added snapshots and information related to the demo application