65.9K
CodeProject 正在变化。 阅读更多。
Home

隆重推出 Model Thread View Thread 模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (68投票s)

2010 年 5 月 1 日

BSD

14分钟阅读

viewsIcon

212668

downloadIcon

886

通过一种扩展 MVVM 的新模式,减少线程代码,提高 UI 响应速度。

title image

目录

引言

在过去一两年中,WPF 和 Silverlight 最常讨论的话题可能是 MVVM(Model View ViewModel)模式。MVVM 作为 MVC 和 MVP 等类似架构模式的替代方案出现,因为它利用了 WPF 和 Silverlight 的特定功能,尤其是数据绑定基础设施,从而加深了视图层与应用程序其他层之间的分离。这带来了几个好处,包括允许交互式设计人员专注于用户界面(UI)、应用程序层的并行开发以及更易于测试。

使 MVVM 如此有效的机制也可以应用于一个辅助模式,我称之为 Model Thread View Thread 模式(MTVT)。它是一种通过分离的执行线程来显式划分视图和视图模型的架构模式。总之,它的主要特点是它由两个独立的执行线程组成(一个用于视图,一个用于其他应用程序层),并且跨线程通信通过熟悉的 WPF 和 Silverlight 功能(如事件(INotifyPropertyChanged、INotifyCollectionChanged 等)和命令)透明地进行。

在概念验证下载中,我提供了

  • 一个弱引用事件管理系统,它与订阅者保持线程亲和性。
  • 一个在应用程序的模型线程上调用处理程序的命令类。
  • 一个同步的 ObservableCollection,它允许在订阅发生时在模型线程上引发其 CollectionChanged 事件。
  • 以及一个在订阅者线程上引发属性更改的 ViewModelBase 类。

MTVT 专门致力于提供一种非以表示层为中心的模型。在 MTVT 中,模型逻辑的执行通过异步线程模型与 UI 控件分离。

MTVT 取代了传统的以 UI 为中心的方法,在该方法中,UI 线程是主要线程,并且所有活动默认都在该线程上发生。

为什么需要新方法?

MTVT 的设计旨在利用 WPF 和 Silverlight 的特定功能,在运行时更好地分离视图或表示层,从而提供更高的 UI 响应保证,并在 Silverlight 的情况下,促进同步 WCF 通信等事项。

.NET GUI 应用程序传统上是 UI 线程中心的。例如,开发人员可能会为事件提供事件处理程序,如果该处理程序被认为是耗时操作,则开发人员将编写代码将其分发到工作线程。然而,开发人员经常会忽略编写异步代码,因为处理程序的执行时间被低估,或者在当时编写代码被认为过于繁琐。这样做不当的后果通常直到之后才会显现。

另一方面,如果开发人员采取在 UI 线程上调用所有状态更改的方法,那么不仅可能导致大量的丑陋的样板代码,而且如果我们不小心,还会导致应用程序性能下降。

MTVT 在一定程度上解决了这种随意的委托方法。它减少了围绕并发控制的重复编码活动的需要,并减轻了从模型中显式调用 UI 更改的需要。此外,显式指定单个模型线程可能有助于减少并发问题,而并发问题在处理多个子线程时是不可避免的。

另一个好处是模型逻辑变得更具可伸缩性,因为引入新功能不会减慢 UI。

如今,我们看到许多工具正在涌现,以帮助透明地并行化工作(例如 PLINQ 和 TPL)。并行化基础设施将变得越来越普遍,而此类技术侧重于抽象检索数据的机制(例如使用 LINQ),并侧重于“做什么而不是怎么做”。这个概念验证试图做同样的事情,即引发事件、执行命令和更新集合的机制保持不变。

背景

很久以前,在 2008 年,我发布了一篇文章,其中描述了如何在 Silverlight 中执行阻塞的 WCF 服务调用(在用户代码级别是同步的),从而消除了异步要求;这可能导致代码混乱。它描述了一个Reactor 实现,为 Silverlight 异步 API 提供同步接口。从那时起,我收到了用户关于如何使其一切正常工作的不懈电子邮件和消息,这让我感到惊讶。许多人认为,尽管这是可能的,正如我所演示的那样,但他们很难理解“UI 线程不一定应该是‘驱动’线程”这一概念。而且,我有点能理解为什么。

MVVM 表明在大多数情况下,可以实现足够的视图分离。然而,由于 WPF 和 Silverlight 都依赖于与 UI 线程的线程亲和性,因此会出现与线程相关的问题,这可能会阻碍该模式的应用。这促使我设计了 MTVT。由于其绑定、命令和事件基础设施,该模式很自然地适用于 Silverlight 和 WPF。正如我们将看到的,我将演示如何透明地利用这些功能来支持该模式。没有什么新的需要做的(事实上需要做的更少),我们也不需要彻底改变工作方式。

示例应用程序

要求

该示例是一个粗糙的 Silverlight 应用程序,它演示了使双线程方法成为可能的操作、事件和集合。它由一个单页组成,带有一个交互式组件:一个按钮。

Sample application screen shot

图:演示应用程序。

“Fire view model command”按钮有一个附加属性,即 Prism 的 Click.Command 属性。

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

当单击按钮时,位于 MainPageViewModel 中的执行处理程序将在模型线程上调用,如下面的摘录所示

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;
}

视图模型的 Message 属性被修改。此属性更改发生在视图模型线程上,但 UI 通过 UISynchronizationContext 得知更改,该上下文在 UI 线程上调用更改处理程序,以避免引发无效的跨线程异常。

screen shot after pressing button

图:视图模型被挂起,但视图保持响应。

模型线程完成挂起后,我们将一个项添加到我们的自定义可观察集合中,该集合安全地在其 CollectionChanged 事件处理程序在 UI 线程上引发,即 Silverlight 数据绑定基础设施订阅事件的线程。

new item added to list

图:项已安全添加到集合中。

这表明我们确实能够将 UI 线程与模型线程分离。

实现中存在一些微妙之处。例如,您可能想知道我们如何处理 CanExecute 事件处理程序,该处理程序要求方法返回一个结果;因此需要阻塞 UI 线程。而在 Silverlight 中,阻塞 UI 线程是不可行的,因为事件循环由 UI 线程服务。但是,我们仍然可以通过使用回调来实现“启用/禁用”功能。这可以在 ModelCommand 类中的以下摘录中看到,该类使用了 Prism 的 WeakEventHandlerManager

ModelCommand 类

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.

}

这里我们看到 CanExecute 方法在调用模型线程上的 CanExecute 事件处理程序时不会阻塞调用者。但它确实会在不阻塞的情况下调用处理程序,并通过调用 OnCanExecuteChanged 方法来指示何时获得了结果。这会触发 CanExecute 方法第二次调用。第二次调用时,canExecuteResultReceived 的值用于指示这次我们有一个值,因此提供了 canExecuteResult,并将标志重置。

ModelCommand

图:ModelCommand 使用 ModelSynchronizationContext 来在模型线程上调用 ExecuteCanExecute 事件处理程序。

当命令被执行时,就像在示例应用程序中单击按钮时一样,我们看到事件处理程序使用 ModelSynchronizationContext 被调用。这是在非阻塞调用中发生的,因此可以防止 UI 锁定。

同步基础设施

我们已经查看了示例应用程序。现在让我们更详细地探讨一下基础设施。我们将从同步基础设施开始,并查看它是如何用于维护两个独立的执行线程的,特别是 ISynchronizationContext 接口,它定义了所有同步上下文的标准功能。

ISynchronizationContext 接口

此接口指定 ActionSendOrPostCallback 可能会被排队以阻塞或非阻塞的方式调用。

ISynchronizationContext Class Diagram

图:ISynchronizationContext 类图。

此接口有两个实现。它们是 ModelSynchronizationContextUISynchronizationContext。两者都负责在模型线程或 UI 线程上调用委托。

ModelSynchronizationContext 类

ModelSynchronizationClass 使用专用线程来调用操作或 CallbackReferences 的队列。这呈现了一个熟悉的生产者-消费者问题;可以使用 AutoResetEvent 轻松解决,它用于在队列中添加项时发出信号。因此,我们不必诉诸轮询机制。以下摘录来自 ModelSynchronizationContext

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;
        }
    }
}

请注意,在 InvokeAndBlockUntilCompletion 方法的任何一个重载中,为了调用一个操作然后阻塞直到该操作完成,我们使用了另一个名为 *itemResetEvent* 的 AutoResetEvent。每当队列被我们的模型线程消耗时,每完成一个操作,就会引发 invokeComplete 事件,这允许匿名处理程序(在 InvokeAndBlockUntilCompletion 方法中)测试被调用的操作是否是正在等待的操作。如果是,这表明操作已被调用,并且该方法可以自由返回。

UISynchronizationContext 类

另一个 ISynchronizationContext 实现是 UISynchronizationContext。它利用 DispatcherDispatcherSynchronizationContext 来处理繁重的工作。为了在 UI 线程上执行阻塞调用,我们使用 DispatcherSynchronizationContext。我经常看到人们想知道如何使用 Dispatcher 执行阻塞调用,方法是:获取一个 Dispatcher 并实例化一个 DispatcherSynchronizationContext,然后调用 context.Post(callback, state);

/// <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();
        }
    }
}

具有线程亲和性的事件

我们已经看到有两个上下文(UISynchronizationContextModelSynchronizationContext)用于在 UI 和模型线程上调用委托。现在让我们将注意力转向用于提供具有线程亲和性的事件的基础设施。

ViewModelBase Class Diagram

图:ViewModelBase 类通过 PropertyChangeNotifier 发送属性更改信号

DelegateManager 类

为了在事件订阅的同一线程上调用委托,我们使用了 DelegateManagerDelegateManager 类允许我们调用委托列表,其中每个委托都可以与特定线程相关联。DelegateManager 还使用 WeakReferences,这有助于确保不会发生内存泄漏。DelegateManager 类在此完整提供

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 类

那么,我们如何在一个特定线程上调用 delegate 呢?嗯,这不能随意进行。为此,我们使用了我在概念验证中广泛使用的扩展点。它是一个 IProvider<ISynchronizationContext>,其默认实现为 SynchronizationContextProvider。它确定用于与委托关联的 ISynchronizationContext。请注意上面它如何传递给 DelegateManager's 构造函数。

/// <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 类

PropertyChangeNotifier 使用两个 DelegateManagers。一个用于 INotifyPropertyChanged 事件,另一个用于 INotifyPropertyChanging 事件。此类使用 WeakReference 将自身与宿主类关联,以接管属性更改通知的职责。它还增加了一些便利功能,如可取消的更改。我最喜欢此类的方法是 Assign 方法。我到处使用它来进行属性更改,因为它

  • 负责通知属性即将更改,
  • 执行更改(除非被取消),
  • 然后通知更改已执行。

MainPageViewModel 类中可以找到几个示例。以下摘录显示了一个这样的示例

string message;

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

Assign 方法接受属性名称(在本例中,它是从T4 元数据生成模板生成的名称)、对可能更改的字段的引用以及新值。

回到 PropertyChangeNotifier 的实际实现,这里是完整的代码

/// <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

}

您会注意到,还可以使用 lambda 表达式来表示属性名称,但出于性能原因,我不建议这样做。

因此,我们看到 PropertyChangeNotifier 使用 DelegateManagers 来聚合委托,并在引发 PropertyChangedPropertyChanging 事件时调用它们。为了避免重复,我有时会使用 NotifyPropertyChangeBase 类。

NotifyPropertyChangeBase 类

NotifyPropertyChangeBase 类封装了一个 PropertyChangeNotifier 实例,该实例在一定程度上实现了 PropertyChangeNotifier 的序列化和延迟加载。它还提供了一些更简洁的代码,可以省略字段限定符。

/// <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
}

此类作为我们 ViewModelBase 类的基类。

ViewModelBase 类

这是一个抽象类,是所有视图模型的基类。在概念验证中,它几乎没有实现,目前仅作为占位符。

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

通过指定我们的 PropertyChangeNotifier 保持线程亲和性,这意味着事件处理程序将在订阅线程(UI 线程或模型线程)上执行。

具有事件线程亲和性的集合

ObservableCollections 经常在 WPF 和 Silverlight 应用程序中使用,以在向集合添加或删除项时自动更新 UI。在本文前面,我们查看了示例应用程序在 MainPageViewModel 中使用的自定义集合。该集合是 SynchronizedObservableCollection,它恰好使用了我们的 DelegateManager 类来关联事件订阅的线程和处理程序。这意味着当向集合添加或删除项时,每个 NotifyCollectionChangedEventHandler 订阅者都会在正确的线程上得到通知。这很重要,因为绑定到集合的 UI 元素如果在非 UI 线程上调用处理程序,将会引发异常。如果没有这种机制,我们将需要手动将任何集合更新调用到 UI 线程。

SynchronizedObservableCollection 类

SynchronizedObservableCollection 在外观上与 FCL 中的 ObservableCollection 实现非常相似,但有一些显著的差异。它使用了 DelegateManager,允许在正确的线程上调用 INotifyCollectionChanged 事件处理程序。为了帮助防止竞态条件,集合的更改是通过 UISynchronizationContext 在 UI 线程上调用的。

/// <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));
            });
    }
}

基础设施单元测试

有趣的是,使我们能够单元测试 Silverlight 应用程序的基础设施并不包含在 Silverlight Tools (SDK) 中,而是包含在 Silverlight Toolkit 中。

我在概念验证开发期间创建了一些测试。这在开发过程中非常有用,因为它允许我在不依赖 UI 的情况下调试和识别线程。

test runner

图:Silverlight 单元测试

Visual Studio 内置的单元测试基础设施不支持 Silverlight 单元测试。这意味着执行单元测试需要将应用程序的默认页面设置为生成的单元测试页面。在示例应用程序的情况下,这是 Tests.aspx 页面。

tests

图:测试完成。

以下列表显示了线程测试的内容

[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);
    }

}

请注意,测试本身派生自 Microsoft.Silverlight.Testing.SilverlightTest,这与使用 Microsoft 单元测试工具的普通桌面 CLR 单元测试不同。派生自 SilverlightTest 允许执行异步测试,但这超出了本文的范围。

结论

在本文中,我们看到了 Model Thread View Thread 模式如何使用与 MVVM 模式相同的设施,更好地将视图控制特定的逻辑执行与应用程序的其余部分分开。这为我们提供了更高的 UI 响应保证,并且在 Silverlight 的情况下,促进了同步 WCF 通信等事项。它还减少了 UI 线程调用的需求,并有助于提高模型的可伸缩性;允许事件处理程序逻辑增长而不会降低 UI 响应速度。

本文旨在作为同行评审的模式定义,并作为支持该模式所需基础设施的概念验证。

我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。

历史

2010年4月

  • 已发布。

© . All rights reserved.