ObservableCollection
multithreading
worker thread
C#
data binding

How do I update an ObservableCollection via a worker thread?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

If an ObservableCollection is bound to WPF UI elements, you generally cannot modify it directly from a worker thread. The collection change notifications are consumed by UI components that belong to the dispatcher thread, so cross-thread updates usually throw exceptions or lead to unstable behavior.

The safe pattern is to do expensive work in the background, then marshal only the collection mutation back to the UI thread. That keeps the interface responsive without violating WPF's thread-affinity rules.

Why Direct Background Updates Fail

A bound ObservableCollection raises CollectionChanged events. WPF expects those notifications to occur on the dispatcher thread that owns the bound controls.

This is why code like this is unsafe:

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

Even if it seems to work sometimes, it is not a valid threading model for a bound collection.

Use the Dispatcher for Collection Mutation

A common approach is to fetch or compute data on a background thread, then update the collection on the UI thread.

csharp
1using System.Collections.Generic;
2using System.Collections.ObjectModel;
3using System.Threading;
4using System.Threading.Tasks;
5using System.Windows;
6
7public partial class MainWindow : Window
8{
9    public ObservableCollection<string> Items { get; } = new();
10
11    public MainWindow()
12    {
13        InitializeComponent();
14        DataContext = this;
15    }
16
17    private async void LoadButton_Click(object sender, RoutedEventArgs e)
18    {
19        List<string> rows = await Task.Run(FetchRows);
20
21        await Dispatcher.InvokeAsync(() =>
22        {
23            foreach (string row in rows)
24            {
25                Items.Add(row);
26            }
27        });
28    }
29
30    private static List<string> FetchRows()
31    {
32        Thread.Sleep(500);
33        return new List<string> { "alpha", "beta", "gamma" };
34    }
35}

This is the normal WPF pattern. The background task handles slow work, and the dispatcher handles the UI-facing mutation.

Batch Updates Are Better Than Many Tiny Dispatcher Calls

A common mistake is calling Dispatcher.Invoke once per item inside a loop. That works, but it produces unnecessary UI-thread churn.

This is worse:

csharp
1foreach (string row in rows)
2{
3    Dispatcher.Invoke(() => Items.Add(row));
4}

This is better:

csharp
1await Dispatcher.InvokeAsync(() =>
2{
3    foreach (string row in rows)
4    {
5        Items.Add(row);
6    }
7});

Move the whole batch to the dispatcher in one operation when possible.

Replacing the Collection Can Also Be Useful

If the refresh is effectively "load a new data set," you may prefer building a new collection off-thread and swapping the bound property on the UI thread.

csharp
1private ObservableCollection<string> _items = new();
2public ObservableCollection<string> Items
3{
4    get => _items;
5    set
6    {
7        _items = value;
8        // Raise PropertyChanged here in a real view model.
9    }
10}

Then assign the new collection on the dispatcher thread once the background work completes. This can be cleaner than many incremental mutations for full refresh scenarios.

EnableCollectionSynchronization Is a Special Tool

WPF also provides BindingOperations.EnableCollectionSynchronization, which lets binding code coordinate access with a lock.

csharp
1private readonly object _sync = new();
2public ObservableCollection<string> Items { get; } = new();
3
4public MainWindow()
5{
6    InitializeComponent();
7    BindingOperations.EnableCollectionSynchronization(Items, _sync);
8}

This can help in advanced multi-threaded scenarios, but it is not the first tool to reach for. Dispatcher-based updates are usually simpler and easier to reason about.

Keep Background Work Separate from UI Work

The real design rule is separation of concerns:

  • do network, disk, or CPU-heavy work in the background
  • do collection mutation on the dispatcher
  • keep the dispatcher block as small as possible

If you put slow work inside Dispatcher.Invoke, you lose the main benefit of using a worker thread in the first place.

Common Pitfalls

  • Adding or removing items from a bound ObservableCollection directly on a worker thread.
  • Calling the dispatcher once per item instead of batching UI updates.
  • Performing expensive work inside Dispatcher.Invoke, which blocks the UI.
  • Treating EnableCollectionSynchronization as a blanket replacement for careful UI-thread design.
  • Forgetting cancellation and error handling in long-running background refresh logic.

Summary

  • A UI-bound ObservableCollection should usually be modified on the WPF dispatcher thread.
  • Run heavy work in the background and marshal only the collection mutation back to the UI thread.
  • Batch updates to reduce dispatcher overhead.
  • Consider replacing the whole collection for full refresh scenarios.
  • Use EnableCollectionSynchronization only when you truly need shared multi-threaded access.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.