如何将 UI Dispatcher 传递给 ViewModel

Posted

技术标签:

【中文标题】如何将 UI Dispatcher 传递给 ViewModel【英文标题】:How to pass the UI Dispatcher to the ViewModel 【发布时间】:2011-01-22 04:54:02 【问题描述】:

我应该能够访问属于我需要将其传递给 ViewModel 的视图的Dispatcher。但是View应该对ViewModel一无所知,那怎么传呢?引入一个接口还是不将其传递给实例,而是创建一个将由 View 编写的全局调度程序单例?您如何在 MVVM 应用程序和框架中解决这个问题?

编辑:请注意,由于我的 ViewModel 可能是在后台线程中创建的,因此我不能只在 ViewModel 的构造函数中执行 Dispatcher.Current

【问题讨论】:

【参考方案1】:

我已经使用接口 IContext 抽象了 Dispatcher:

public interface IContext

   bool IsSynchronized  get; 
   void Invoke(Action action);
   void BeginInvoke(Action action);

这样做的好处是您可以更轻松地对 ViewModel 进行单元测试。 我使用 MEF(托管可扩展性框架)将接口注入到我的 ViewModel 中。另一种可能性是构造函数参数。 但是,我更喜欢使用 MEF 进行注射。

更新(来自 cmets 中的 pastebin 链接的示例):

public sealed class WpfContext : IContext

    private readonly Dispatcher _dispatcher;

    public bool IsSynchronized
    
        get
        
            return this._dispatcher.Thread == Thread.CurrentThread;
        
    

    public WpfContext() : this(Dispatcher.CurrentDispatcher)
    
    

    public WpfContext(Dispatcher dispatcher)
    
        Debug.Assert(dispatcher != null);

        this._dispatcher = dispatcher;
    

    public void Invoke(Action action)
    
        Debug.Assert(action != null);

        this._dispatcher.Invoke(action);
    

    public void BeginInvoke(Action action)
    
        Debug.Assert(action != null);

        this._dispatcher.BeginInvoke(action);
    

【讨论】:

您能否提供一个 wpf UserControl 中的实现示例? 是的,可以提供任何关于实现的示例吗?谢谢。【参考方案2】:

你为什么不使用

 System.Windows.Application.Current.Dispatcher.Invoke(
         (Action)(() => ObservableCollectionMemeberOfVM.Add("xx");  ));

而不是保留对 GUI 调度程序的引用。

【讨论】:

奇怪的 Dispatcher.CurrentDispatcher 对我不起作用。 Application.Current.Dispatcher 工作。【参考方案3】:

您可能实际上并不需要调度程序。如果将视图模型上的属性绑定到视图中的 GUI 元素,WPF 绑定机制会使用调度程序自动将 GUI 更新编组到 GUI 线程。


编辑:

此修改是为了回应 Isak Savo 的评论。

在 Microsoft 处理绑定到属性的代码中,您会发现以下代码:

if (Dispatcher.Thread == Thread.CurrentThread)
 
    PW.OnPropertyChangedAtLevel(level);
 
else 

    // otherwise invoke an operation to do the work on the right context 
    SetTransferIsPending(true);
    Dispatcher.BeginInvoke(
        DispatcherPriority.DataBind,
        new DispatcherOperationCallback(ScheduleTransferOperation), 
        new object[]o, propName);
 

此代码将所有 UI 更新编组到线程 UI 线程,这样即使您更新了从不同线程中获取部分绑定的属性,WPF 也会自动序列化对 UI 线程的调用。

【讨论】:

但在很多情况下,您可能需要这样做,想象一下绑定到 UI 的 ObservableCollection 并且您尝试从工作线程调用 _collection.Add() 我知道。当然,通常的主题注意事项仍然适用。 目前我们需要调度程序来将项目添加到 ObservableCollection。 嗨,伊萨克。你正确地理解了我,但你错了。如果您通过 Microsoft 的 WPF 绑定代码进行调试(或使用 Reflector 查看它),您将看到代码检查您是否在 GUI 线程上,如果不在,它将使用 Dispatcher 在 GUI 线程上更新。我不知道它是否适用于 ObservableCollection 但它适用于“正常”属性。我为此写了一篇博客文章(虽然是丹麦语)。在博客条目的底部显示了微软的代码:dotninjas.dk/post/Flere-trade-og-binding-i-WPF.aspx 雅各布:你完全正确。我已将我的 -1 修改为 +1。我在 .net 3.5 和 4.0 中都试过了,似乎在后台线程上引发 PropertyChanged 是完全可以的。【参考方案4】:

我让 ViewModel 将当前调度程序存储为成员。

如果 ViewModel 是由视图创建的,则您知道在创建时当前的调度程序将是视图的调度程序。

class MyViewModel

    readonly Dispatcher _dispatcher;
    public MyViewModel()
    
        _dispatcher = Dispatcher.CurrentDispatcher;
    

【讨论】:

在我的场景中,ViewModel 是在线程中创建的。这就是我一开始问的原因。 但是单元测试有问题。你如何编写这种代码并对你的虚拟机进行单元测试? @Roy:看到这个问题***.com/questions/1106881/… 是的,DispatcherFrame 使您的单元测试变得笨拙且难以编写。为什么不把它抽象掉呢?【参考方案5】:

从 MVVM Light 5.2 开始,该库现在在 GalaSoft.MvvmLight.Threading 命名空间中包含一个 DispatcherHelper 类,该类公开了一个函数 CheckBeginInvokeOnUI(),该函数接受一个委托并在 UI 线程上运行它。如果您的 ViewModel 正在运行一些影响您的 UI 元素绑定到的 VM 属性的工作线程,则非常方便。

DispatcherHelper 必须在应用程序生命周期的早期阶段通过调用DispatcherHelper.Initialize() 来初始化(例如App_Startup)。然后,您可以使用以下调用运行任何委托(或 lambda):

DispatcherHelper.CheckBeginInvokeOnUI(
        () =>
        
           //Your code here
        );

请注意,该类是在GalaSoft.MvvmLight.Platform 库中定义的,当您通过 NuGet 添加它时,默认情况下不会引用该库。您必须手动添加对此库的引用。

【讨论】:

DispatcherHelper.CheckBeginInvokeOnUI 已使用但 datagrid 加载大量数据然后 UI 挂起为什么? DispatcherHelper 仅用于将您的调用编组到 UI 线程。在工作线程完成工作后使用它。它不会神奇地减少加载时间,也不会以某种方式使DataGrid 加载数据更快。它只是摆脱了臭名昭著的“对 UI 的非法跨线程调用”异常。为了提高您的 UI 响应能力,请查看 async/await 模式以及 WPF DataGrid 中的虚拟化支持。 Datagrid 使用 observable 集合加载数据非常慢 不要同时在网格中加载数千行。此外,如果您不需要单元格级别的编辑,您最好不要使用标准的ListBox。如果您必须使用DataGrid,请使用某种分页来最大限度地减少一次需要显示的数据量。 顺便说一句,如果您需要针对特定​​场景的帮助,请发布一个新问题。评论只应与发布的问题或答案相关,或在某些方面使其更好。【参考方案6】:

另一个常见的模式(现在在框架中得到了广泛的使用)是SynchronizationContext。

它使您能够同步和异步调度。您还可以在当前线程上设置当前 SynchronizationContext,这意味着它很容易被模拟。 WPF 应用程序使用 DispatcherSynchronizationContext。 SynchronizationContext 的其他实现由 WCF 和 WF4 使用。

【讨论】:

我认为这是最好和最简单的方法。您可以默认 ViewModel 的 SynchronizationContext 为创建 ViewModel 的线程的当前上下文,如果 UserControl 需要更改它,他们可以随意更改它。【参考方案7】:

从 WPF 4.5 版开始,可以使用CurrentDispatcher

Dispatcher.CurrentDispatcher.Invoke(() =>

    // Do GUI related operations here

, DispatcherPriority.Normal); 

【讨论】:

这个单例在多线程场景中效果不是很好。【参考方案8】:

如果您只需要调度程序来修改另一个线程中的绑定集合,请查看这里的 SynchronizationContextCollection http://kentb.blogspot.com/2008/01/cross-thread-collection-binding-in-wpf.html

效果很好,我发现的唯一问题是当使用带有 ASP.NET 同步上下文的 SynchronizationContextCollection 属性的视图模型时,但很容易解决。

HTH 山姆

【讨论】:

【参考方案9】:

嗨,也许我来得太晚了,因为距离你第一次发帖已经 8 个月了... 我在 silverlight mvvm 应用程序中遇到了同样的问题。我找到了这样的解决方案。对于我拥有的每个模型和视图模型,我还有一个名为控制器的类。 像这样

public class MainView : UserControl  // (because it is a silverlight user controll)
public class MainViewModel
public class MainController

我的 MainController 负责模型和视图模型之间的命令和连接。在构造函数中,我实例化了视图及其视图模型,并将视图的数据上下文设置为其视图模型。

mMainView = new MainView();
mMainViewModel = new MainViewModel();
mMainView.DataContext = mMainViewModel; 

//(在我的命名约定中,我有一个前缀 m 用于成员变量)

我的 MainView 类型中也有一个公共属性。像这样

public MainView View  get  return mMainView;  

(这个 mMainView 是公共属性的局部变量)

现在我完成了。我只需要像这样使用我的调度程序来处理我的 ui therad...

mMainView.Dispatcher.BeginInvoke(
    () => MessageBox.Show(mSpWeb.CurrentUser.LoginName));

(在这个例子中,我要求我的控制器获取我的 sharepoint 2010 登录名,但你可以做你需要的)

我们几乎完成了,您还需要像这样在 app.xaml 中定义您的根视觉对象

var mainController = new MainController();
RootVisual = mainController.View;

这对我的申请有所帮助。也许它也可以帮助你......

【讨论】:

【参考方案10】:

您不需要将 UI Dispatcher 传递给 ViewModel。 UI Dispatcher 可从当前应用程序单例中获得。

App.Current.MainWindow.Dispatcher

这将使您的 ViewModel 依赖于 View。根据您的应用程序,这可能会也可能不会。

【讨论】:

【参考方案11】:

对于 WPF 和 Windows 商店应用使用:-

       System.Windows.Application.Current.Dispatcher.Invoke((Action)(() => ObservableCollectionMemeberOfVM.Add("xx");  ));

保留对 GUI 调度程序的引用并不是真正正确的方法。

如果这不起作用(例如在 windows phone 8 应用程序的情况下),请使用:-

       Deployment.Current.Dispatcher

【讨论】:

【参考方案12】:

如果您使用uNhAddIns,您可以轻松地进行异步行为。看看here

我认为需要进行一些修改才能使其在温莎城堡上运行(没有 uNhAddIns)

【讨论】:

【参考方案13】:

我找到了另一种(最简单的)方法:

添加到应在 Dispatcher 中调用的视图模型操作:

public class MyViewModel

    public Action<Action> CallWithDispatcher;

    public void SomeMultithreadMethod()
    
        if(CallWithDispatcher != null)
            CallWithDispatcher(() => DoSomethingMetod(SomeParameters));
    

并在视图构造函数中添加此操作处理程序:

    public View()
    
        var model = new MyViewModel();

        DataContext = model;
        InitializeComponent();

        // Here 
        model.CallWithDispatcher += act => _taskbarIcon.Dispatcher
            .BeginInvoke(DispatcherPriority.Normal, act) ;
    

现在测试没有问题,而且很容易实现。 我已将其添加到我的site

【讨论】:

【参考方案14】:

当您可以使用应用程序调度程序访问时,无需传递调度程序

Dispatcher dis = Application.Current.Dispatcher 

【讨论】:

【参考方案15】:

我的一些 WPF 项目遇到了同样的情况。在我的 MainViewModel(单例实例)中,我的 CreateInstance() 静态方法采用了调度程序。并且从视图调用创建实例,以便我可以从那里传递调度程序。 并且 ViewModel 测试模块调用 CreateInstance() 无参数。

但在复杂的多线程场景中,在 View 端有一个接口实现总是好的,以便获得当前 Window 的正确 Dispatcher。

【讨论】:

【参考方案16】:

也许我这个讨论有点晚了,但我找到了一篇不错的文章https://msdn.microsoft.com/en-us/magazine/dn605875.aspx

有1段

此外,View 层之外的所有代码(即 ViewModel 和模型层、服务等)不应依赖于任何类型 绑定到特定的 UI 平台。任何直接使用 Dispatcher (WPF/Xamarin/Windows Phone/Silverlight)、CoreDispatcher (Windows Store)或 ISynchronizeInvoke(Windows 窗体)是一个坏主意。 (SynchronizationContext 稍微好一点,但几乎没有。)对于 例如,互联网上有很多代码可以做一些 异步工作,然后使用 Dispatcher 更新 UI;一个更 可移植且不那么繁琐的解决方案是使用 await 进行异步 在不使用 Dispatcher 的情况下工作和更新 UI。

假设您可以正确使用 async/await,这不是问题。

【讨论】:

以上是关于如何将 UI Dispatcher 传递给 ViewModel的主要内容,如果未能解决你的问题,请参考以下文章

姜戈。如何将下一个变量传递给 auth_views.login

如何将选项更改传递给 Angular-UI ui-select2?

c#winform不是用this.dispatcher.invoke来异步更新ui吗?

如何使用 styled-components 将 prop 传递给 Material-UI 组件

JSF 2:如何将包含要调用的参数的动作传递给 Facelets 子视图(使用 ui:include 和 ui:param)?

在 Material UI 中,如何动态地将颜色传递给我的样式?