WPF/多线程:MVVM中的UI调度程序

16

假设在MVVM环境下,我在后台线程中,想要在UI控件上运行更新操作。通常情况下,我会使用myButton.Dispatcher.BeginInvoke(blabla)方法,但是我无法访问myButton(因为视图模型无法访问视图的控件)。那么,如何正常地完成这一任务呢?

(我猜总有绑定的方式,但我想知道如何通过调度程序完成)


2
这是 https://dev59.com/NnRB5IYBdhLWcg3w3K4J 的副本吗? - Andrew T Finnell
不是重复问题...他正在询问如何从由ViewModel启动的后台线程中获取调度程序(通常无法访问调度程序)。 - Michael Brown
7个回答

38

我通常使用Application.Current.Dispatcher:由于Application.Current是静态的,所以不需要引用控件。


7
没有应用程序对象,你将如何对视图模型进行单元测试? - Geert van Horrik
2
@Geert van Horrik,您可以模拟应用程序对象来对视图模型进行单元测试。抱歉打扰了,我在搜索适用于多线程编程的有效MVVM解决方案时偶然发现了这篇SO文章。 - SRM
1
@aaronburro 没有任何阻止你将其放在一个抽象层后面,这样你就可以在单元测试中进行模拟。 - Thomas Levesque
我认为你甚至已经写了一篇关于如何做到这一点的不错博客文章。 - aaronburro

15

来自Caliburn Micro源代码:

public static class Execute
{
    private static Action<System.Action> executor = action => action();

    /// <summary>
    /// Initializes the framework using the current dispatcher.
    /// </summary>
    public static void InitializeWithDispatcher()
    {
#if SILVERLIGHT
        var dispatcher = Deployment.Current.Dispatcher;
#else
        var dispatcher = Dispatcher.CurrentDispatcher;
#endif
        executor = action =>{
            if(dispatcher.CheckAccess())
                action();
            else dispatcher.BeginInvoke(action);
        };
    }

    /// <summary>
    /// Executes the action on the UI thread.
    /// </summary>
    /// <param name="action">The action to execute.</param>
    public static void OnUIThread(this System.Action action)
    {
        executor(action);
    }
}

在使用它之前,您需要从UI线程调用Execute.InitializeWithDispatcher(),然后您可以像这样使用它:Execute.OnUIThread(()=>SomeMethod())


如果我使用Dispatcher.CurrentDispatcher,仍然会出现错误。如果我使用Application.Current.Dispatcher,它就可以正常工作。 - Ken Smith

5
我倾向于让我的ViewModel继承自DependencyObject并确保它们在UI线程上构造,这使它们完美地适应了这种情况 - 它们有一个与UI线程的Dispatcher对应的属性。然后,您不需要用ViewModel的实现细节来污染您的视图。
其他几点优势:
- 可以进行单元测试: 您可以在没有运行应用程序的情况下对它们进行单元测试(而不是依赖于Application.Current.Dispatcher) - 视图和ViewModel之间松耦合 - 您可以在ViewModel中定义依赖属性,并写入零代码来更新视图,随着这些属性的更改。

DependencyObject 的体量相当大,你真正需要的是 DispatcherObject。它有一个简单的实现:它的构造函数调用 Dispatcher.CurrentDispatcher 并将该值存储在字段中供以后使用。如果你不能向你的 ViewModel 添加一个基类,你可以轻松地实现 DispatcherObject 所做的事情。 - Tomas Karban

4

Catel 的 ViewModelBase 类有一个 Dispatcher 属性,您可以使用它。


DispatcherHelper类中的GetCurrentDispatcher方法。https://catel.codeplex.com/SourceControl/latest#src/Catel.MVVM/Catel.MVVM.NET40/Windows/Threading/Helpers/Dispatcherhelper.cs - Der_Meister

0
你可以在你的视图模型上引发一个事件(可能使用命名约定来指示它将从非 UI 线程引发 - 例如 NotifyProgressChangedAsync)。然后,附加到该事件的视图可以适当地处理调度程序。
或者,你可以将委托传递给一个同步函数,以便从你的视图向你的视图模型传递。

0
将UI线程的调度程序传递给ViewModel的构造函数,并将其存储在VM中。
请注意,每个线程可能都有自己的调度程序。您需要使用UI线程的调度程序!

可能存在多个需要关注的调度程序(虽然这是一个糟糕的设计想法,但确实会发生),这表明传递调度程序是一个糟糕的想法。 - aaronburro

0

大多数情况下,ViewModel 中不需要使用 Dispatcher(>99% 的情况)。早期版本的 .NET 无法适当地将 PropertyChanged 事件转发到 UI 线程,这会导致问题。有一种方法可以解决这个问题,需要以一种能够意识到 Dispatcher 并在需要时自动转发的方式引发该事件。.NET 3.5 及以上版本现在会自动执行此操作。

因为 Dispatcher 是一个 UI 概念,在 ViewModel 中出现它是一个很大的代码异味。这表明您正在做错事情。更可能的情况是,您需要注入某些东西,以将您的 ViewModel 与您正在操作的 UI 资源(更改鼠标光标是一个很好的例子)抽象出来,或者您实际上已经将 View 和 ViewModel 耦合在一起。在后一种情况下,通常可以通过某种附加行为来修复此问题,该附加行为将订阅 ViewModel 上的事件和属性更改。

这里有一个问题... CollectionChanged 具有线程亲和性(间接通过 WPF 自动创建的 CollectionViews),最好的解决方法是在引发该事件时检查事件委托订阅者上的 SynchronizationContexts。虽然这很糟糕,但仍不需要将 Dispatcher 传递给 VM。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接