While playing around with WPF, I tried to do some multithreading where I have a worker thread updating my ObservableCollection, while having a ListCollectionView of that ObservableCollection being shown on a ListBox.

It was surprising to see that I get a NotSupportedException thrown, with the message saying 'This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.'.  That doesn't seem to make sense - In my mind, I understand how the thread that created the UI should be the one that handles all UI updates.  However, the data itself should be able to reside anywhere, and I should be able to update it however and whenever I want.

Looking for a solution, I created a class deriving from ObservableCollection (the class name I chose is ObservableCollectionEx) thinking that I would just manually walk through the event's invocation list.  Well, that didn't quite work, since events are not accessible (other than for adding/removing delegates) to child classes.  Looking at the documentation, the CollectionChanged event in ObservableCollection is virtual - that means I can override it! Yeah!  I am glad someone at Microsoft decided to make that virtual.

So here's the code that I created - the pain is I now have to rename all occurrences of ObservableCollection to this new class.  Oh well, at least making it work with threads isn't too painful.

public class ObservableCollectionEx<T> : ObservableCollection<T>
{
   // Override the event so this class can access it
   public override event System.Collections.Specialized.NotifyCollectionChangedEventHandler CollectionChanged;

   protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
   {
      // Be nice - use BlockReentrancy like MSDN said
      using (BlockReentrancy())
      {
         System.Collections.Specialized.NotifyCollectionChangedEventHandler eventHandler = CollectionChanged;
         if (eventHandler == null)
            return;
    
         Delegate[] delegates = eventHandler.GetInvocationList();
         // Walk thru invocation list
         foreach (System.Collections.Specialized.NotifyCollectionChangedEventHandler handler in delegates)
         {
            DispatcherObject dispatcherObject = handler.Target as DispatcherObject;
            // If the subscriber is a DispatcherObject and different thread
            if (dispatcherObject != null && dispatcherObject.CheckAccess() == false)
            {
               // Invoke handler in the target dispatcher's thread
               dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e);
            }
            else // Execute handler as is
               handler(this, e);
         }
      }
   }
}

Update:

There is a bug with the code where subscribers in the same thread won't get called - the code above has been updated with the fix.

It also uses CheckAccess (available, but not shown via Intellisense as mentioned here).

posted on 2011-12-19 11:25  higirle  阅读(609)  评论(0编辑  收藏  举报