Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WPF
Tip/Trick

Thread Safe Improvement for ObservableCollection

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
2 Jul 2012CPOL2 min read 61.6K   472   11   13
Why didn’t Microsoft provide this in the ObservableCollection?

Introduction

I was working on a WPF project where I had the ViewModel mirroring the Model displaying a hierarchical view of the data (like a tree view, but using XamDataGrid). I wanted to have an event in the Model that triggered when a record was added or removed to reduce coupling (probably when changes are made in the Model in the future). This was because I expected to be using the Model side with many different ViewModels, although at this point not sure since I am being given the requirements piece-meal. When I did this in a case where I had a BackgroundWorker, I started having problems with being on the wrong thread. Of course I expected that, and I immediately implemented a Dispatcher on the receiving class using code like the following:

C#
Dispatcher.CurrentDispatcher.Invoke(new Action<StagedBlotterOrderAggViewModel>(Add), adding);

This did not work so I attempted to use DispatcherPriority. Nothing looked right to me, but I tried several options and none worked.

I could go back to a design of trying to ensure that the record updates were done in the BackgroundWorkerCompleted handler, but that would complicate the code.

Of course I know that you can get the Dispatcher from the control, but that was not very elegant. Therefore I started to search the web, and found something interesting at “Have worker thread update ObservableCollection that is bound to a ListCollectionView” (http://geekswithblogs.net/NewThingsILearned/archive/2008/01/16/have-worker-thread-update-observablecollection-that-is-bound-to-a.aspx). I was not sure it would work, but I tried it, and it did. Of course I guess eventually I would have come up with inheriting from ObservableCollection<T> since I knew that I could get the Dispatcher from the control and then would not have had to do anything special in the ViewModel, but I am sure this is better than what I would have come up with. I did make a small number of improvements of adding the other constructors.

Here is my slightly improved version of this code that worked so well for me:

C#
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Threading;
 
namespace Custom.Collections
{
  public class ObservableCollectionEx<t> : ObservableCollection<t>
  {
    // Override the event so this class can access it
    public override event NotifyCollectionChangedEventHandler CollectionChanged;
 
    public ObservableCollectionEx(IEnumerable<t> collection) : base(collection) { }
    public ObservableCollectionEx(List<t> collection) : base(collection) { }
 
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
      // Be nice - use BlockReentrancy like MSDN said
      using (BlockReentrancy())
      {
        var eventHandler = CollectionChanged;
        if (eventHandler != null)
        {
          Delegate[] delegates = eventHandler.GetInvocationList();
          // Walk thru invocation list
          foreach (NotifyCollectionChangedEventHandler handler in delegates)
          {
            var 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);
          }
        }
      }
    }
  }
}</t></t></t></t>

Now my big question is: Why didn’t Microsoft provide this in the original ObservableCollection?

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) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
BugA race condition can result in the UI displaying each item twice Pin
Dave Mackersie4-Nov-15 6:16
Dave Mackersie4-Nov-15 6:16 
While using this approach we discovered that, on rare occasions, each item would be displayed twice in the databound UI. Because the problem happened so infrequently it was difficult to debug and track down, but we believe what was happening was as follows:

1. The ObservableCollectionEx was populated on a worker thread.
2. A series of CollectionChanged events got dispatched to the UI thread, one for each new item added to the collection.
3. While the CollectionChanged events were still in the Dispatcher queue, the UI databinding to the collection occurred, adding all of the items to the display.
4. After databinding was complete, the UI thread handled the CollectionChanged events, adding each of the items to the UI a second time.

The big breakthrough came when we observed the following exception while running under the debugger:

Information for developers (use Text Visualizer to read this):
This exception was thrown because the generator for control 'System.Windows.Controls.ListView Items.Count:5' with name '(unnamed)' has received sequence of CollectionChanged events that do not agree with the current state of the Items collection.  The following differences were detected:
  Accumulated count 1 is different from actual count 5.  [Accumulated count is (Count at last Reset + #Adds - #Removes since last Reset).]

One or more of the following sources may have raised the wrong events:
     System.Windows.Controls.ItemContainerGenerator
      System.Windows.Controls.ItemCollection
       System.Windows.Data.ListCollectionView
  *     Custom.Collections.ObservableCollectionEx`1[[MyApp.ViewModel, MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
(The starred sources are considered more likely to be the cause of the problem.)

The most common causes are (a) changing the collection or its Count without raising a corresponding event, and (b) raising an event with an incorrect index or item parameter.

The exception's stack trace describes how the inconsistencies were detected, not how they occurred.  To get a more timely exception, set the attached property 'PresentationTraceSources.TraceLevel' on the generator to value 'High' and rerun the scenario.  One way to do this is to run a command similar to the following:
   System.Diagnostics.PresentationTraceSources.SetTraceLevel(myItemsControl.ItemContainerGenerator, System.Diagnostics.PresentationTraceLevel.High)
from the Immediate window.  This causes the detection logic to run after every CollectionChanged event, so it will slow down the application.


Our solution was to abandon the ObservableCollectionEx class, and instead use a SynchronizationContext to dispatch all work for ObservableCollection to the UI thread.
AnswerRe: A race condition can result in the UI displaying each item twice Pin
Clifford Nelson4-Nov-15 8:54
Clifford Nelson4-Nov-15 8:54 
Questionperformance Pin
kadzbi25-May-14 23:24
kadzbi25-May-14 23:24 
QuestionChange made in .Net 4.5.1 (at least) Pin
BC3Tech16-May-14 7:32
BC3Tech16-May-14 7:32 
AnswerRe: Change made in .Net 4.5.1 (at least) Pin
Clifford Nelson4-Nov-15 8:54
Clifford Nelson4-Nov-15 8:54 
QuestionNot thread-safe Pin
Michael Bendtsen4-Dec-12 2:25
Michael Bendtsen4-Dec-12 2:25 
AnswerRe: Not thread-safe Pin
Clifford Nelson4-Dec-12 5:20
Clifford Nelson4-Dec-12 5:20 
QuestionNice try Pin
Jasper4C#3-Jul-12 2:54
Jasper4C#3-Jul-12 2:54 
AnswerRe: Nice try Pin
FatCatProgrammer3-Jul-12 4:11
FatCatProgrammer3-Jul-12 4:11 
AnswerRe: Nice try Pin
Clifford Nelson3-Jul-12 6:54
Clifford Nelson3-Jul-12 6:54 
AnswerRe: Nice try Pin
Clifford Nelson3-Jul-12 6:50
Clifford Nelson3-Jul-12 6:50 
QuestionReponse to your question Pin
Nicolas Dorier3-Jul-12 1:49
professionalNicolas Dorier3-Jul-12 1:49 
GeneralRe: Reponse to your question Pin
Clifford Nelson3-Jul-12 6:59
Clifford Nelson3-Jul-12 6:59 

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.