Click here to Skip to main content
Click here to Skip to main content

Multi-Threaded ObservableCollection and NotifyCollectionChanged Wrapper

By , 12 Mar 2010
 

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); // experimental (can return null)...
      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()
   {
   // create your ObservableCollection as you normally would
   items = new ObservableCollection<string>();
   // Now wrap it up to make it thread-safe (I'm using that term loosely here)
   ItemsWrapper = new NotifyCollectionChangedWrapper<string>(items);
   // Bind it to your UI control and you're done!
   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.

pic1.JPG

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:

pic2.JPG - Click to enlarge image

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:

pic3.JPG - Click to enlarge image

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

License

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

About the Author

AnthonyPaulO
Software Developer (Senior) Wells Fargo
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberhari111r15 Nov '12 - 19:59 
Questionvery nicememberCIDev22 Jun '12 - 8:02 
GeneralMy vote of 5membermarcel heeremans11 Dec '11 - 20:40 
GeneralMy vote of 5memberKanasz Robert18 Nov '11 - 5:38 
GeneralNice ArticlememberWonde Tadesse10 Nov '11 - 15:28 
SuggestionMy vote of 5memberEugene Sadovoi26 Oct '11 - 4:52 
QuestionDo you have a solution for the InvalidOperationException that sometimes occurs with this approach?memberRobert Ranck13 Sep '11 - 9:57 
AnswerRe: Do you have a solution for the InvalidOperationException that sometimes occurs with this approach?memberMember 867064622 Feb '12 - 22:30 
GeneralRe: Do you have a solution for the InvalidOperationException that sometimes occurs with this approach?memberksebas15 Jun '12 - 2:12 
GeneralRe: Do you have a solution for the InvalidOperationException that sometimes occurs with this approach?memberKawao7 Dec '12 - 4:37 
GeneralRe: Do you have a solution for the InvalidOperationException that sometimes occurs with this approach?memberteisnet16 Dec '12 - 9:10 
QuestionMy vote of 5memberFilip D'haene9 Sep '11 - 6:17 
QuestionHigh five from me!!!memberTom the Coder27 Jun '11 - 22:59 
GeneralPoritng to VBmemberausadmin7 Nov '10 - 11:19 
GeneralMy vote of 5memberEric Ouellet29 Oct '10 - 10:13 
GeneralMy vote of 5memberOliver Zürcher6 Sep '10 - 22:47 
GeneralMy vote of 5memberbillburns10 Aug '10 - 15:06 
GeneralNice!memberNathan Allen-Wagner14 Jul '10 - 7:00 
GeneralMy vote of 5memberMember 285884212 Jul '10 - 22:59 
Generalissue compiling MTObservableCollectionmemberJobLot_124 Mar '10 - 16:52 
GeneralIndex Error After Removing First Item, and Adding New Item [modified]membertmh11422 Mar '10 - 6:49 
GeneralExcellent ArticlememberRelativityE=mc212 Mar '10 - 9:20 

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 12 Mar 2010
Article Copyright 2010 by AnthonyPaulO
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid