How do I update an ObservableCollection via a worker thread?

73,765

Solution 1

Technically the problem is not that you are updating the ObservableCollection from a background thread. The problem is that when you do so, the collection raises its CollectionChanged event on the same thread that caused the change - which means controls are being updated from a background thread.

In order to populate a collection from a background thread while controls are bound to it, you'd probably have to create your own collection type from scratch in order to address this. There is a simpler option that may work out for you though.

Post the Add calls onto the UI thread.

public static void AddOnUI<T>(this ICollection<T> collection, T item) {
    Action<T> addMethod = collection.Add;
    Application.Current.Dispatcher.BeginInvoke( addMethod, item );
}

...

b_subcollection.AddOnUI(new B());

This method will return immediately (before the item is actually added to the collection) then on the UI thread, the item will be added to the collection and everyone should be happy.

The reality, however, is that this solution will likely bog down under heavy load because of all the cross-thread activity. A more efficient solution would batch up a bunch of items and post them to the UI thread periodically so that you're not calling across threads for each item.

The BackgroundWorker class implements a pattern that allows you to report progress via its ReportProgress method during a background operation. The progress is reported on the UI thread via the ProgressChanged event. This may be another option for you.

Solution 2

New option for .NET 4.5

Starting from .NET 4.5 there is a built-in mechanism to automatically synchronize access to the collection and dispatch CollectionChanged events to the UI thread. To enable this feature you need to call BindingOperations.EnableCollectionSynchronization from within your UI thread.

EnableCollectionSynchronization does two things:

  1. Remembers the thread from which it is called and causes the data binding pipeline to marshal CollectionChanged events on that thread.
  2. Acquires a lock on the collection until the marshalled event has been handled, so that the event handlers running UI thread will not attempt to read the collection while it's being modified from a background thread.

Very importantly, this does not take care of everything: to ensure thread-safe access to an inherently not thread-safe collection you have to cooperate with the framework by acquiring the same lock from your background threads when the collection is about to be modified.

Therefore the steps required for correct operation are:

1. Decide what kind of locking you will be using

This will determine which overload of EnableCollectionSynchronization must be used. Most of the time a simple lock statement will suffice so this overload is the standard choice, but if you are using some fancy synchronization mechanism there is also support for custom locks.

2. Create the collection and enable synchronization

Depending on the chosen lock mechanism, call the appropriate overload on the UI thread. If using a standard lock statement you need to provide the lock object as an argument. If using custom synchronization you need to provide a CollectionSynchronizationCallback delegate and a context object (which can be null). When invoked, this delegate must acquire your custom lock, invoke the Action passed to it and release the lock before returning.

3. Cooperate by locking the collection before modifying it

You must also lock the collection using the same mechanism when you are about to modify it yourself; do this with lock() on the same lock object passed to EnableCollectionSynchronization in the simple scenario, or with the same custom sync mechanism in the custom scenario.

Solution 3

With .NET 4.0 you can use these one-liners:

.Add

Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));

.Remove

Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));

Solution 4

Collection synchronization code for posterity. This uses simple lock mechanism to enable collection sync. Notice that you'll have to enable collection sync on the UI thread.

public class MainVm
{
    private ObservableCollection<MiniVm> _collectionOfObjects;
    private readonly object _collectionOfObjectsSync = new object();

    public MainVm()
    {

        _collectionOfObjects = new ObservableCollection<MiniVm>();
        // Collection Sync should be enabled from the UI thread. Rest of the collection access can be done on any thread
        Application.Current.Dispatcher.BeginInvoke(new Action(() => 
        { BindingOperations.EnableCollectionSynchronization(_collectionOfObjects, _collectionOfObjectsSync); }));
    }

    /// <summary>
    /// A different thread can access the collection through this method
    /// </summary>
    /// <param name="newMiniVm">The new mini vm to add to observable collection</param>
    private void AddMiniVm(MiniVm newMiniVm)
    {
        lock (_collectionOfObjectsSync)
        {
            _collectionOfObjects.Insert(0, newMiniVm);
        }
    }
}
Share:
73,765
Maciek
Author by

Maciek

Updated on January 25, 2022

Comments

  • Maciek
    Maciek over 2 years

    I've got an ObservableCollection<A> a_collection; The collection contains 'n' items. Each item A looks like this:

    public class A : INotifyPropertyChanged
    {
    
        public ObservableCollection<B> b_subcollection;
        Thread m_worker;
    }
    

    Basically, it's all wired up to a WPF listview + a details view control which shows the b_subcollection of the selected item in a separate listview (2-way bindings, updates on propertychanged etc.).

    The problem showed up for me when I started to implement threading. The entire idea was to have the whole a_collection use it's worker thread to "do work" and then update their respective b_subcollections and have the gui show the results in real time.

    When I tried it , I got an exception saying that only the Dispatcher thread can modify an ObservableCollection, and work came to a halt.

    Can anyone explain the problem, and how to get around it?

  • Maciek
    Maciek over 14 years
    what about the BackgroundWorker's runWorkerAsyncCompleted? is that bound to the UI thread as well?
  • Josh
    Josh over 14 years
    Yeah the way BackgroundWorker is designed is to use the SynchronizationContext.Current to raise its completion and progress events. The DoWork event will run on the background thread. Here's a good article about threading in WPF that discusses BackgroundWorker too msdn.microsoft.com/en-us/magazine/cc163328.aspx#S4
  • Beaker
    Beaker almost 14 years
    This answer is beautiful in its simplicity. Thanks for sharing it!
  • supercat
    supercat over 11 years
    Does this cause collection updates to block until the UI thread gets around to handling them? In scenarios involving one-way data bound collections of immutable objects (a relatively common scenario), it would seem like it would be possible to have a collection class that would keep a "last displayed version" of each object as well as a change queue, and use BeginInvoke to run a method that would perform all appropriate changes in the UI thread [at most one BeginInvoke would be pending at any given time.
  • Josh
    Josh about 8 years
    @Michael In the majority of cases, the background thread should not be blocking and waiting on the UI to update. Using Dispatcher.Invoke runs the risk of dead locking if the two threads wind up waiting on each other and at best will stunt the performance of your code significantly. In your particular case, you may need to do it this way, but for the vast majority of situations, your last sentence is simply not correct.
  • Kohanz
    Kohanz over 7 years
    It's unclear to me how this is an improvement over invoking to the Dispatcher when necessary. They're clearly different approaches, both seemingly requiring about the same amount of extra code - so an explanation over why one is preferred over the other would be helpful. Off the top of my head, it would seem that failure to invoke to the dispatcher would result in a more predictable failure (that could be caught and fixed), while forgetting to synchronize access to a collection change could be missed until an environment with different timing behaviour is encountered.
  • Mike Marynowski
    Mike Marynowski about 7 years
    @Kohanz Invoking to the UI thread dispatcher has a number of downsides. The biggest one is that your collection won't be updated until the UI thread actually processes the dispatch, and then you will be running on the UI thread which can cause responsiveness issues. With the locking method on the other hand, you immediately update the collection and can continue to do processing on your background thread without depending on the UI thread doing anything. The UI thread will catch up with the changes on the next render cycle as needed.
  • Reginald Blue
    Reginald Blue about 7 years
    I've been looking at collection synchronization in 4.5 for about a month now, and I don't think some of this answer is correct. The answer states that the enable call must occur on the UI thread and that the callback occurs on the UI thread. Neither of these seem to be the case. I am able to enable collection synchronization on a background thread and still utilize this mechanism. Further, the deep calls in the framework do not do any marshalling (cf ViewManager.AccessCollection. referencesource.microsoft.com/#PresentationFramework/src/… )
  • Matthew S
    Matthew S over 6 years
    There is more insight from the answer to this thread about EnableCollectionSynchronization: stackoverflow.com/a/16511740/2887274
  • Wouter
    Wouter over 4 years
    This approach will eventually lead to hard to debug concurrency problems and application crashes.
  • ToolmakerSteve
    ToolmakerSteve about 4 years
    @supercat - "Does this cause collection updates to block until the UI thread gets around to handling them?" No. It just causes the UI thread to wrap its accesses in lock(YourLockObject) { ... }. Just like you do in your code. Nothing mysterious. The reason to do this, instead of any technique that defers updates to the collection, is that this way, the collection is always up-to-date.
  • Herman
    Herman over 2 years
    @LadderLogic's answer already provided an example 3 years ago.
  • CBFT
    CBFT over 2 years
    This assumes Application.Current is not null.