Fixing the Cross-Thread Exception When Updating an ObservableCollection in WPF

Modifying a bound ObservableCollection off the UI thread throws a NotSupportedException from CollectionView affinity. This covers the cause and two fixes.

Overview

By default, modifying an ObservableCollection<T> bound to an ItemsControl from a thread other than the UI thread throws a NotSupportedException (registering BindingOperations.EnableCollectionSynchronization, described below, lifts this restriction).
The message reads along the lines of “This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread” (the exact wording varies by .NET version and locale).
This article explains that the exception comes from the thread affinity of the CollectionView rather than the collection itself, and it organizes the fixes based on BindingOperations.EnableCollectionSynchronization and the Dispatcher, along with the criteria for choosing between them.


Prerequisites / Environment


Problem

Modifying a bound collection directly from a background thread raises an exception.
The following example adds items to an ObservableCollection<T> from work started with Task.Run.
File.ReadLines (from System.IO) is used as a stand-in data source; it enumerates a file’s lines lazily.
The snippets in this article are members of a ViewModel class and assume the namespaces System.Collections.ObjectModel, System.IO, System.Threading.Tasks, System.Windows, and System.Windows.Data.

public ObservableCollection<string> Items { get; } = new();

private async Task LoadAsync(string path)
{
    await Task.Run(() =>
    {
        foreach (var line in File.ReadLines(path))
        {
            // Add from a non-UI thread throws NotSupportedException
            Items.Add(line);
        }
    });
}

When Items is bound to ItemsControl.ItemsSource, the Add call reaches the CollectionView through a CollectionChanged notification.
Because that notification arrives from a non-UI thread, the CollectionView throws the exception.

A three-lane diagram of the data flow between threads. The first lane shows a CollectionChanged notification arriving directly at the CollectionView from a background thread and raising an exception. The second shows EnableCollectionSynchronization: the change stays on the background thread while the notification is queued and applied asynchronously to the UI thread's shadow copy. The third shows the Dispatcher moving the collection operation itself onto the UI thread.
The exception is raised not when the collection is touched, but when the change notification reaches the CollectionView. The two fixes work differently. EnableCollectionSynchronization makes the CollectionView participate in the same synchronization mechanism and applies queued notifications asynchronously to the shadow copy it keeps for the UI thread — the mutation itself may stay on the background thread. The Dispatcher instead marshals the collection operation onto the UI thread.

Cause / Background

ObservableCollection<T> itself is not thread-safe, but that is a separate matter: the direct cause of this NotSupportedException is the CollectionView that WPF routes collection access through when displaying it.
The official documentation states that both the ItemsControl and the CollectionView have affinity to the thread on which the ItemsControl was created, that using them on a different thread is forbidden, and that doing so throws an exception.
In effect, this restriction extends to the bound collection as well.

Most WPF objects derive from DispatcherObject and carry thread affinity to their creating thread, which is normally the UI thread.
The CollectionView also derives from DispatcherObject and, by default, does not allow its bound collection to be changed from another thread.
As a result, when a CollectionChanged notification arrives from a non-UI thread, the CollectionView throws NotSupportedException because it does not permit cross-thread changes.
The root of the problem is therefore not that the collection was touched on another thread, but that the UI-thread-owned CollectionView cannot receive a change notification originating from a different thread.


Solution

There are two approaches.

The former moves changes onto the UI thread; the latter lets WPF safely take in changes made on another thread.


Implementation

Marshal to the UI thread with the Dispatcher

Move the collection mutation to the UI thread with Dispatcher.Invoke (or InvokeAsync).
Using Application.Current.Dispatcher obtains the UI thread Dispatcher even from a view model.
This assumes a single UI thread; in an application with multiple UI threads, Application.Current.Dispatcher refers to the main thread and may not own the bound CollectionView, so capture the Dispatcher associated with the bound ItemsControl (or its CollectionView) instead.

private async Task LoadAsync(string path)
{
    var dispatcher = Application.Current.Dispatcher;
    await Task.Run(() =>
    {
        foreach (var line in File.ReadLines(path))
        {
            // Add runs on the UI thread, so no exception occurs
            dispatcher.Invoke(() => Items.Add(line));
        }
    });
}

Because the mutation runs on the UI thread, no affinity violation occurs in the CollectionView.
Invoking per item causes many round-trips to the UI thread, however, so processing several items within a single Invoke is preferable when items can be added in batches.

Share a lock with EnableCollectionSynchronization

Provide a lock object and register it with WPF by calling EnableCollectionSynchronization on the UI thread.
From then on, all application-side modifications must be protected by that same lock.

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

public ViewModel()
{
    // Call on the UI thread and before using the collection on another thread
    BindingOperations.EnableCollectionSynchronization(Items, _lock);
}

private async Task LoadAsync(string path)
{
    await Task.Run(() =>
    {
        foreach (var line in File.ReadLines(path))
        {
            lock (_lock)
            {
                Items.Add(line);
            }
        }
    });
}

Once EnableCollectionSynchronization is called, the CollectionView accesses the collection using the registered lock and maintains a “shadow copy” for the UI thread.
Change notifications are queued as they arrive and applied when the UI thread has the opportunity to do so.
This allows Add to be called directly from a background thread.
As required by the documentation, the call must occur on the UI thread and before the collection is used on another thread (or attached to the control), whichever is later.


Notes


Alternatives / Comparison

Approach Pros Cons Best suited for
Dispatcher.Invoke / InvokeAsync No extra setup; simple and easy to retrofit Per-item round-trips can strain the UI thread Low update frequency and volume; occasional add or remove
EnableCollectionSynchronization (simple lock) Direct modification from the background; less UI pressure Requires consistent locking; slightly more design effort High-volume, high-frequency updates on another thread
EnableCollectionSynchronization (callback) Allows non-lock mechanisms such as semaphores Most complex to implement A design that already has a custom synchronization mechanism
Batch on the UI thread Avoids the threading issue entirely Loses the benefit of background work Work that can apply all changes at once after gathering

Summary

The exception raised when a bound ObservableCollection<T> is modified from another thread comes from the thread affinity of the CollectionView, not the collection.
The selection criteria are as follows.

In every case, design with the understanding that UI elements themselves remain UI-thread-only, and that only collection access is relaxed.