Thread-Safe & Dispatcher-Safe Observable Collection for WPF

by Dean 1. February 2010 12:22

A common problem in WPF (& Silverlight) development is when you are working with multiple threads that need to change a collection that is a binding source and implements INotifyCollectionChanged.

Basically, the standard ObservableCollection<T> will only allow updates from the dispatcher thread, which means you need to write a lot of code for the worker threads to marshal changes onto the main message pump via the dispatcher. This can be a bit tedious, so I recently wrote a collection that performs all of the necessary marshalling internally, so users of this type do not have to be concerned about thread affinity issues.

Also, I decided to use a ReaderWriterLock to provide thread-safety during updates to the collection.

Here is my collection class:

 
public class SafeObservable<T> : IList<T>, INotifyCollectionChanged
{
    private IList<T> collection = new List<T>();
    private Dispatcher dispatcher;
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    private ReaderWriterLock sync = new ReaderWriterLock();
 
    public SafeObservable()
    {
        dispatcher = Dispatcher.CurrentDispatcher;
    }
 
    public void Add(T item)
    {
        if (Thread.CurrentThread == dispatcher.Thread)
            DoAdd(item);
        else
            dispatcher.BeginInvoke((Action)(() => { DoAdd(item); }));
    }
 
    private void DoAdd(T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        collection.Add(item);
        if (CollectionChanged != null)
            CollectionChanged(this,
                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
        sync.ReleaseWriterLock();
    }
 
    public void Clear()
    {
        if (Thread.CurrentThread == dispatcher.Thread)
            DoClear();
        else
            dispatcher.BeginInvoke((Action)(() => { DoClear(); }));
    }
 
    private void DoClear()
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        collection.Clear();
        if (CollectionChanged != null)
            CollectionChanged(this,
                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        sync.ReleaseWriterLock();
    }
 
    public bool Contains(T item)
    {
        sync.AcquireReaderLock(Timeout.Infinite);
        var result = collection.Contains(item);
        sync.ReleaseReaderLock();
        return result;
    }
 
    public void CopyTo(T[] array, int arrayIndex)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        collection.CopyTo(array, arrayIndex);
        sync.ReleaseWriterLock();
    }
 
    public int Count
    {
        get
        {
            sync.AcquireReaderLock(Timeout.Infinite);
            var result = collection.Count;
            sync.ReleaseReaderLock();
            return result;
        }
    }
 
    public bool IsReadOnly
    {
        get { return collection.IsReadOnly; }
    }
 
    public bool Remove(T item)
    {
        if (Thread.CurrentThread == dispatcher.Thread)
            return DoRemove(item);
        else
        {
            var op = dispatcher.BeginInvoke(new Func<T,bool>(DoRemove), item);
            if (op == null || op.Result == null)
                return false;
            return (bool)op.Result;
        }
    }
 
    private bool DoRemove(T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        var index = collection.IndexOf(item);
        if (index == -1)
        {
            sync.ReleaseWriterLock();
            return false;
        }
        var result = collection.Remove(item);
        if (result && CollectionChanged != null)
            CollectionChanged(this, new
                NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        sync.ReleaseWriterLock();
        return result;
    }
 
    public IEnumerator<T> GetEnumerator()
    {
        return collection.GetEnumerator();
    }
 
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return collection.GetEnumerator();
    }
 
    public int IndexOf(T item)
    {
        sync.AcquireReaderLock(Timeout.Infinite);
        var result = collection.IndexOf(item);
        sync.ReleaseReaderLock();
        return result;
    }
 
    public void Insert(int index, T item)
    {
        if (Thread.CurrentThread == dispatcher.Thread)
            DoInsert(index, item);
        else
            dispatcher.BeginInvoke((Action)(() => { DoInsert(index, item); }));
    }
 
    private void DoInsert(int index, T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        collection.Insert(index, item);
        if (CollectionChanged != null)
            CollectionChanged(this,
                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
        sync.ReleaseWriterLock();
    }
 
    public void RemoveAt(int index)
    {
        if (Thread.CurrentThread == dispatcher.Thread)
            DoRemoveAt(index);
        else
            dispatcher.BeginInvoke((Action)(() => { DoRemoveAt(index); }));
    }
 
    private void DoRemoveAt(int index)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        if (collection.Count == 0 || collection.Count <= index)
        {
            sync.ReleaseWriterLock();
            return;
        }
        collection.RemoveAt(index);
        if (CollectionChanged != null)
            CollectionChanged(this,
                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        sync.ReleaseWriterLock();
 
    }
 
    public T this[int index]
    {
        get
        {
            sync.AcquireReaderLock(Timeout.Infinite);
            var result = collection[index];
            sync.ReleaseReaderLock();
            return result;
        }
        set
        {
            sync.AcquireWriterLock(Timeout.Infinite);
            if (collection.Count == 0 || collection.Count <= index)
            {
                sync.ReleaseWriterLock();
                return;
            }
            collection[index] = value;
            sync.ReleaseWriterLock();
        }
 
    }
}
 

To test the effectiveness of this collection class, I wrote a simple WPF app, that bound to the new collection class and updated it via multiple threads:

 

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <StackPanel Orientation="Vertical" VerticalAlignment="Top">
        <Button Content="Start" Click="Button_Click" />
        <ListView Name="list" ItemsSource="{Binding}" DisplayMemberPath="Text" />        
    </StackPanel>
</Window>

And the code behind is below:

 

public partial class Window1 : Window
{
    class TestData
    {
        public string Text { get; set; }
    }
 
    private Random rand = new Random(DateTime.Now.Millisecond);
    private SafeObservable<TestData> data = new SafeObservable<TestData>();
    public Window1()
    {
        InitializeComponent();
    }
 
    void Button_Click(object sender, RoutedEventArgs e)
    {
        list.DataContext = data;
        List<Action> work = new List<Action>();
        for (int i = 0; i < 100; i++)
        {
            work.Add(new Action(DoWorkAdd));
            work.Add(new Action(DoWorkClear));
            work.Add(new Action(DoWorkRemove));
            work.Add(new Action(DoWorkRemoveAt));
            work.Add(new Action(DoWorkInsert));
            work.Add(new Action(DoWorkReplace));
        }
        for (int i = 0; i < 1000; i++)
            work[rand.Next(0, work.Count)].BeginInvoke(null, null);
 
    }
 
    void DoWorkAdd()
    {
        Thread.Sleep(rand.Next(500, 30000));
        data.Add(new TestData() { Text = string.Format("Thread {0} Added", Thread.CurrentThread.ManagedThreadId) });
    }
 
    void DoWorkClear()
    {
        Thread.Sleep(rand.Next(500, 10000));
        data.Clear();
        Debug.WriteLine((string.Format("Thread {0} Clear", Thread.CurrentThread.ManagedThreadId)));
    }
 
    void DoWorkRemove()
    {
        Thread.Sleep(rand.Next(500, 10000));
        if (data.Count == 0)
            return;
        var item = data[0];
        data.Remove(item);
        Debug.WriteLine((string.Format("Thread {0} Remove", Thread.CurrentThread.ManagedThreadId)));
    }
 
    void DoWorkRemoveAt()
    {
        Thread.Sleep(rand.Next(500, 10000));
        if (data.Count == 0)
            return;
        data.RemoveAt(0);
        Debug.WriteLine((string.Format("Thread {0} RemoveAt", Thread.CurrentThread.ManagedThreadId)));
    }
 
    void DoWorkInsert()
    {
        Thread.Sleep(rand.Next(500, 10000));
        data.Insert(rand.Next(0, data.Count), new TestData() 
            { Text = string.Format("Thread {0} Insert", Thread.CurrentThread.ManagedThreadId) });
    }
 
    void DoWorkReplace()
    {
        Thread.Sleep(rand.Next(500, 10000));
        data[rand.Next(0, data.Count)] = new TestData() 
            { Text = string.Format("Thread {0} Replace", Thread.CurrentThread.ManagedThreadId) };
    }
 
}

All my WPF app does is run a number of random actions against the collection from a variety of threads.

NOTE:  When removing items from the collection I used the Refresh action of NotifyCollectionChangedAction instead of Remove. This is because the remove action doesnt work correctly in a multi-threaded scenario when used as a binding source for a list control in WPF.

If anyone has any siggestions or enhancements, please let me know

Dean

Tags:

DataBinding | C# | Threading

Comments


February 1. 2010 13:29
trackback
Trackback from DotNetKicks.com

Thread-Safe


February 1. 2010 14:05
trackback
Trackback from DotNetShoutout

ButtonChrome.com | Thread-Safe & Dispatcher-Safe Observable Collection for WPF


 Tom 
February 1. 2010 16:54
Tom
One thing I noticed with a custom INotifyCollectionChanged I was using is that if you don't implement IList (not templated), you'll end up with an EnumerableCollectionView instead of a ListCollectionView.  This has a performance penalty that you'd probably like to avoid.


February 1. 2010 17:33
Dean
Sure, good comment.
This class implements IList - so should perform well, however I havent had a chance to fully test it's performance yet Smile

Dean


United States Eric 
March 16. 2010 23:01
Eric
I like this, we did notice though that the lock in DoRemove will not be released if (index == -1)


March 20. 2010 08:54
Dean
Good catch - thanks. I have updated the code Smile


Israel Saragani 
March 28. 2010 15:18
Saragani
Hi,
You should replace the following line:
dispatcher = Dispatcher.CurrentDispatcher;

With this one:
dispatcher = Application.Current.Dispatcher;


The difference is that Application.Current.Dispatcher is being created on the main application thread (The GUI thread), while Dispatcher.CurrentDispatcher is on the current thread.
If for example, you create your ViewModel from a thread which is different from the GUI thread then your Thread-Safe Observable Collection is no longer thread safe (The binding to the GUI creates errors).


Usually you don't create ViewModels in the background, but with List/Collection you sometimes wanna do that to prevent the GUI from hanging if adding lots of elements or the process of creating an element and adding it to the collection is slow (from example, it's being loaded from database).


March 30. 2010 13:21
Dean Chalk
Hi Saragani

You should not use 'Application.Current.Dispatcher' as you may have created multiple UI message pump threads in the same WPF application (a common solution in trading desk software). I cant think of a good reason to create a ViewModel on a background thread. I recently built a virtual observable collection for retrieving live data from a Tibco system that was changing data on hundreds of rows concurrently and I had no problems. Have you looked at DispatcherPriority ?  


Israel Saragani 
March 31. 2010 06:05
Saragani
Ok, here is something I don't understand... WPF GUI has 2 threads, 1 for inputs and 1 for rendering.
WPF by default has only 1 SynchronizationContext and each thread has only one Dispatcher.

In a code I'm writing I've had the following code in the ViewModelBase:

        public ViewModelBase()
        {
            dispatcher = Dispatcher.CurrentDispatcher;
        }

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;

            if (handler != null)
            {
                Action action = delegate()
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                };
                updateUI(action);
            }
        }

        private void updateUI(Action action)
        {
            if (Thread.CurrentThread == dispatcher.Thread)
                action();
            else
                dispatcher.Invoke(action);
        }



But I understand that it doesn't matter since WPF automatically marshals property changes to the UI thread.


The Dispatcher is looping through all the messages in the queue while pumping first messages with a higher priority.


If the ViewModel was created on the GUI thread then Application.Current.Dispatcher should be equal to Dispatcher.CurrentDispatcher


Can you please give me a link to an article that explains the multiple UI message pump threads?
(Cause right now, from the data I read in the internet the messages are being pumped into 1 dispatcher only).

Thanks


Israel Saragani 
March 31. 2010 06:17
Saragani
Hi, nevermind.... I've found this:  Smile
eprystupa.wordpress.com/.../


It explains how to create a WPF application where each window has it's own GUI thread and dispatcher.

Now I understand the difference between the application.current.dispatcher and dispatcher.currentdispatcher.


Thank you Smile


United States Andrew Walker 
January 13. 2011 18:52
Andrew Walker
Thanks for the nice post.  This is exactly what I was looking for. One question: if you were to write this today, would you use ReaderWriterLockSlim?  Why or why not?


United States Andrew Walker 
January 13. 2011 19:29
Andrew Walker
Perhaps you would indulge one more question.  I used your class at at this line:
if (m_ollection.Select(pp => pp.Timestamp).Contains(obj.Timestamp))
{
...
{
got the dreaded debug assert about:
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.InvalidOperationException: Collection was modified; enumeration operation may not execute.

This is not surprising.  You class guarantees that the act of getting the enumerator is thread safe and works.  Undoubtedly what's happened here is that I added or removed and element from the collection from another thread (in fact, the main Gui thread) after I used GetEnumerator in this thread.
I wonder if there is a slick "pattern" or design that should be used here.  I could just throw a lock around the whole collection, or perhaps there is a way to add the method "Select" to your class.  Yet another idea, would be to only use your IndexOf method.  But then it would be nice to somehow disallow the LINQ commands like Select so the programmer gets a compile time error.  I would imagine that this is a common problem.  Comments?


United States Mark Ewer 
May 15. 2011 22:46
Mark Ewer
What is the license on this code?


Switzerland daniel marbach 
May 17. 2011 05:45
daniel marbach
@andrew
You could simply take a snapshot of the collection and return the snapshot for enumeration. This is hreadsafe.

Daniel


May 17. 2011 19:20
pingback
Pingback from planetgeek.ch

planetgeek.ch » Threadsafe ObservableCollection


September 15. 2011 02:45
pingback
Pingback from programmersgoodies.com

Slow WPF 4 Datagrid Refresh - Programmers Goodies


Germany krisha 
December 21. 2011 23:26
krisha
Thanks for showing how to do. Very nice, don't understand why something like this is not provided by MS :-/

Two possible improvements:
a) make is serializable, so it can be written easy to XML e.g.
b) if the code is modified by a Timer (Elapsed Event), it might crash, since the Timer runs in a ThreadPool and the thread might be the same like the UI thread... Not sure how to detect this.


Germany krisha 
December 21. 2011 23:59
krisha
sorry, remove the point for serialization Smile

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading

FAO Comment Spammers : Please note that this blog is fully moderated by me personally and no comment spam will ever appear on this site.



RecentComments

Comment RSS
Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2012 Dean Chalk's Blog