Click here to Skip to main content
15,861,168 members
Articles / Desktop Programming / WPF
Article

Introducing the Model Thread View Thread Pattern

Rate me:
Please Sign up or sign in to vote.
4.93/5 (69 votes)
1 May 2010BSD14 min read 161.5K   862   172   73
Reduce threading code, and increase UI responsiveness with a new pattern extending MVVM.

title image

Contents

Introduction

Probably the most often discussed topic around WPF and Silverlight in the last year or two has been the MVVM (Model View View Model) pattern. MVVM has surfaced as a candidate replacement for similar architectural patterns such as the MVC and MVP patterns, because it leverages specific features of WPF and Silverlight, most notably the data binding infrastructure, to deepen the separation of the view layer from the rest of an application's layers. This has several benefits, including allowing interactive designers to focus exclusively on the user interface (UI), concurrent development of application layers, and easier testability.

The mechanisms that make MVVM so effective can also be applied to a secondary pattern, one that I have called the Model Thread View Thread pattern (MTVT). It is an approach that may be seen as extending the principles behind MVVM. The MTVT pattern is an architectural pattern used for explicit demarcation of the view and view model via disparate threads of execution. In summary, its main characteristics are that it is comprised of two distinct threads of execution, (one for the view and one for the other application layers), and that cross thread communication occurs transparently over familiar WPF and Silverlight features; such as events (INotifyPropertyChanged, INotifyCollectionChanged etc.) and commands.

In the proof of concept download, I have provided:

  • A weak referencing event management system that maintains thread affinity with subscribers.
  • A command class that invokes handlers on an application's model thread.
  • A synchronized ObservableCollection that allows its collection changed event to be raised on the thread where subscription took place.
  • And a ViewModelBase class that raises property changes on the subscribers thread.

MTVT is specifically focused on providing for a non-presentation layer centric approach. In MTVT, the execution of model logic is separated from UI controls using an asynchronous threading model.

MTVT displaces the traditional UI centric approach in which the UI thread is the primary thread, and where all activity occurs there by default.

Why a new approach?

MTVT is designed to make use of specific capabilities of WPF and Silverlight to better separate the view or presentation layer at run-time, which in turn provides greater assurance of UI responsiveness and, in the case of Silverlight, facilitates such things as synchronous WCF communication.

.NET GUI applications are traditionally UI thread centric. For example, a developer may provide an event handler for an event, and if the handler is deemed to be a long running activity, then the developer will write code to spin off the work to a worker thread. It is, however, common that the developer will neglect to write the asynchronous code, because the execution time of the handler is underestimated, or it is seen as too laborious to write the code at the time. The ramifications of not doing so, usually don't present themselves until later.

If, on the other hand, the developer takes the approach of invoking all state changes on the UI thread, then not only can we end up with lots of ugly boiler plate code, but we can end up degrading an application's performance if we're not careful.

MTVT, in part, addresses this haphazard approach to delegation. It reduces the need for repetitive coding activities around controlling concurrency, and it alleviates the need to explicitly invoke changes in the UI from the model. Also, explicit designation of a single model thread may help to reduce concurrency issues, which are otherwise inevitable when working with multiple child threads.

Another benefit is that model logic becomes more scalable, as the introduction of new functionality won't slow the UI.

These days we see lots of tools emerging that assist in transparently parallelizing work, (such as PLINQ and TPL). Infrastructure for parallelization will become evermore prevalent, and such technologies focus on abstracting the mechanism for retrieving data, using e.g., LINQ, and focusing on the what not the how. This proof of concept attempts to do the same, in that the mechanisms for raising events, performing commands, and updating collections remain the same.

Background

Some time ago, back in 2008, I published an article in which I describe how to perform blocking WCF service calls (synchronous at the user code level) in Silverlight, thereby removing the asynchronous requirement; which can lead to spaghetti code. It describes a reactor implementation to provide a synchronous interface for the Silverlight asynchronous API. Since then I’ve been surprised by the frequent emails and messages I have received by users that don’t really understand the approach necessary to make it all work fluidly. Many contend that even though it is possible, as I demonstrate how to do it, they have a hard time reconciling the notion that the UI thread should not necessarily be the 'driving' thread. And, I can kind of see why.

MVVM has shown that in most cases, adequate view separation is attainable. However, because both WPF and Silverlight rely on thread affinity with the UI thread, issues around threading occur, and can hinder the application of the pattern. This has lead me to devise MTVT. This pattern lends itself quite naturally to both Silverlight and WPF because of the binding, commanding, and event infrastructure. And as we shall see, I demonstrate how the features can be put to work transparently to support the pattern. There's nothing new to do, (in fact there is less to do), and we don't need to radically change the way we work.

Sample Application

Requirements:

The sample is a rudimentary Silverlight application, which demonstrates the commanding, events, and collection that makes the dual thread approach possible. It consists of a single page, with a single interactive component: a button.

Sample application screen shot

Figure: Demonstration application.

The "Fire view model command" button has an attached property, which is the Prism Click.Command property.

XML
<Content="Fire view model command"
    cal:Click.Command="{Binding TestCommand}" />

When the button is clicked, an execution handler located in the MainPageViewModel is invoked on the model thread, as shown in the following excerpt:

C#
void OnTestCommandExecute(object obj)
{
    /* Setting of properties will cause the property change handlers to be invoked 
        * on the thread in which they were subscribed (either the UI or model thread). 
        * This means we do not need to invoke the change on the UI thread 
        * as would otherwise be the case. */
    Message = "Processing command in view model \n (sleeping to show UI doesn't lock)";
 
    /* Sleep to simulate some long running activity. */
    Thread.Sleep(2000);
 
    string newItem = "Test " + ++clickCount;
 
    exampleCollection.Add(newItem);
 
    TestString = newItem;
    Message = string.Empty;
}

The Message property of the view model is modified. This property change occurs on the view model thread, yet the UI is informed of the change via the UISynchronizationContext, which invokes the change handler on the UI thread, as not to raise an invalid cross thread exception.

screen shot after pressing button

Figure: View model is put to sleep, yet the view remains responsive.

Once the model thread is finished sleeping, we add an item to our custom observable collection, which safely invokes its CollectionChanged event handlers on the UI thread; the thread where the Silverlight data binding infrastructure subscribed to the event.

new item added to list

Figure: Item safely added to the collection.

This demonstrates that we are indeed able to separate the UI thread from the model thread.

There are some subtleties to the implementation. For example, you may be wondering how we are able to handle the CanExecute event handler, which requires the method to return a result; thereby requiring the UI thread to be blocked. Well, in Silverlight, blocking the UI thread is not an option because the event loop is serviced by the UI thread. But, we can still achieve the 'enabled/disabled' functionality by using a callback. This can be seen in the following excerpt from the ModelCommand class, which makes use of Prism's WeakEventHandlerManager.

ModelCommand Class

C#
public class ModelCommand<T> : ICommand, IActiveAware
{
    readonly Action<T> executeMethod;
    readonly Func<T, bool> canExecuteMethod;
    List<WeakReference> canExecuteChangedHandlers;
    bool active;
 
    public ModelCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod)
    {
        if (executeMethod == null && canExecuteMethod == null)
        {
            throw new ArgumentNullException("executeMethod");
        }
 
        this.executeMethod = executeMethod;
        this.canExecuteMethod = canExecuteMethod;
    }
 
    /// <summary>
    /// Indicates that a can execute handler has provided a result.
    /// </summary>
    volatile bool canExecuteResultReceived;
    /// <summary>
    /// Indicates whether this command can be executed.
    /// </summary>
    volatile bool canExecuteResult;
 
    public bool CanExecute(T parameter)
    {
        if (canExecuteMethod == null)
        {
            return true;
        }
 
        if (canExecuteResultReceived)
        {
            var result = canExecuteResult;
            canExecuteResultReceived = false;
            return result;
        }
 
        /* We dispatch the call to determine if the command 
            * can execute in a non blocking manner. 
            * This is because we can't block the UI thread without causing a lockup. */
        ModelSynchronizationContext.Instance.InvokeWithoutBlocking(
            delegate
            {
                canExecuteResult = canExecuteMethod(parameter);
                canExecuteResultReceived = true;
                /* Once we get the real result we can signal to the UI 
                    * that the command can or can't execute. If canExecuteResult is not null, 
                    * that result will be returned and canExecuteResult will be set to null. See above. */
                OnCanExecuteChanged();
            });
 
        return false;
    }
 
    public void Execute(T parameter)
    {
        if (executeMethod == null)
        {
            return;
        }
        ModelSynchronizationContext.Instance.InvokeWithoutBlocking(
            () => executeMethod(parameter));
    }
 
 // Shortened for clarity.

}

Here we see that the CanExecute method does not block the caller when invoking the CanExecute event handler on the model thread. But, what it does do is invokes the handler without blocking, and signals when a result has been obtained by calling the OnCanExecuteChanged method. This then triggers the CanExecute method to be called a second time. And for the second call the value of canExecuteResultReceived is used to signal that this time around we have a value, and thus the canExecuteResult is supplied, and the flag reset.

ModelCommand

Figure: The ModelCommand makes use of the ModelSynchronizationContext in order to invoke the Execute and CanExecute event handlers on the model thread.

When a command is executed, as when the button is clicked in the sample application, we see that the event handler is invoked using the ModelSynchronizationContext. This occurs in a non blocking call, and thus prevents the UI from locking up.

Synchronization Infrastructure

So, we've looked at the sample application. Now let's explore the infrastructure in a bit more detail. We shall start with the synchronization infrastructure, and look at how it is used to maintain two separate threads of execution, and in particular the ISynchronizationContext interface, which defines the standard functionality of all synchronization contexts.

ISynchronizationContext Interface

This interface specifies that an Action or SendOrPostCallback may be queued for invocation, in either a blocking or non-blocking manner.

ISynchronizationContext Class Diagram

Figure: ISynchronizationContext class diagram.

There are two implementations of this interface. They are the ModelSynchronizationContext, and the UISynchronizationContext. Both are tasked with invoking delegates on either the model thread, or UI thread respectively.

ModelSynchronizationContext Class

The ModelSynchronizationClass uses a dedicated thread to invoke a queue of actions or CallbackReferences. This presents a familiar producer and consumer problem; easily solved with an AutoResetEvent, which is used to signal when an item has been added to the queue. Thus, we do not need to resort to using a polling mechanism. The following excerpt is taken from the ModelSynchronizationContext class:

C#
public class ModelSynchronizationContext : ISynchronizationContext
{
    volatile bool stopRunning;
    readonly Queue<CallbackReference> callbacks = new Queue<CallbackReference>();
    readonly object callbacksLock = new object();        
    readonly AutoResetEvent consumerThreadResetEvent = new AutoResetEvent(false);

    internal void Stop()
    {
        stopRunning = true;
    }

    class CallbackReference
    {
        public SendOrPostCallback Callback { get; set; }
        public object State { get; set; }
    }

    #region Singleton implementation

    ModelSynchronizationContext()
    {
        /* Consumer thread initialization. */
        threadOfCreation = new Thread(
                delegate(object o)
                {
                    while (!stopRunning)
                    {
                        ConsumeQueue();
                        consumerThreadResetEvent.WaitOne();
                    }
                });
        threadOfCreation.Start();
    }

    public static ModelSynchronizationContext Instance
    {
        get
        {
            return Nested.instance;
        }
    }

    /// <summary>
    /// Inner class for full lazy loading of singleton.
    /// </summary>
    class Nested
    {
        static Nested()
        {
        }

        internal static readonly ModelSynchronizationContext instance = new ModelSynchronizationContext();
    }

    #endregion

    readonly Thread threadOfCreation;

    /// <summary>
    /// Gets the model thread.
    /// </summary>
    /// <value>The thread.</value>
    internal Thread Thread
    {
        get
        {
            return threadOfCreation;
        }
    }

    /// <summary>
    /// Invokes the callback without blocking. Call will return immediately.
    /// </summary>
    /// <param name="callback">The callback to invoke.</param>
    /// <param name="state">The state to pass during the callback invocation.</param>
    public void InvokeWithoutBlocking(SendOrPostCallback callback, object state)
    {
        lock (callbacksLock)
        {
            callbacks.Enqueue(new CallbackReference { Callback = callback, State = state });
        }
        consumerThreadResetEvent.Set();
    }

    /// <summary>
    /// Invokes the specified action without blocking. Call will return immediately.
    /// </summary>
    /// <param name="action">The action to invoke.</param>
    public void InvokeWithoutBlocking(Action action)
    {
        lock (callbacksLock)
        {
            callbacks.Enqueue(new CallbackReference { Callback = o => action() });
        }
        consumerThreadResetEvent.Set();
    }

    /// <summary>
    /// Invokes the specified callback and blocks until completion.
    /// </summary>
    /// <param name="callback">The callback to invoke.</param>
    /// <param name="state">The state to pass during invocation.</param>
    /// <exception cref="InvalidOperationException">
    /// Occurs if call made from the UI thread.</exception>
    public void InvokeAndBlockUntilCompletion(SendOrPostCallback callback, object state)
    {
        RaiseExceptionIfUIThread();

        AutoResetEvent innerResetEvent = new AutoResetEvent(false);
        var callbackState = new CallbackReference { Callback = callback, State = state};
        lock (callbacksLock)
        {
            var processedHandler = new EventHandler<InvokeCompleteEventArgs>(
                delegate(object o, InvokeCompleteEventArgs args)
                {
                    if (args.CallbackReference == callbackState)
                    {
                        innerResetEvent.Set();
                    }
                });

            invokeComplete += processedHandler;
            callbacks.Enqueue(callbackState);
        }

        consumerThreadResetEvent.Set();
        innerResetEvent.WaitOne();
    }

    /// <summary>
    /// Invokes the specified callback and blocks until completion.
    /// </summary>
    /// <param name="action">The action to invoke.</param>
    /// <exception cref="InvalidOperationException">
    /// Occurs if call made from the UI thread.</exception>
    public void InvokeAndBlockUntilCompletion(Action action)
    {
        RaiseExceptionIfUIThread();

        var itemResetEvent = new AutoResetEvent(false);
        var callbackReference = new CallbackReference {Callback = o => action()};

        lock (callbacksLock)
        {
            var processedHandler = new EventHandler<InvokeCompleteEventArgs>(
                delegate(object o, InvokeCompleteEventArgs args)
                {
                    if (args.CallbackReference == callbackReference)
                    {
                        itemResetEvent.Set();
                    }
                });

            invokeComplete += processedHandler;
            callbacks.Enqueue(callbackReference);
        }

        consumerThreadResetEvent.Set();
        itemResetEvent.WaitOne();
    }

    void RaiseExceptionIfUIThread()
    {
        if (!UISynchronizationContext.Instance.InvokeRequired)
        {
            throw new InvalidOperationException(
                "Blocking the UI thread may cause the application to become unresponsive.");
        }
    }

    void ConsumeQueue()
    {
        while (callbacks.Count > 0)
        {
            var callback = callbacks.Dequeue();
            callback.Callback(callback.State);
            OnInvokeComplete(callback);
        }
    }

    event EventHandler<InvokeCompleteEventArgs> invokeComplete;

    /// <summary>
    /// Used to signal that an invocation has occurred.
    /// </summary>
    class InvokeCompleteEventArgs : EventArgs
    {
        public CallbackReference CallbackReference { get; private set; }

        public InvokeCompleteEventArgs(CallbackReference callbackReference)
        {
            CallbackReference = callbackReference;
        }
    }

    void OnInvokeComplete(CallbackReference callbackReference)
    {
        if (invokeComplete != null)
        {
            invokeComplete(this, new InvokeCompleteEventArgs(callbackReference));
        }
    }

    public void Initialize()
    {
        /* Intentionally left blank. Constructor performs initialization. */
    }

    void ISynchronizationContext.Initialize(Dispatcher dispatcher)
    {
        /* Intentionally left blank. Constructor performs initialization. */
    }

    /// <summary>
    /// Gets a value indicating whether the current thread 
    /// is the thread associated with the model thread.
    /// </summary>
    /// <value><c>true</c> if the current thread is the model thread; 
    /// otherwise, <c>false</c>.</value>
    public bool InvokeRequired
    {
        get
        {
            var result = threadOfCreation.ManagedThreadId != Thread.CurrentThread.ManagedThreadId;
            return result;
        }
    }
}

Notice that in either of the InvokeAndBlockUntilCompletion method overloads, to invoke an action, and then block until the action is complete, we use another AutoResetEvent named itemResetEvent. Whenever the queue is being consumed by our model thread, as each action is completed, the invokeComplete event is raised, which allows the anonymous handlers (in the InvokeAndBlockUntilCompletion methods), to test whether the action invoked is the one that is being waited on. If so, this indicates that the action has been invoked, and that the method is free to return.

UISynchronizationContext Class

The other ISynchronizationContext implementation is the UISynchronizationContext. It makes use of a Dispatcher and a DispatcherSynchronizationContext to do the heavy lifting for us. In order to perform blocking calls on the UI thread we use a DispatcherSynchronizationContext. I often see people wondering how to perform blocking calls with the Dispatcher, and here's how it's done: take a Dispatcher and instantiate a DispatcherSynchronizationContext, then call context.Post(callback, state);

C#
/// <summary>
/// Singleton class providing the default implementation 
/// for the <see cref="ISynchronizationContext"/>, specifically for the UI thread.
/// </summary>
public partial class UISynchronizationContext : ISynchronizationContext
{
    DispatcherSynchronizationContext context;
    Dispatcher dispatcher;

    #region Singleton implementation

    static readonly UISynchronizationContext instance = new UISynchronizationContext();

    /// <summary>
    /// Gets the singleton instance.
    /// </summary>
    /// <value>The singleton instance.</value>
    public static ISynchronizationContext Instance
    {
        get
        {
            return instance;
        }
    }

    #endregion

    public void Initialize()
    {
        EnsureInitialized();
    }

    readonly object initializationLock = new object();

    void EnsureInitialized()
    {
        if (dispatcher != null && context != null)
        {
            return;
        }

        lock (initializationLock)
        {
            if (dispatcher != null && context != null)
            {
                return;
            }

            try
            {
#if SILVERLIGHT
                dispatcher = System.Windows.Deployment.Current.Dispatcher;
#else
                dispatcher = Dispatcher.CurrentDispatcher;
#endif
                context = new DispatcherSynchronizationContext(dispatcher);
            }
            catch (InvalidOperationException)
            {
                /* TODO: Make localizable resource. */
                throw new ConcurrencyException("Initialised called from non-UI thread.");
            }
        }
    }

    public void Initialize(Dispatcher dispatcher)
    {
        ArgumentValidator.AssertNotNull(dispatcher, "dispatcher");
        lock (initializationLock)
        {
            this.dispatcher = dispatcher;
            context = new DispatcherSynchronizationContext(dispatcher);
        }
    }

    public void InvokeWithoutBlocking(SendOrPostCallback callback, object state)
    {
        ArgumentValidator.AssertNotNull(callback, "callback");
        EnsureInitialized();

        context.Post(callback, state);
    }

    public void InvokeWithoutBlocking(Action action)
    {
        ArgumentValidator.AssertNotNull(action, "action");
        EnsureInitialized();

        context.Post(state => action(), null);
    }

    public void InvokeAndBlockUntilCompletion(SendOrPostCallback callback, object state)
    {
        ArgumentValidator.AssertNotNull(callback, "callback");
        EnsureInitialized();

        context.Send(callback, state);
    }

    public void InvokeAndBlockUntilCompletion(Action action)
    {
        ArgumentValidator.AssertNotNull(action, "action");
        EnsureInitialized();

        if (dispatcher.CheckAccess())
        {
            action();
        }
        else
        {
            context.Send(delegate { action(); }, null);
        }
    }

    public bool InvokeRequired
    {
        get
        {
            EnsureInitialized();
            return !dispatcher.CheckAccess();
        }
    }
}

Events with Thread Affinity

We have seen how there are two contexts (UISynchronizationContext and ModelSynchronizationContext) that are used to invoke delegates on the UI and model threads. Let us now turn our attention to the infrastructure which is used to give us events with thread affinity.

ViewModelBase Class Diagram

Figure: ViewModelBase class signals property changes via the PropertyChangeNotifier

DelegateManager Class

In order to invoke a delegate on the same thread that the event was subscribed to, we make use of a DelegateManager. The DelegateManager class allows us to invoke a list of delegates, where each delegate can be associated with a particular thread. The DelegateManager also makes use of WeakReferences, which helps to ensure that memory leaks do not occur. The DelegateManager class is provided here in its entirety:

C#
public class DelegateManager
{
    readonly bool preserveThreadAffinity;
    readonly DelegateInvocationMode invocationMode;
    readonly IProvider<ISynchronizationContext> contextProvider;
    readonly bool useWeakReferences = true;
    readonly List<DelegateReference> delegateReferences = new List<DelegateReference>();
    readonly object membersLock = new object();
    readonly Dictionary<DelegateReference, ISynchronizationContext> synchronizationContexts
        = new Dictionary<DelegateReference, ISynchronizationContext>();

    /// <summary>Initializes a new instance 
    /// of the <see cref="DelegateManager"/> class.</summary>
    /// <param name="preserveThreadAffinity">If set to <c>true</c>, 
    /// delegate invocation  will occur using the <see cref="ISynchronizationContext"/> 
    /// provided by the specified context provider.</param>
    /// <param name="useWeakReferences">If <c>true</c> weak references will be used.</param>
    /// <param name="invocationMode">The invocation mode. 
    /// If <c>Blocking</c> delegates will be invoked 
    /// in serial, other in parallel.</param>
    /// <param name="contextProvider">The context provider, 
    /// which is used to supply a context when a delegate is added.
    /// If preservedThreadAffinity is <c>false</c>, this value will be ignored.</param>
    public DelegateManager(bool preserveThreadAffinity = false,
        bool useWeakReferences = false, 
        DelegateInvocationMode invocationMode = DelegateInvocationMode.Blocking, 
        IProvider<ISynchronizationContext> contextProvider = null)
    {
        this.preserveThreadAffinity = preserveThreadAffinity;
        this.invocationMode = invocationMode;
        this.contextProvider = contextProvider;
        this.useWeakReferences = useWeakReferences;

        if (contextProvider == null)
        {
            this.contextProvider = new SynchronizationContextProvider();
        }
    }

    /// <summary>
    /// Adds the specified target delegate to the list of delegates 
    /// that are invoked when <see cref="InvokeDelegates"/> is called.
    /// </summary>
    /// <param name="targetDelegate">The target delegate.</param>
    public void Add(Delegate targetDelegate)
    {
        ArgumentValidator.AssertNotNull(targetDelegate, "targetDelegate");

        var reference = new DelegateReference(targetDelegate, useWeakReferences);

        lock (membersLock)
        {
            delegateReferences.Add(reference);
            if (preserveThreadAffinity)
            {
                synchronizationContexts[reference] = contextProvider.ProvidedItem;
            }
        }
    }

    /// <summary>
    /// Removes the specified target delegate from the list of delegates.
    /// </summary>
    /// <param name="targetDelegate">The target delegate.</param>
    public void Remove(Delegate targetDelegate)
    {
        lock (membersLock)
        {
            var removedItems = delegateReferences.RemoveAll(
                reference =>
                {
                    Delegate target = reference.Delegate;
                    return target == null || targetDelegate.Equals(target);
                });

            if (preserveThreadAffinity)
            {
                foreach (var delegateReference in removedItems)
                {
                    synchronizationContexts.Remove(delegateReference);
                }
            }
        }
    }

    /// <summary>
    /// Invokes each delegate.
    /// </summary>
    /// <param name="args">The args included during delegate invocation.</param>
    /// <exception cref="Exception">
    /// Rethrown exception if a delegate invocation raises an exception.
    /// </exception>
    public void InvokeDelegates(params object[] args)
    {
        IEnumerable<DelegateReference> delegates;
        /* Retrieve the valid delegates by first trim 
            * the collection of null delegates. */
        lock (membersLock)
        {
            var removedItems = delegateReferences.RemoveAll(
                listener => listener.Delegate == null);

            if (preserveThreadAffinity)
            {
                /* Clean the synchronizationContexts of those removed 
                    * in the preceding step. */
                foreach (var delegateReference in removedItems)
                {
                    synchronizationContexts.Remove(delegateReference);
                }
            }
            /* The lock prevents changes to the collection, 
                * therefore we can safely compile our list. */
            delegates = (from reference in delegateReferences
                        select reference).ToList();
        }

        /* At this point any changes to the delegateReferences collection 
            * won't be noticed. */

        foreach (var reference in delegates)
        {
            if (!preserveThreadAffinity)
            {
                reference.Delegate.DynamicInvoke(args);
                continue;
            }

            var context = synchronizationContexts[reference];
            DelegateReference referenceInsideCloser = reference;
            Exception exception = null;

            var callback = new SendOrPostCallback(
                delegate
                    {
                        try
                        {
                            referenceInsideCloser.Delegate.DynamicInvoke(args);
                        }
                        catch (Exception ex)
                        {
                            exception = ex;
                        }
                    });

            switch (invocationMode)
            {
                case DelegateInvocationMode.Blocking:
                    context.InvokeAndBlockUntilCompletion(callback, null);
                    break;
                case DelegateInvocationMode.NonBlocking:
                    context.InvokeWithoutBlocking(callback, null);
                    break;
                default:
                    throw new ArgumentOutOfRangeException("Unknown DispatchMode: " 
                        + invocationMode.ToString("G"));
            }

            /* Rethrowing the exception may be missed 
                * in a DispatchMode.Post scenario. */
            if (exception != null) 
            {
                throw exception;
            }
        }
    }
}

SynchronizationContextProvider Class

So how do we invoke a delegate on a particular thread? Well, this can't be done arbitrarily. For that we make use of an extensibility point which I use throughout the proof of concept. It is an IProvider<ISynchronizationContext> whose default implementation is the SynchronizationContextProvider. It determines the ISynchronizationContext that is used to associate with a delegate. Notice above, how it is passed to the DelegateManager's constructor.

C#
/// <summary>
/// The default implementation for an <see cref="IProvider{T}"/> 
/// providing an <see cref="ISynchronizationContext"/> instance.
/// </summary>
public class SynchronizationContextProvider : IProvider<ISynchronizationContext>
{
    public ISynchronizationContext ProvidedItem
    {
        get
        {
            if (Deployment.Current.Dispatcher.CheckAccess())
            {
                return UISynchronizationContext.Instance;
            }
            return ModelSynchronizationContext.Instance;
        }
    }
}

PropertyChangeNotifier Class

The PropertyChangeNotifier makes use of two DelegateManagers. One for the INotifyPropertyChanged event, and the other for the INotifyPropertyChanging event. This class uses a WeakReference to associate itself with a host class, in order to take over the responsibilities of property change notification. It also adds some niceties like cancellable changes. My favourite method from this class is the Assign method. I use it everywhere for property changes, because it

  • takes care of notifying that the property is about to be changed,
  • performs the changes (unless it was cancelled),
  • and then notifies that the change has been performed.

Several examples of its use can be found in the MainPageViewModel class. The following excerpt shows one such example:

C#
string message;

public string Message
{
    get
    {
        return message;
    }
    set
    {
        Assign(Meta.Message, ref message, value);
    }
}

The Assign method takes the name of the property, in this case it is a generated name from the T4 Metadata Generation template, a reference to the field that may be changed, and the new value.

Returning to the actual implementation of the PropertyChangeNotifier, here it is provided in full:

C#
/// <summary>
/// This class provides an implementation of the <see cref="INotifyPropertyChanged"/>
/// and <see cref="INotifyPropertyChanging"/> interfaces. 
/// Extended <see cref="PropertyChangedEventArgs"/> and <see cref="PropertyChangingEventArgs"/>
/// are used to provides the old value and new value for the property. 
/// <seealso cref="PropertyChangedEventArgs{TProperty}"/>
/// <seealso cref="PropertyChangingEventArgs{TProperty}"/>
/// </summary>
[Serializable]
public sealed class PropertyChangeNotifier : INotifyPropertyChanged, INotifyPropertyChanging
{
    readonly WeakReference ownerWeakReference;
    readonly DelegateManager changedEventManager;
    readonly DelegateManager changingEventManager;

    /// <summary>
    /// Gets the owner for testing purposes.
    /// </summary>
    /// <value>The owner.</value>
    internal object Owner
    {
        get
        {
            if (ownerWeakReference.Target != null)
            {
                return ownerWeakReference.Target;
            }
            return null;
        }
    }

    /// <summary>
    /// Initializes a new instance 
    /// of the <see cref="PropertyChangeNotifier"/> class.
    /// </summary>
    /// <param name="owner">The intended sender 
    /// of the <code>PropertyChanged</code> event.</param>
    public PropertyChangeNotifier(object owner)
        : this(owner, true)
    {
        /* Intentionally left blank. */
    }

    /// <summary>
    /// Initializes a new instance 
    /// of the <see cref="PropertyChangeNotifier"/> class.
    /// </summary>
    /// <param name="owner">The intended sender 
    /// <param name="useExtendedEventArgs">If <c>true</c> the
    /// generic <see cref="PropertyChangedEventArgs{TProperty}"/>
    /// and <see cref="PropertyChangingEventArgs{TProperty}"/> 
    /// are used when raising events. 
    /// Otherwise, the non-generic types are used, and they are cached 
    /// to decrease heap fragmentation.</param>
    /// of the <code>PropertyChanged</code> event.</param>
    public PropertyChangeNotifier(object owner, bool useExtendedEventArgs) 
        : this(owner, useExtendedEventArgs, true)
    {
        /* Intentionally left blank. */
    }

    /// <summary>
    /// Initializes a new instance 
    /// of the <see cref="PropertyChangeNotifier"/> class.
    /// </summary>
    /// <param name="owner">The intended sender 
    /// <param name="useExtendedEventArgs">If <c>true</c> the
    /// generic <see cref="PropertyChangedEventArgs{TProperty}"/>
    /// and <see cref="PropertyChangingEventArgs{TProperty}"/> 
    /// are used when raising events. 
    /// Otherwise, the non-generic types are used, and they are cached 
    /// to decrease heap fragmentation.</param>
    /// of the <code>PropertyChanged</code> event.</param>
    /// <param name="useExtendedEventArgs">If <c>true</c> the
    /// generic <see cref="PropertyChangedEventArgs{TProperty}"/>
    /// and <see cref="PropertyChangingEventArgs{TProperty}"/> 
    /// are used when raising events. Otherwise, the non-generic types 
    /// are used, and they are cached 
    /// to decrease heap fragmentation.</param>
    /// <param name="preserveThreadAffinity">Indicates whether to invoke handlers 
    /// on the thread that the subscription took place.</param>
    public PropertyChangeNotifier(object owner, bool useExtendedEventArgs, bool preserveThreadAffinity)
    {
        ArgumentValidator.AssertNotNull(owner, "owner");

        ownerWeakReference = new WeakReference(owner);
        this.useExtendedEventArgs = useExtendedEventArgs;
        changedEventManager = new DelegateManager(preserveThreadAffinity);
        changingEventManager = new DelegateManager(preserveThreadAffinity);
    }

    #region event PropertyChanged

    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged
    {
        add
        {
            if (OwnerDisposed)
            {
                return;
            }
            changedEventManager.Add(value);
        }
        remove
        {
            if (OwnerDisposed)
            {
                return;
            }
            changedEventManager.Remove(value);
        }
    }

    #region Experimental Thread Affinity
    public bool MaintainThreadAffinity { get; set; }
    #endregion

    /// <summary>
    /// Raises the <see cref="E:PropertyChanged"/> event.
    /// If the owner has been GC'd then the event will not be raised.
    /// </summary>
    /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> 
    /// instance containing the event data.</param>
    void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        changedEventManager.InvokeDelegates(Owner, e);
    }

    #endregion

    /// <summary>
    /// Assigns the specified newValue to the specified property
    /// and then notifies listeners that the property has changed.
    /// </summary>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <param name="propertyName">Name of the property. Can not be null.</param>
    /// <param name="property">A reference to the property that is to be assigned.</param>
    /// <param name="newValue">The value to assign the property.</param>
    /// <exception cref="ArgumentNullException">
    /// Occurs if the specified propertyName is <code>null</code>.</exception>
    /// <exception cref="ArgumentException">
    /// Occurs if the specified propertyName is an empty string.</exception>
    public PropertyAssignmentResult Assign<TProperty>(
        string propertyName, ref TProperty property, TProperty newValue)
    {
        if (OwnerDisposed)
        {
            return PropertyAssignmentResult.OwnerDisposed;
        }

        ArgumentValidator.AssertNotNullOrEmpty(propertyName, "propertyName");
        ValidatePropertyName(propertyName);

        return AssignWithNotification(propertyName, ref property, newValue);
    }

    /// <summary>
    /// Slow. Not recommended.
    /// Assigns the specified newValue to the specified property
    /// and then notifies listeners that the property has changed.
    /// Assignment nor notification will occur if the specified
    /// property and newValue are equal. 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <param name="expression">The expression that is used to derive the property name.
    /// Should not be <code>null</code>.</param>
    /// <param name="property">A reference to the property that is to be assigned.</param>
    /// <param name="newValue">The value to assign the property.</param>
    /// <exception cref="ArgumentNullException">
    /// Occurs if the specified propertyName is <code>null</code>.</exception>
    /// <exception cref="ArgumentException">
    /// Occurs if the specified propertyName is an empty string.</exception>
    public PropertyAssignmentResult Assign<T, TProperty>(
        Expression<Func<T, TProperty>> expression, ref TProperty property, TProperty newValue)
    {
        if (OwnerDisposed)
        {
            return PropertyAssignmentResult.OwnerDisposed;
        }

        string propertyName = GetPropertyName(expression);
        return AssignWithNotification(propertyName, ref property, newValue);
    }

    PropertyAssignmentResult AssignWithNotification<TProperty>(
        string propertyName, ref TProperty property, TProperty newValue)
    {
        /* Boxing may occur here. We should consider 
            * providing some overloads for primitives. */
        if (Equals(property, newValue))
        {
            return PropertyAssignmentResult.AlreadyAssigned;
        }

        if (useExtendedEventArgs)
        {
            var args = new PropertyChangingEventArgs<TProperty>(propertyName, property, newValue);

            OnPropertyChanging(args);
            if (args.Cancelled)
            {
                return PropertyAssignmentResult.Cancelled;
            }

            var oldValue = property;
            property = newValue;
            OnPropertyChanged(new PropertyChangedEventArgs<TProperty>(
                propertyName, oldValue, newValue));
        }
        else
        {
            var args = RetrieveOrCreatePropertyChangingEventArgs(propertyName);
            OnPropertyChanging(args);

            var changedArgs = RetrieveOrCreatePropertyChangedEventArgs(propertyName);
            OnPropertyChanged(changedArgs);
        }

        return PropertyAssignmentResult.Success;
    }

    readonly Dictionary<string, string> expressions = new Dictionary<string, string>();

    /// <summary>
    /// Notifies listeners that the specified property has changed.
    /// </summary>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <param name="propertyName">Name of the property. Can not be null.</param>
    /// <param name="oldValue">The old value before the change occured.</param>
    /// <param name="newValue">The new value after the change occured.</param>
    /// <exception cref="ArgumentNullException">
    /// Occurs if the specified propertyName is <code>null</code>.</exception>
    /// <exception cref="ArgumentException">
    /// Occurs if the specified propertyName is an empty string.</exception>
    public void NotifyChanged<TProperty>(
        string propertyName, TProperty oldValue, TProperty newValue)
    {
        if (OwnerDisposed)
        {
            return;
        }
        ArgumentValidator.AssertNotNullOrEmpty(propertyName, "propertyName");
        ValidatePropertyName(propertyName);

        if (ReferenceEquals(oldValue, newValue))
        {
            return;
        }

        var args = useExtendedEventArgs
            ? new PropertyChangedEventArgs<TProperty>(propertyName, oldValue, newValue)
            : RetrieveOrCreatePropertyChangedEventArgs(propertyName);

        OnPropertyChanged(args);
    }

    /// <summary>
    /// Slow. Not recommended.
    /// Notifies listeners that the property has changed.
    /// Notification will occur if the specified
    /// property and newValue are equal. 
    /// </summary>
    /// <param name="expression">The expression that is used to derive the property name.
    /// Should not be <code>null</code>.</param>
    /// <param name="oldValue">The old value of the property before it was changed.</param>
    /// <param name="newValue">The new value of the property after it was changed.</param>
    /// <exception cref="ArgumentNullException">
    /// Occurs if the specified propertyName is <code>null</code>.</exception>
    /// <exception cref="ArgumentException">
    /// Occurs if the specified propertyName is an empty string.</exception>
    public void NotifyChanged<T, TResult>(
        Expression<Func<T, TResult>> expression, TResult oldValue, TResult newValue)
    {
        if (OwnerDisposed)
        {
            return;
        }

        ArgumentValidator.AssertNotNull(expression, "expression");

        string name = GetPropertyName(expression);
        NotifyChanged(name, oldValue, newValue);
    }

    static MemberInfo GetMemberInfo<T, TResult>(Expression<Func<T, TResult>> expression)
    {
        var member = expression.Body as MemberExpression;
        if (member != null)
        {
            return member.Member;
        }

        /* TODO: Make localizable resource. */
        throw new ArgumentException("MemberExpression expected.", "expression");
    }

    #region INotifyPropertyChanging Implementation

    public event PropertyChangingEventHandler PropertyChanging
    {
        add
        {
            if (OwnerDisposed)
            {
                return;
            }
            changingEventManager.Add(value);
        }
        remove
        {
            if (OwnerDisposed)
            {
                return;
            }
            changingEventManager.Remove(value);
        }
    }

    /// <summary>
    /// Raises the <see cref="E:PropertyChanging"/> event.
    /// If the owner has been disposed then the event will not be raised.
    /// </summary>
    /// <param name="e">The <see cref="PropertyChangingEventArgs"/> 
    /// instance containing the event data.</param>
    void OnPropertyChanging(PropertyChangingEventArgs e)
    {
        changingEventManager.InvokeDelegates(Owner, e);
    }
    #endregion

#if SILVERLIGHT
    readonly object expressionsLock = new object();

    string GetPropertyName<T, TResult>(Expression<Func<T, TResult>> expression)
    {
        string name;
        lock (expressionsLock)
        {
            if (!expressions.TryGetValue(expression.ToString(), out name))
            {
                if (!expressions.TryGetValue(expression.ToString(), out name))
                {
                    var memberInfo = GetMemberInfo(expression);
                    if (memberInfo == null)
                    {
                        /* TODO: Make localizable resource. */
                        throw new InvalidOperationException("MemberInfo not found.");
                    }
                    name = memberInfo.Name;
                    expressions.Add(expression.ToString(), name);
                }
            }
        }

        return name;
    }
#else
    readonly ReaderWriterLockSlim expressionsLock = new ReaderWriterLockSlim();

    string GetPropertyName<T, TResult>(Expression<Func<T, TResult>> expression)
    {
        string name;
        expressionsLock.EnterUpgradeableReadLock();
        try
        {
            if (!expressions.TryGetValue(expression.ToString(), out name))
            {
                expressionsLock.EnterWriteLock();
                try
                {
                    if (!expressions.TryGetValue(expression.ToString(), out name))
                    {
                        var memberInfo = GetMemberInfo(expression);
                        if (memberInfo == null)
                        {
                            /* TODO: Make localizable resource. */
                            throw new InvalidOperationException("MemberInfo not found.");
                        }
                        name = memberInfo.Name;
                        expressions.Add(expression.ToString(), name);
                    }
                }
                finally
                {
                    expressionsLock.ExitWriteLock();
                }
            }
        }
        finally
        {
            expressionsLock.ExitUpgradeableReadLock();
        }
        return name;
    }
#endif

    bool cleanupOccured;

    bool OwnerDisposed
    {
        get
        {
            /* Improve performance here 
                * by avoiding multiple Owner property calls 
                * after the Owner has been disposed. */
            if (cleanupOccured)
            {
                return true;
            }

            var owner = Owner;
            if (owner != null)
            {
                return false;
            }
            cleanupOccured = true;

            return true;
        }
    }

    [Conditional("DEBUG")]
    void ValidatePropertyName(string propertyName)
    {
#if !SILVERLIGHT
        var propertyDescriptor = TypeDescriptor.GetProperties(Owner)[propertyName];
        if (propertyDescriptor == null)
        {
            /* TODO: Make localizable resource. */
            throw new Exception(string.Format(
                "The property '{0}' does not exist.", propertyName));
        }
#endif
    }

    readonly bool useExtendedEventArgs;
    readonly Dictionary<string, PropertyChangedEventArgs> propertyChangedEventArgsCache 
        = new Dictionary<string, PropertyChangedEventArgs>();
    readonly Dictionary<string, PropertyChangingEventArgs> propertyChangingEventArgsCache 
        = new Dictionary<string, PropertyChangingEventArgs>();

#if SILVERLIGHT
    readonly object propertyChangingEventArgsCacheLock = new object();

    PropertyChangingEventArgs RetrieveOrCreatePropertyChangingEventArgs(string propertyName)
    {
        var result = RetrieveOrCreateEventArgs(
            propertyName,
            propertyChangingEventArgsCacheLock,
            propertyChangingEventArgsCache,
            x => new PropertyChangingEventArgs(x));

        return result;
    }

    readonly object propertyChangedEventArgsCacheLock = new object();

    PropertyChangedEventArgs RetrieveOrCreatePropertyChangedEventArgs(string propertyName)
    {
        var result = RetrieveOrCreateEventArgs(
            propertyName,
            propertyChangedEventArgsCacheLock,
            propertyChangedEventArgsCache,
            x => new PropertyChangedEventArgs(x));

        return result;
    }

    static TArgs RetrieveOrCreateEventArgs<TArgs>(
        string propertyName, object cacheLock, Dictionary<string, TArgs> argsCache,
        Func<string, TArgs> createFunc)
    {
        ArgumentValidator.AssertNotNull(propertyName, "propertyName");
        TArgs result;

        lock (cacheLock)
        {
            if (argsCache.TryGetValue(propertyName, out result))
            {
                return result;
            }

            result = createFunc(propertyName);
            argsCache[propertyName] = result;
        }
        return result;
    }
#else
    readonly ReaderWriterLockSlim propertyChangedEventArgsCacheLock = new ReaderWriterLockSlim();
        
    PropertyChangedEventArgs RetrieveOrCreatePropertyChangedEventArgs(string propertyName)
    {
        ArgumentValidator.AssertNotNull(propertyName, "propertyName");
        var result = RetrieveOrCreateArgs(
            propertyName,
            propertyChangedEventArgsCache,
            propertyChangedEventArgsCacheLock,
            x => new PropertyChangedEventArgs(x));

        return result;
    }

    readonly ReaderWriterLockSlim propertyChangingEventArgsCacheLock = new ReaderWriterLockSlim();

    static TArgs RetrieveOrCreateArgs<TArgs>(string propertyName, Dictionary<string, TArgs> argsCache,
        ReaderWriterLockSlim lockSlim, Func<string, TArgs> createFunc)
    {
        ArgumentValidator.AssertNotNull(propertyName, "propertyName");
        TArgs result;
        lockSlim.EnterUpgradeableReadLock();
        try
        {
            if (argsCache.TryGetValue(propertyName, out result))
            {
                return result;
            }
            lockSlim.EnterWriteLock();
            try
            {
                if (argsCache.TryGetValue(propertyName, out result))
                {
                    return result;
                }
                result = createFunc(propertyName);
                argsCache[propertyName] = result;
                return result;
            }
            finally
            {
                lockSlim.ExitWriteLock();
            }
        }
        finally
        {
            lockSlim.ExitUpgradeableReadLock();
        }
    }

    PropertyChangingEventArgs RetrieveOrCreatePropertyChangingEventArgs(string propertyName)
    {
        ArgumentValidator.AssertNotNull(propertyName, "propertyName");
        var result = RetrieveOrCreateArgs(
            propertyName,
            propertyChangingEventArgsCache,
            propertyChangingEventArgsCacheLock,
            x => new PropertyChangingEventArgs(x));

        return result;
    }
#endif

}

As you will notice, there is also the facility to use lambda expressions for property names, which I wouldn't recommend for poor performance reasons.

So we see that the PropertyChangeNotifier makes use of DelegateManagers to aggregate delegates, and invoke them when the PropertyChanged or PropertyChanging events are raised. In order to avoid duplication, I sometimes make use of the NotifyPropertyChangeBase class.

NotifyPropertyChangeBase Class

The NotifyPropertyChangeBase class encapsulates a PropertyChangeNotifier instance, that goes some way to enabling serialization of the PropertyChangeNotifier, and lazy loading. It also provides for a little terser code, in that field qualification can be omitted.

C#
/// <summary>
/// A base class for property change notification.
/// <seealso cref="PropertyChangeNotifier"/>.
/// </summary>
[Serializable]
public abstract class NotifyPropertyChangeBase : INotifyPropertyChanged, INotifyPropertyChanging
{
    [field: NonSerialized]
    PropertyChangeNotifier notifier;

    [field: NonSerialized]
    object notifierLock;

    /// <summary>
    /// Gets the PropertyChangeNotifier. It is lazy loaded.
    /// </summary>
    /// <value>The PropertyChangeNotifier.</value>
    protected PropertyChangeNotifier Notifier
    {
        get
        {
            /* It is cheaper to create an object to lock, than to instantiate 
                * the PropertyChangeNotifier, because hooking up the events 
                * for many instances is expensive. */
            if (notifier == null)
            {
                lock (notifierLock)
                {
                    if (notifier == null)
                    {
                        notifier = new PropertyChangeNotifier(this);
                    }
                }
            }
            return notifier;
        }
    }

    [OnDeserializing]
    internal void OnDeserializing(StreamingContext context)
    {
        Initialize();
    }

    /// <summary>
    /// Assigns the specified newValue to the specified property
    /// and then notifies listeners that the property has changed.
    /// </summary>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <param name="propertyName">Name of the property. Can not be null.</param>
    /// <param name="property">A reference to the property that is to be assigned.</param>
    /// <param name="newValue">The value to assign the property.</param>
    /// <exception cref="ArgumentNullException">
    /// Occurs if the specified propertyName is <code>null</code>.</exception>
    /// <exception cref="ArgumentException">
    /// Occurs if the specified propertyName is an empty string.</exception>
    protected PropertyAssignmentResult Assign<TProperty>(
        string propertyName, ref TProperty property, TProperty newValue)
    {
        return Notifier.Assign(propertyName, ref property, newValue);
    }

    /// <summary>
    /// When deserialization occurs fields are not instantiated,
    /// therefore we must instantiate the notifier.
    /// </summary>
    void Initialize()
    {
        notifierLock = new object();
    }

    public NotifyPropertyChangeBase()
    {
        Initialize();
    }

    #region Property change notification

    /// <summary>
    /// Occurs when a property value changes.
    /// <seealso cref="PropertyChangeNotifier"/>
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged
    {
        add
        {
            Notifier.PropertyChanged += value;
        }
        remove
        {
            Notifier.PropertyChanged -= value;
        }
    }

    /// <summary>
    /// Occurs when a property value is changing.
    /// <seealso cref="PropertyChangeNotifier"/>
    /// </summary>
    public event PropertyChangingEventHandler PropertyChanging
    {
        add
        {
            Notifier.PropertyChanging += value;
        }
        remove
        {
            Notifier.PropertyChanging -= value;
        }
    }

    #endregion
}

This class serves as the base class for our ViewModelBase class.

ViewModelBase Class

This class is an abstract class that is the base class for all, well, view models. In the proof of concept it has virtually no implementation, and serves merely as a placeholder for now.

C#
public abstract class ViewModelBase : NotifyPropertyChangeBase
{
    protected ViewModelBase()
    {
        Notifier.MaintainThreadAffinity = true;
    }
}

By specifying that our PropertyChangeNotifier maintains thread affinity, it means that event handlers will be executed on the thread of subscription (either the UI thread or the model thread).

A Collection with Event Thread Affinity

ObservableCollections are frequently used in WPF and Silverlight applications to enable automatic UI updates when items are added to, or removed from, a collection. Earlier in this article, we looked at the sample application's use of a custom collection in the MainPageViewModel. This collection was a SynchronizedObservableCollection, which happens to make use of our DelegateManager class in order to associate the thread on which an event is subscribed, and the handler. This means that when an item is added or removed from the collection, each NotifyCollectionChangedEventHandler subscriber is notified on the correct thread. This is important, because UI elements that are data bound to the collection will raise an exception if the handler is not invoked on the UI thread. If we did not have this mechanism then we would need to manually invoke any updates to the collection on the UI thread.

SynchronizedObservableCollection Class

The SynchronizedObservableCollection looks much like the ObservableCollection implementation in the FCL, but with some notable differences. It makes use of the DelegateManager, which allows for the INotifyCollectionChanged event handlers to be invoked on the correct threads. In order to help prevent race conditions, changes to the collection are invoked on the UI thread using the UISynchronizationContext.

C#
/// <summary>
/// Provides <see cref="INotifyCollectionChanged"/> events on the subscription thread 
/// using an <see cref="ISynchronizationContext"/>.
/// </summary>
/// <typeparam name="T">The type of items in the collection.</typeparam>
public class SynchronizedObservableCollection<T> : Collection<T>, 
    INotifyCollectionChanged, INotifyPropertyChanged
{
    bool busy;
    readonly DelegateManager collectionChangedManager;
    readonly ISynchronizationContext uiContext;

    /// <summary>
    /// Occurs when the items list of the collection has changed, 
    /// or the collection is reset.
    /// </summary>
    public event NotifyCollectionChangedEventHandler CollectionChanged
    {
        add
        {
            collectionChangedManager.Add(value);
        }
        remove
        {
            collectionChangedManager.Remove(value);
        }
    }

    PropertyChangedEventHandler propertyChanged;

    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
    {
        add
        {
            propertyChanged += value;
        }
        remove
        {
            propertyChanged -= value;
        }
    }

    /// <summary>
    /// Initializes a new instance 
    /// of the <see cref="SynchronizedObservableCollection<T>"/> class.
    /// </summary>
    /// <param name="contextProvider">The synchronization context provider, 
    /// which is used to determine on what thread a handler is invoked.</param>
    public SynchronizedObservableCollection(
        IProvider<ISynchronizationContext> contextProvider = null)
    {
        uiContext = UISynchronizationContext.Instance;
        collectionChangedManager = new DelegateManager(true, contextProvider: contextProvider);
    }

    /// <summary>
    /// Initializes a new instance 
    /// of the <see cref="SynchronizedObservableCollection<T>"/> class.
    /// </summary>
    /// <param name="collection">The collection to copy.</param>
    /// <param name="contextProvider">The synchronization context provider, 
    /// which is used to determine on what thread a handler is invoked.</param>
    public SynchronizedObservableCollection(IEnumerable<T> collection, 
        IProvider<ISynchronizationContext> contextProvider = null) : this(contextProvider)
    {
        ArgumentValidator.AssertNotNull(collection, "collection");
        CopyFrom(collection);
    }

    public SynchronizedObservableCollection(List<T> list, 
        IProvider<ISynchronizationContext> contextProvider = null) 
        : base(list != null ? new List<T>(list.Count) : list)
    {
        uiContext = UISynchronizationContext.Instance;
        collectionChangedManager = new DelegateManager(true, contextProvider: contextProvider);
        CopyFrom(list);
    }

    void PreventReentrancy()
    {
        if (busy)
        {
            throw new InvalidOperationException(
                "Cannot Change SynchronizedObservableCollection");
        }
    }

    protected override void ClearItems()
    {
        uiContext.InvokeAndBlockUntilCompletion(
            delegate
            {
                PreventReentrancy();
                base.ClearItems();
                OnPropertyChanged("Count");
                OnPropertyChanged("Item[]");
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(
                    NotifyCollectionChangedAction.Reset));                                          
            });
    }

    void CopyFrom(IEnumerable<T> collection)
    {
        uiContext.InvokeAndBlockUntilCompletion(
            delegate
            {
                IList<T> items = Items;
                if (collection != null && items != null)
                {
                    using (IEnumerator<T> enumerator = collection.GetEnumerator())
                    {
                        while (enumerator.MoveNext())
                        {
                            items.Add(enumerator.Current);
                        }
                    }
                }
            });
    }

    protected override void InsertItem(int index, T item)
    {
        uiContext.InvokeAndBlockUntilCompletion(
            delegate
            {
                base.InsertItem(index, item);
                OnPropertyChanged("Count");
                OnPropertyChanged("Item[]");
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(
                    NotifyCollectionChangedAction.Add, item, index));
            });
    }

    protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        busy = true;
        try
        {
            collectionChangedManager.InvokeDelegates(null, e);
        }
        finally
        {
            busy = false;
        }
    }

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (propertyChanged != null)
        {
            busy = true;
            try
            {
                propertyChanged(this, e);
            }
            finally
            {
                busy = false;
            }
        }
    }

    void OnPropertyChanged(string propertyName)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }
        
    protected override void RemoveItem(int index)
    {
        uiContext.InvokeAndBlockUntilCompletion(
            delegate
            {
                PreventReentrancy();
                T changedItem = base[index];
                base.RemoveItem(index);

                OnPropertyChanged("Count");
                OnPropertyChanged("Item[]");
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(
                    NotifyCollectionChangedAction.Remove, changedItem, index));
            });
    }

    protected override void SetItem(int index, T item)
    {
        uiContext.InvokeAndBlockUntilCompletion(
            delegate
            {
                PreventReentrancy();
                T oldItem = base[index];
                base.SetItem(index, item);

                OnPropertyChanged("Item[]");
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(
                    NotifyCollectionChangedAction.Replace, item, oldItem, index));
            });
    }
}

Infrastructure Unit Tests

Curiously, the infrastructure that gives us the ability to unit test Silverlight applications does not come with the Silverlight Tools (SDK), but with the Silverlight Toolkit.

I created some tests during development of the proof of concept. This proved to be very useful during its development, as it allowed me to debug and identify threads, without relying on the UI.

test runner

Figure: Silverlight Unit Testing

The unit testing infrastructure within Visual Studio is not Silverlight Unit Test aware. This means that executing a unit test requires setting the default page of your application, to the generated unit test page. In the case of the sample application, this is the Tests.aspx page.

tests

Figure: Tests completed.

The following listing shows the content of the threading tests:

C#
[TestClass]
public class ViewModelBaseTests : SilverlightTest
{
    [TestMethod]
    public void ViewModelShouldRaisePropertyChangedOnSameThread()
    {
        var autoResetEvent = new AutoResetEvent(false);
        bool raised = false;

        var mockViewModel = new MockViewModel();
        mockViewModel.PropertyChanged += 
            delegate
            {
                raised = true;
                autoResetEvent.Set();
            };

        mockViewModel.StringMember = "Test";
        autoResetEvent.WaitOne();
        Assert.IsTrue(raised);
    }

    [TestMethod]
    public void ViewModelShouldRaisePropertyChangedOnSubscriptionThread()
    {
        var mockViewModel = new MockViewModel();
        var autoResetEvent = new AutoResetEvent(false);
        bool raised = false;
        Thread subscriberThread = null;
        Thread raisedOnThread = null;

        /* We can't sleep, and signal on the same thread, 
            * hence the profuse use of the ThreadPool. */
        ThreadPool.QueueUserWorkItem(
            delegate 
            {
                AutoResetEvent innerResetEvent = new AutoResetEvent(false);
                ModelSynchronizationContext.Instance.InvokeWithoutBlocking(
                    delegate
                    {
                        mockViewModel.PropertyChanged +=
                            delegate
                            {
                                raised = true;
                                raisedOnThread = Thread.CurrentThread;
                                autoResetEvent.Set();
                            };
                        innerResetEvent.Set();                                                 
                    });

                Assert.IsTrue(innerResetEvent.WaitOne(30000), "timed out.");

                Thread threadToSetProperty = new Thread(
                    delegate()
                    {
                        Assert.AreNotEqual(subscriberThread, Thread.CurrentThread);
                        mockViewModel.StringMember = "Test";
                    });
                threadToSetProperty.Start();
            });

        subscriberThread = ModelSynchronizationContext.Instance.Thread;

        autoResetEvent.WaitOne();
        Assert.IsTrue(raised);
        Assert.AreEqual(subscriberThread.ManagedThreadId, raisedOnThread.ManagedThreadId);
    }

}

Notice that the test itself derives from Microsoft.Silverlight.Testing.SilverlightTest, which differs from run-of-the-mill Desktop CLR unit tests that use the Microsoft unit testing tools. Deriving from SilverlightTest affords the ability to perform asynchronous tests, but that is outside of the scope of this article.

Conclusion

In this article we have seen how the Model Thread View Thread pattern uses the same facilities as the MVVM pattern, to better separate the execution of view control specific logic from the rest of the application. This provides us with greater assurance of UI responsiveness, and in the case of Silverlight, facilitates such things as synchronous WCF communication. It also reduces the need for UI thread invocation, and helps to increase model scalability; allowing for event handler logic to grow without degrading UI responsiveness.

This article serves as a definition of the pattern for peer review, and also as proof of concept for the infrastructure required to support the pattern.

I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.

History

April 2010

  • Published.

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Engineer
Switzerland Switzerland
Daniel is a former senior engineer in Technology and Research at the Office of the CTO at Microsoft, working on next generation systems.

Previously Daniel was a nine-time Microsoft MVP and co-founder of Outcoder, a Swiss software and consulting company.

Daniel is the author of Windows Phone 8 Unleashed and Windows Phone 7.5 Unleashed, both published by SAMS.

Daniel is the developer behind several acclaimed mobile apps including Surfy Browser for Android and Windows Phone. Daniel is the creator of a number of popular open-source projects, most notably Codon.

Would you like Daniel to bring value to your organisation? Please contact

Blog | Twitter


Xamarin Experts
Windows 10 Experts

Comments and Discussions

 
GeneralAwesome Pin
CrazyHT2420-Jun-13 10:55
CrazyHT2420-Jun-13 10:55 
GeneralRe: Awesome Pin
Daniel Vaughan21-Jun-13 0:08
Daniel Vaughan21-Jun-13 0:08 
GeneralMy vote of 5 Pin
BadassAlien7-Jan-13 23:30
professionalBadassAlien7-Jan-13 23:30 
GeneralRe: My vote of 5 Pin
Daniel Vaughan21-Jun-13 0:08
Daniel Vaughan21-Jun-13 0:08 
Thank you kindly!
Sorry for the delay in responding.

Cheers,
Daniel
Daniel Vaughan
Twitter | Blog | Microsoft MVP | Projects: Calcium SDK, Clog | LinkedIn

GeneralMy vote of 5 Pin
Manoj Kumar Choubey17-Apr-12 2:09
professionalManoj Kumar Choubey17-Apr-12 2:09 
GeneralRe: My vote of 5 Pin
Daniel Vaughan21-Jun-13 0:10
Daniel Vaughan21-Jun-13 0:10 
QuestionError: 'Microsoft.Practices.Composite.Presentation.Commands.WeakEventHandlerManager' is inaccessible due to its protection level Pin
jobert12313-Nov-11 17:57
jobert12313-Nov-11 17:57 
GeneralMy vote of 5 Pin
Saraf Talukder31-Aug-11 3:07
Saraf Talukder31-Aug-11 3:07 
GeneralRe: My vote of 5 Pin
Daniel Vaughan31-Aug-11 22:48
Daniel Vaughan31-Aug-11 22:48 
Generalgood one - have 5 Pin
Pranay Rana2-Jan-11 19:07
professionalPranay Rana2-Jan-11 19:07 
GeneralRe: good one - have 5 Pin
Daniel Vaughan2-Jan-11 23:32
Daniel Vaughan2-Jan-11 23:32 
QuestionAre there any plans on adding support for WPF applications? Pin
bswanson27-Sep-10 5:49
bswanson27-Sep-10 5:49 
AnswerRe: Are there any plans on adding support for WPF applications? Pin
Daniel Vaughan27-Sep-10 23:39
Daniel Vaughan27-Sep-10 23:39 
QuestionHow to use this pattern with asynchronous methods? Pin
ericdes19-Aug-10 21:51
ericdes19-Aug-10 21:51 
AnswerRe: How to use this pattern with asynchronous methods? Pin
Daniel Vaughan19-Aug-10 23:12
Daniel Vaughan19-Aug-10 23:12 
GeneralOutstanding work, Daniel, as always Pin
Marcelo Ricardo de Oliveira10-Jun-10 3:24
mvaMarcelo Ricardo de Oliveira10-Jun-10 3:24 
GeneralRe: Outstanding work, Daniel, as always Pin
Daniel Vaughan10-Jun-10 21:19
Daniel Vaughan10-Jun-10 21:19 
GeneralHey nice article Pin
NavnathKale9-Jun-10 8:12
NavnathKale9-Jun-10 8:12 
GeneralRe: Hey nice article Pin
Daniel Vaughan9-Jun-10 8:32
Daniel Vaughan9-Jun-10 8:32 
Generalwow Pin
Petr Pechovic8-Jun-10 3:06
professionalPetr Pechovic8-Jun-10 3:06 
GeneralRe: wow Pin
Daniel Vaughan8-Jun-10 3:13
Daniel Vaughan8-Jun-10 3:13 
GeneralAmazing job! Pin
Thiago de Arruda14-May-10 17:21
Thiago de Arruda14-May-10 17:21 
GeneralRe: Amazing job! Pin
Daniel Vaughan15-May-10 3:26
Daniel Vaughan15-May-10 3:26 
GeneralGreat article. a quick question.... Pin
Nrupal Prattipati7-May-10 3:09
professionalNrupal Prattipati7-May-10 3:09 
GeneralRe: Great article. a quick question.... Pin
Nrupal Prattipati7-May-10 3:19
professionalNrupal Prattipati7-May-10 3:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.