Observablecollection and Threading

How do I update an ObservableCollection via a worker thread?

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.

ObservableCollection and threading

JaredPar's approach is a valid one. Another approach which is worth considering is using a thread safe ObservableCollection instead of the built-in ObservableCollection. There's a few implementations out there, but Sasha Barber's implementation and the CLinq Continuous Collection class are some of the better ones in my opinion. Internally, these classes essentially use the approach outlined by JaredPar, but encapsulate it inside the collection class.

Threading and creating ObservableCollection

The problem isn't actually the ObservableCollection<Something> at all, it's the <ItemsControl ItemsSource="{Binding Settings.collection}".

ObservableCollection itself doesn't have any problem being accessed across multiple threads (as long as those threads are accessing it one at a time). The same goes for normal (non-DependencyProperty) properties- they don't inheritly belond to a single thread. The WPF binding system can (for the most part) handle changes to bound properties from other threads (provided you aren't using DependencyProperty). What can't handle changes from other threads, is ItemsControl.

Let's step through it, but first I have to make a few assumptions. First, I have to assume that collection is a actually a property, not a field (or is at least exposed via a property), since you can't bind to a field. Second, I'll have to assume that the INotifyPropertyChanged interface is involved, probably implemented on the Settings class.

So stepping through:

  1. In Settings.Modify, you execute collection = new ObservableCollection<Something>();. This successfully updates the property. The WPF binding system gets notified of this change (through the presumed INotifyPropertyChanged interface) and handles the thread change for you. So ItemsControl.ItemsSource gets updated on the UI thread, via the binding, to the new value of collection.
  2. ItemsControl takes this new ObservableCollection and creates a CollectionView which it uses as the data source for its items.
  3. Back in Settings.Modify, you call collection.Add(...). This successfully adds an item to the ObservableCollection<Something>, which triggers its INotifyCollectionChanged.CollectionChanged event.
  4. The CollectionView which the ItemsControl created, handles the CollectionChanged event, but still on the other thread, where it was raised. The CollectionView, as the exception says, doesn't "support changes to its SourceCollection from a thread different from the Dispatcher thread". So it throws an exception. This exception looks like it's coming from collection.Add, but if you look in the call stack, you'll see it actually comes from many frames deeper. collection.Add is just the deepest level of your code that is involved.

When working with ObservableCollections and multiple threads, I would advise creating the full collection on the background thread (if possible), before passing the full collection back to the UI thread for binding. In your example, you could create a local variable, add your items to that, then set it to your collection property once all the items are in place. Alternatively, you could pass individual items back to the UI thread to be added, using Dispatcher.Invoke or a Task<Something>.

Best/cleanest strategy to update an ObservableCollection from another thread

If you are on .NET Framework 4.5 or later you can enable the collection to be accessed across multiple threads by calling the BindingOperations.EnableCollectionSynchronization method. Note that this method must be called on the UI thread:

public class FooClass
{
private readonly object _lock = new object();
public ObservableCollection<string> ItemsCollection { get; } = new ObservableCollection<string>();

internal static FooClass Instance => _Instance ?? (_Instance = new FooClass());
private static FooClass _Instance;

private FooClass()
{
BindingOperations.EnableCollectionSynchronization(ItemsCollection, _lock);
}

private void _UpdateTheCollectionFromAnotherThread()
{
List<string> items = new List<string>();
ItemsCollection.Clear();
foreach (string item in items)
{
ItemsCollection.Add(item);
}
}
}

If the FooClass may be created on a background thread, you might still improve your code a bit by not repeating yourself:

public class FooClass
{
public ObservableCollection<string> ItemsCollection { get; } = new ObservableCollection<string>();

internal static FooClass Instance => _Instance ?? (_Instance = new FooClass());
private static FooClass _Instance;

private FooClass() { }

private void _UpdateTheCollectionFromAnotherThread()
{
List<string> items = new List<string>();

if (System.Windows.Application.Current.Dispatcher.CheckAccess())
ClearCollection(items);
else
System.Windows.Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => ClearCollection(items)));
}

private void ClearCollection(List<string> items)
{
ItemsCollection.Clear();
foreach (string item in items)
{
ItemsCollection.Add(item);
}
}
}

WPF clear ObservableCollection in another thread

I've resolve this problem.
First time ChildReviews was cleared in another thread:

   Task.Run(() => Application.Current.Dispatcher.Invoke((Action)delegate
{
GetChildReviews();
}); )

and then in another dispather:

Application.Current.Dispatcher.Invoke((Action)delegate
{
GetChildReviews();
});

I had to remove Task.Run() and now it works

Using ObservableCollection on a new thread

The approach of multi-threading within any 'heavy' work within an application you want to keep responsive is the right way of thinking about it, so you're on the right track.

However, whilst you are creating and working with other threads here, you are still relying on the Dispatcher too much. Consider that, with multi-threading here, your process should be as follows:

  1. Do the heavy lifting on a separate thread.
  2. Once done, ask the Dispatcher to update the UI as necessary.

This mimimises the load on the Dispatcher.

Have you considered using Tasks? They are great from a 'clean code' point of view, but are applicable here because with Task Continuation, you can chain Tasks together to invoke the relevant code on the UI once the heavy work is complete on its thread.

Take a look at the answer here for a good start.

I'll happily provide a more detailed example if you need it after that.

EDIT: As mentioned in another answer, BackgroundWorker is just as effective here... and the end result is exactly the same from a threading perspective. I just like the Task syntax!

EDIT: Just thought I'd provide some code. I'll avoid continuation for simplicity at the moment. Consider the following method which would do your heavy lifting:

    public void HeavyLifting(Action<List<Items>> callback)
{
Task<List<Items>> task = Task.Factory.StartNew(
() =>
{
var myResults = new List<Items>();

// do the heavy stuff.

return myResults;
});

callback.Invoke(task.Result);
}

Then for your UI (in your ViewModel for example), you can both call and handle the callback. When needed, call the 'heavy lifting' and pass in your callback:

HeavyLifting(this.HandleHeavyLiftingCompleted);

And then, you method you pass as the callback gets executed upon task completion. Note that here is where I'm asking the Dispatcher to do the work:

private void HandleHeavyLiftingCompleted(List<Items> results)
{
this._uiDispatcher.BeginInvoke(
new Action(() => { this.MyItems = new ObservableCollection<Items>(results); }));
}

Note that in this case the UI work involved is updating an ObvservableCollection to which I'm bound from the View. For the example here I'm using a random 'Item' object that can be whatever you like!

I'm using Cinch, and therefore relying on a service to obtain the relevant Dispatcher (which you see as this._uiDispatcher here). In your case you can get a reference to it using the methods mentioned in other questions here.

Also, if you have the time to read, there is some great information here regarding the WPF threading model.

Using ObservableCollection across UI and Non-UI threads

My question is: Am I doing something wrong in the first listing or is the blog post incorrect with the claim that thread boundaries can always succesfully be crossed with this mechanism?

The thing is that you need to call the BindingOperations.EnableCollectionSynchronization method on the UI thread, i.e. you need to instantiate your SortableObservableCollection<T> on the UI thread for this approach to work.

If you can't guarantee that the collection will be initialized on the UI thread you should use the dispatcher to marshal all operations that modifies the data-bound collection back to the UI thread. Calling BindingOperations.EnableCollectionSynchronization on a background thread won't solve your issue.



Related Topics



Leave a reply



Submit