How to Update an Observablecollection Via a Worker Thread

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.

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);
}
}
}

Update ObservableCollection from background Worker


  1. You can use BackgroundWorker instead of thread, to report progress as it goes.
    Here is a simple tutorial
  2. I believe that simply calling Dispatcher will use thread of a context, which is not UI-thread in your case. Try Application.Current.Dispatcher instead

In short, I believe you should do the following:

  1. Create public ObservableCollection in UI-thread and bind it to DataGrid
  2. Create a background worker. Set reporting to true. Subscribe to ReportProgress and DoWork events.
  3. Run worker async
  4. In DoWork handler create a list and read some amount of values to it. As you reach some amount, let's say a hundred, call (sender as BackgroundWorker).ReportProgress method, passing in event args this collection you have populated.
  5. In report progress handler, populate your ObservableCollection from a list you've passed throught event args.
  6. Steps 4 - 5 are repeated until everything is done

Updating an ObservableCollection in a separate thread

With the built-in ObservableCollection<T> class, you can't change the content from a separate thread if the UI is bound to the collection, it throws a NotSupportedException (but change notification for properties of collection items works fine). I wrote an AsyncObservableCollection<T> class to handle this case. It works by invoking the event handlers on the UI synchronization context

How to update only a property in an observable collection from thread other than dispatcher thread in WPF MVVM?

Either use the dispatcher to call the Add method on the UI thread:

Application.Current.Dispatcher.BeginInvoke(() => DailyEmployees.Add(em)); 

Or call the BindingOperations.EnableCollectionSynchronization method on the UI thread to enable the collection to be used on multiple threads:

public class DisplayViewModel
{
private readonly ObservableCollection<Employee> _dailyEmployees = new ObservableCollection<Employee>();
private readonly object _lock = new object();

public ObservableCollection<Employee> DailyEmployees
{
get { return _dailyEmployees; }
}

public DisplayViewModel()
{
System.Windows.Data.BindingOperations.EnableCollectionSynchronization(_dailyEmployees, _lock);
OnStatusChanged += DisplayViewModel_OnStatusChanged;
}

//invoked in other thread
private void DisplayViewModel_OnStatusChanged(object sender, EventArgs e)
{
var d = sender as Employee;
if (d == null)
return;
var em = DailyEmployees.FirstOrDefault(a => a.Name == d.Name);

if (em == null)
{
DailyEmployees.Add(em);
}
else
{
em.Status = d.Status;
}
}
}

Also note that the Employee class should implement the INotifyPropertyChanged interface and raise the PropertyChanged event for the Status property if you want the value of this one to be reflected in the view when you set it.

What's the best way to update an ObservableCollection from another thread?

If MVVM

public class MainWindowViewModel : ViewModel {

private ICommand loadcommand;
public ICommand LoadCommand { get { return loadcommand ?? (loadcommand = new RelayCommand(param => Load())); } }

private ObservableCollection<ViewModel> items;
public ObservableCollection<ViewModel> Items {
get {
if (items == null) {
items = new ObservableCollection<ViewModel>();
}
return items;
}
}

public void Load() {
BackgroundWorker bgworker = new BackgroundWorker();
bgworker.WorkerReportsProgress = true;
bgworker.DoWork += (s, e) => {
for(int i=0; i<10; i++) {
System.Threading.Thread.Sleep(1000);
bgworker.ReportProgress(i, new List<ViewModel>());
}
e.Result = null;
};
bgworker.ProgressChanged += (s, e) => {
List<ViewModel> partialresult = (List<ViewModel>)e.UserState;
partialresult.ForEach(i => {
Items.Add(i);
});
};
bgworker.RunWorkerCompleted += (s, e) => {
//do anything here
};
bgworker.RunWorkerAsync();
}
}

Updating my ObservableCollection on a worker thread still hangs my UI

Some thoughts to consider to improve the speed and responsiveness of your code

A few days ago I asked a similar question and someone advised me not to use the threadpool for long running Tasks. The thread pool is a collection of available threads, that can be started swiftly in comparison to starting a traditional thread like System.ComponentModel.BackGroundWorker.

Although it takes more time to create and start a real thread, this is no problem for long running tasks.

The number of threads in the thread pool is limited, so better not use it for longer running tasks.

If you run a task, it is only scheduled to run in the near future when a thread is available. If all threads are busy it will take some time before the thread starts.

The change from Task to Backgroundworker is limited. If you really want to stick to tasks, consider creating an async function:

async void FilteredLogEvents.AddRangeAsync(IEnumerable<LogEvent> logEvents)

or maybe better:

async void FilteredLogEvents.SetAsync(IEnumerable<LogEvent> logEvents)

which does the clear and add in one async call.

Make your own function async:

private async void FilterList()
{
var filteredLogEvents = FilterLogEvents();
var myTask = Task.Run( () => FilteredLogEvents.SetAsync(filteredLogEvents);
// if desired do other things.
// wait until ready:
await myTask();
RaisePropertyChanged("FilteredLogEvents");
FinishedFilteringLogs();
}

By the way: Are you sure that your sequence of logEvents does not change while you are filtering it?

  • If so, why do you use ToList() instead of returning an IEnumerable and use deferred execution?
  • If you are not certain: what happens if during the FilterLogEvents your sequence of logEvents changes?

Updating ObservableCollection with CollectionSynchronization

The exception occurs because of thread affinity in WPF, which is by design.

Both the ItemsControl and the CollectionView have affinity to the thread on which the ItemsControl was created, meaning that using them on a different thread is forbidden and throws an exception.

When your collection changes, it will raise a CollectionChanged event, which will be handled by the control that binds the ObservableCollection. This control will then update its items on the same thread. In your example this is not the UI thread, but your worker thread.

Binding operations solution

Your approach using BindingOperations.EnableCollectionSynchronization is available since .NET Framework 4.5 and perfectly valid, but you must ensure that there are no deadlocks with the synchronization mechanism of your choice, which can be tricky. I recommend you to check the out reference for details on that.

Dispatcher solution

A more general way to resolve this issue is to delegate adding items to the UI thread.

public void HandleNewOffers(object sender, ICollection<Offer> newOffers)
{
// The same as you do, but simplified with Linq to return a filtered enumerable
var filteredOffers = newOffers.Where(no => !_currentOffers.Any(co => co.Id == no.Id)).Select(MapOffer);

// Use this to invoke the add method synchronously on the UI thread
System.Windows.Application.Current.Dispatcher.Invoke(() => AddToCurrentOffers(filteredOffers));

// Use this to alternatively invoke the add method asynchronously on the UI thread
System.Windows.Application.Current.Dispatcher.InvokeAsync(() => AddToCurrentOffers(filteredOffers));
}

// Just a helper method that can also be inlined as lambda
private void AddToCurrentOffers(IEnumerable<Offer> offers)
{
foreach (var offer in offers)
{
_currentOffers.Add(offer);
}
}

What the code does is use the Dispatcher of the application to invoke a helper lambda method that adds your items to the _currentOffers collection on the UI thread. You can either do this synchronously or asynchronously as commented in the code.

I have replaced your loop with a Linq query that results in an enumerable on purpose. This offers the advantage, that you queue the lambda for execution on the UI thread one time for all items, instead of countless times for a single item, which is more efficient to reduce context switches that can degrade performance drastically. On the other hand, adding a large number of items can be a long running operation that blocks the UI thread an therefore freezes your application. In effect, it depends on the actual workload and the frequency of adding items to make the right choice for your use-case. This also applies to the mechanism itself, be it the dispatcher or the binding operations approach.

Worker thread update ObservableCollection that is bound to a ListCollectionView (.NET 4.0)

When you're updating the collection, just switch it over so it updates in the UI Thread. You can do that by using the Dispatcher.

// Other code here...

// Now update the collection in the UI Thread. Use BeginInvoke if it needs to be Async
Application.Current.Dispatcher.Invoke(
new Action(() =>
{
// Add to the collection here
foreach (object myObject in myObjects)
{
this.myCollection.Add(myObject);
}
}));

// Any other code needed here...


Related Topics



Leave a reply



Submit