Thread-safe observable collection in .NET

 
 
  • Gérald Barré

If you have ever bound an ObservableCollection<T> to a WPF control, you know it's hard to add or remove items from multi-threads. Indeed, the UI expects the events to be raised on the UI thread. Also, you should not change the collection until the change event is processed by the UI. This means your algorithm is blocked until the UI process the events. This could slow down your processing as multiple threads may be waiting for the UI.

I've made a new collection ConcurrentObservableCollection<T>. The idea is to have two linked collections. The first one, a thread-safe collection, that you can use in the view model, and a second collection that you can safely bind to a control. When you make a change in the first collection, it will change this collection, and it will also enqueue the change in the second collection. The second collection dequeues the pending changes on the UI thread. This way there is no issue with WPF. This requires a little more of memory, but this is the trade-off to have an easy to use thread-safe observable collection.

Here are the main features of the thread-safe observable collection:

  • ConcurrentObservableCollection<T> is a thread-safe collection that you can manipulate in a view-model or the code-behind. You don't need to lock to access the collection from multiple threads.
  • You can safely bind collection.AsObservable to a WPF control. This observable collection implements INotifyCollectionChanged, INotifyPropertyChanged and IReadOnlyList<T>, so it integrates well with WPF controls.
  • Any change to the collection is directly made to the thread-safe collection and will be replicated to the bind-able collection on the UI thread (using the Dispatcher)
  • Any access to the bind-able collection must be done from the UI thread. While you should not manipulate the observable collection directly, some WPF controls such as the DataGrid need to add items in this list.

Here's an overview of how it work when you add items in a ConcurrentObservableCollection<T>:

  1. Initialize the collection ⇒ Both collections are empty

  2. Add an item to the collection

    • The collection contains 1 item
    • There is 1 pending event (Add) waiting for the dispatcher to be processed, Dispatcher.BeginInvoke is called
    • The bindable collection is still empty has the dispatcher hasn't run the synchronization method

  3. Add a second item to the collection ⇒ The first collection contains 2 items, the bindable collection is still empty has the dispatcher hasn't run the synchronization method

    • The collection contains 2 items
    • There is 2 pending events (Add) waiting for the dispatcher to be processed
    • The bindable collection is still empty has the dispatcher hasn't run the synchronization method

  4. Eventually the dispatcher will process the pending items and do the change to the observable collection ⇒ Both collections contain the same data and the UI should be updated

#How to use it?

  1. Add the NuGet package Meziantou.Framework.WPF (NuGet, GitHub)

  2. Create a new ConcurrentObservableCollection<T> in the code-behind or the view-model and bind the AsObservable property to a control

    XAML
    <Window x:Class="Meziantou.Framework.WPF.CollectionSamples.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Meziantou.Framework.WPF.CollectionSamples"
        Title="MainWindow" Height="450" Width="800">
        <Window.DataContext>
            <local:SampleViewModel />
        </Window.DataContext>
        <StackPanel Orientation="Vertical">
            <Button Command="{Binding AddItems}">Add items</Button>
    
            <!-- ⚠️ Bind Items.AsObservable, not Items -->
            <ListBox Grid.Row="1" ItemsSource="{Binding Items.AsObservable}" />
        </StackPanel>
    </Window>
    C#
    public class SampleViewModel
    {
        public SampleViewModel()
        {
            AddItems = new DelegateCommand(AddItemsImpl);
        }
    
        public ConcurrentObservableCollection<string> Items { get; } = new ConcurrentObservableCollection<string>();
    
        public ICommand AddItems { get; }
    
        private void AddItemsImpl()
        {
            Task.Run(() => Parallel.For(0, 1000, i =>
            {
                // No need to lock here as the collection is thread-safe
                Items.Add($"Item {i}");
            }));
        }
    }
  3. Click the button and observe that everything works great!

That's it! Just use the collection as you would do with any other thread-safe list. The methods are thread-safe, so you don't need to lock before adding or removing an item to the collection nor when enumerating it.

Using this collection, you don't have to worry about the UI thread in the view-model or the code-behind!

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub