ObservableCollection
multithreading
thread safety
C#
data binding

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

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

ObservableCollection<T> is widely used in WPF and similar XAML-based UI code because it notifies the UI when items are added or removed. The catch is that the collection is not generally safe to update directly from a background thread.

If a collection is bound to UI elements, the safest default is to treat the UI thread as the owner of that collection. Background work can prepare data, but the actual collection mutation should usually be marshaled back to the UI thread.

Why Cross-Thread Updates Fail

UI frameworks such as WPF have thread affinity. The UI thread owns bound objects and the binding engine expects notifications to occur in a thread-safe way relative to that owner thread.

So code like this is risky:

csharp
1Task.Run(() =>
2{
3    Items.Add("new item");
4});

Even if it appears to work sometimes, it can throw cross-thread exceptions or produce inconsistent UI behavior.

The Usual Solution: Dispatch Back to the UI Thread

In WPF, the most common pattern is to perform background work off the UI thread and then use the dispatcher for the actual update.

csharp
1using System.Collections.ObjectModel;
2using System.Threading.Tasks;
3using System.Windows;
4
5public class MainViewModel
6{
7    public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();
8
9    public async Task LoadAsync()
10    {
11        var data = await Task.Run(() =>
12        {
13            return new[] { "Alpha", "Beta", "Gamma" };
14        });
15
16        await Application.Current.Dispatcher.InvokeAsync(() =>
17        {
18            foreach (var item in data)
19            {
20                Items.Add(item);
21            }
22        });
23    }
24}

The expensive work happens in the background. The collection mutation happens on the UI thread.

Batch Updates Are Better Than Per-Item Crossings

If you add thousands of items one by one through the dispatcher, the repeated thread hops can become expensive. A better pattern is to collect results in the background and then perform one UI-thread batch update.

csharp
1await Application.Current.Dispatcher.InvokeAsync(() =>
2{
3    foreach (var item in batch)
4    {
5        Items.Add(item);
6    }
7});

That is still a loop, but it is one marshaled UI operation rather than many separate cross-thread calls.

In more advanced scenarios, developers replace the whole collection or use a custom range-enabled collection to reduce notification overhead further.

What About BindingOperations.EnableCollectionSynchronization

WPF also provides BindingOperations.EnableCollectionSynchronization, which helps the binding system coordinate access to a shared collection.

csharp
1using System.Collections.ObjectModel;
2using System.Windows.Data;
3
4private readonly object _lock = new object();
5public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();
6
7public MainViewModel()
8{
9    BindingOperations.EnableCollectionSynchronization(Items, _lock);
10}

This can be useful in advanced cases, but it is not a magic replacement for sensible threading design. For many applications, keeping collection ownership on the UI thread is still simpler and more predictable.

A Better Architectural Pattern

A strong pattern is:

  1. load or compute data on a background thread
  2. produce a plain list or array there
  3. dispatch a single UI-thread update to reflect the result

That keeps ObservableCollection<T> focused on UI notification rather than using it as a general-purpose concurrent data structure.

Example with Replacement

If a full refresh is acceptable, replacing the collection content may be simpler than interleaving many background additions.

csharp
1using System.Collections.ObjectModel;
2using System.Linq;
3using System.Threading.Tasks;
4using System.Windows;
5
6public async Task RefreshAsync()
7{
8    var data = await Task.Run(() => Enumerable.Range(1, 5).Select(i => $"Item {i}").ToList());
9
10    await Application.Current.Dispatcher.InvokeAsync(() =>
11    {
12        Items.Clear();
13        foreach (var item in data)
14        {
15            Items.Add(item);
16        }
17    });
18}

This is often easier to reason about than incremental background mutation.

Common Pitfalls

One common mistake is treating ObservableCollection<T> as thread-safe just because it is used for binding. Notification support is not the same as concurrency support.

Another issue is dispatching every single small update individually. That can work, but it often creates unnecessary UI-thread overhead and choppy performance.

It is also easy to overuse synchronization APIs instead of simplifying the design. If the UI thread can own the collection cleanly, that is usually the easier path.

Finally, do not perform long-running work on the UI thread just because the collection update must end there. Only the mutation should return to the UI thread; the expensive computation should stay in the background.

Summary

  • 'ObservableCollection<T> should usually be updated on the UI thread when it is bound to UI controls.'
  • Background threads can prepare data, but collection mutation should be marshaled through the dispatcher.
  • Batch updates are usually better than many tiny cross-thread additions.
  • 'BindingOperations.EnableCollectionSynchronization can help in advanced cases, but it is not a substitute for good ownership design.'
  • Treat the collection as a UI-notification structure, not as a general concurrent container.

Course illustration
Course illustration

All Rights Reserved.