DataBinding & Multi-Threading In WPF [Part One]

|

One of the scenario I think the current version of WPF hasn't done enough is the multi-threaded data binding. Imagine your UI is data bound to a data source collection whose items are modified(added, removed, cleared etc) from another thread rather than the UI thread. you will get the following exception:

This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

Actually, generic data source collections are actually free-threaded(I mean free-threaded not thread safe), but WPF's implementations of CollectionView (such as BindingListCollectionView for ADO.NET data source and  ListCollectionView for general purpose data collections) are actually single-threaded, If you look at the base class of CollectionView, you can see that It directly derives from DispatcherObject, this tells us that CollectionViews are actually Disptacher affinitized, because Dispatchers are tied to the thread which creates them, so CollectionViews end up tying to the their spawning threads. This type of restriction makes multi-threaded data binding a bit more cumbersome and complicated. Beatriz Costa who's working on WPF's data binding feature wrote up a great article ages ago talking about how to do multi-threaded data binding using current version of WPF. She sincerely admit the drawbacks and limitations of current WPF data binding mechanism when it comes to multi-threading. And she came up with an approach to make this type of binding work properly. But one of the things I don't like about her solution is to marshal all the collection modification operations back to the UI thread. I don't think this type of technique can fully leverage the capabilities of worker threads, and much of the benefits inherent with multi-threading are depreciated. How about if the operation you post to UI thread takes a long time to complete, the UI thread will get busy processing this operation, and your UI gets frozen for quite a bit. In this post, I will show another way of doing multi-threaded data binding, and my solution will make as best use of worker threads as possible, enter BindingCollection.

The following code illustrates the implementation of BindingCollection:

using System;
using System.Threading;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;

namespace Sheva.Windows.Data
{
    /// <summary>
    /// An ObservableCollection&lt;T&gt; enhanced with cross threads marshaling.
    /// </summary>
    [Serializable]
    public class BindingCollection<T> : ObservableCollection<T>
    {
        private SynchronizationContext creationSyncContext;
        private Thread creationThread;

        /// <summary>
        /// Initializes a new instance of the<see cref="BindingCollection&lt;T&gt;">BindingCollection</see>.
        /// </summary>
        public BindingCollection() : base()
        {
            InstallSynchronizationContext();
        }

        /// <summary>
        /// Initializes a new instance of the<see cref="BindingCollection&lt;T&gt;">BindingCollection</see>
        /// class that contains elements copied from the specified list.
        /// </summary>
        /// <param name="list">The list from which the elements are copied.</param>
        /// <exception cref="System.ArgumentNullException">The list parameter cannot be null.</exception>
        public BindingCollection(List<T> list) : base(list)
        {
            InstallSynchronizationContext();
        }

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (this.creationThread == Thread.CurrentThread)
            {
                // We are running in the same thread, so just directly fire the collection changed notification.
                this.OnCollectionChangedInternal(e);
            }
            else if (this.creationSyncContext.GetType() == typeof(SynchronizationContext))
            {
                // We are running in free threaded context, also directly fire the collection changed notification.
                this.OnCollectionChangedInternal(e);
            }
            else
            {
                // We are running in WindowsFormsSynchronizationContext or DispatcherSynchronizationContext,
                // so we should marshal the collection changed notification back to the creation thread.
                // Note that we should use the blocking SynchronizationContext.Send operation to marshal the call,
                // because SynchronizationContext.Post will fail under heavy usage scenario.
                this.creationSyncContext.Send(new SendOrPostCallback(delegate
                {
                    this.OnCollectionChangedInternal(e);
                }), null);
            }
        }

        private void InstallSynchronizationContext()
        {
            this.creationSyncContext = SynchronizationContext.Current;
            this.creationThread = Thread.CurrentThread;
        }

        internal void OnCollectionChangedInternal(NotifyCollectionChangedEventArgs e)
        {
            base.OnCollectionChanged(e);
        }
    }
}

Note that the reason I use SynchronizationContext rather than Dispatcher here is that I want this collection to be usable in different presentation technologies such as Windows Forms, WPF, and ASP.NET(You might think I am too of frameworkitis, actually I am). I really hope that the logic I create in the OnCollectionChanged method can make into the future implementation of CollectionViews, so that we don't need to worry about the threading problem illustrated above. I've created a simple WPF application to demonstrate how to use this collection, here is the vs project for this little app.

You can make this test more demanding by pressing the "Add Items" and "Remove Items" buttons multiple times to create more threads for testing.

6 comments:

Anonymous said...

thanks, it is much cleaner.

Anonymous said...

Excellent stuff. Just what I was looking for to optimize my background threads. Thank you.

Anonymous said...

Sweet!
I've been looking for a good solution like this :).
Great job!

Anonymous said...

Thank You, sir.

Anonymous said...

Sorry I don't believe this will work.

Bea marshalls the actual changes to the dispatcher threads so the changes to the collection are serialized.

You are only marshalling the event raising to the dispatcher thread.

What stops the collection from changing while the UI thread is reading it? I believe what will happen is that the UI will create an enumerator which will get invalidated and there will be an exception and various badness...

Bruno Brant said...

I hope I can get your attention... Why is it necessary to dispatch the change notification to the UI thread when dealing with collections? I have a scenario where a collection (actually, a DataTable) gets fully updated before notifying the observers, so I'd see no need to actually dispatch to the UI. Only, if I don't, I get a uncanny behavior.