确保在MVVM WPF应用程序中,OnPropertyChanged()方法在UI线程上被调用

40
在我所编写的使用MVVM模式的WPF应用程序中,我有一个后台进程在执行任务,但需要将其状态更新传递到UI界面上。
我正在使用MVVM模式,因此我的ViewModel几乎不知道呈现模型给用户的视图(UI)是什么。
假设我在ViewModel中有以下方法:
public void backgroundWorker_ReportProgress(object sender, ReportProgressArgs e)
{
    this.Messages.Add(e.Message);
    OnPropertyChanged("Messages");
}

在我的视图中,我有一个绑定到 ViewModel 的 Messages 属性(一个 List<string>)的 ListBox。通过调用 PropertyChangedEventHandlerOnPropertyChanged 扮演了 INotifyPropertyChanged 接口的角色。

我需要确保 OnPropertyChanged 在 UI 线程上被调用 - 我该如何做到这一点?我尝试了以下方法:

public Dispatcher Dispatcher { get; set; }
public MyViewModel()
{ 
    this.Dispatcher = Dispatcher.CurrentDispatcher;
}

然后在 OnPropertyChanged 方法中添加以下内容:

if (this.Dispatcher != Dispatcher.CurrentDispatcher)
{
    this.Dispatcher.Invoke(DispatcherPriority.Normal, new ThreadStart(delegate
    {
        OnPropertyChanged(propertyName);
    }));
    return;
}

但这并没有起作用。有什么想法吗?

5个回答

38

WPF会自动将属性更改派遣到UI线程。但是,它不会派遣集合更改,因此我怀疑您添加的消息导致了失败。

您可以手动自行派遣添加操作(请参见下面的示例),或使用像这种技术(我之前在博客中介绍过)。

手动派遣:

public void backgroundWorker_ReportProgress(object sender, ReportProgressArgs e)
{
    Dispatcher.Invoke(new Action<string>(AddMessage), e.Message);
    OnPropertyChanged("Messages");
}

private void AddMessage(string message)
{
    Dispatcher.VerifyAccess();
    Messages.Add(message);
}

搞定了,只进行了一个小修改——我把我的List<string>改成了ObservableColletion<string>,现在它可以完美地工作了。谢谢! - Adam Barney
2
如果'Messages'是可观察的,就不需要为它调用OnPropertyChanged()。 - Doug

8
我非常喜欢Jeremy的回答:Silverlight中的调度 总结:
- 在ViewModel中放置Dispatcher似乎不太优雅 - 创建一个Action<Action>属性,在VM构造函数中将其设置为仅运行该操作 - 当从V使用VM时,将Action属性设置为调用Dispatcher

1
原始链接已经失效。这里提供 Wayback Machine 的替代链接:https://web.archive.org/web/20160402132636/http://www.wintellect.com/devcenter/jlikness/dispatching-in-silverlight - Tomas Karban

3
我最近遇到了一个类似的情况(也是在MVVM中)。我有一个独立的类执行它的任务,在事件处理程序上报告状态。事件处理程序按预期被调用,我可以看到Debug.WriteLine的结果及时返回。
但是,在WPF中,无论我做什么,UI都不会更新,直到进程完成。一旦进程完成,UI就会按预期更新。好像它已经得到了PropertyChanged,但在做完线程后才进行UI更新。
(令我失望的是,在Windows.Forms中具有DoEvents和.Refresh()的相同代码运行良好。)
到目前为止,我通过在其自己的线程上启动该进程来解决了这个问题:
//hook up event handler
myProcess.MyEvent += new EventHandler<MyEventArgs>(MyEventHandler); 

//start it on a thread ...
ThreadStart threadStart = new ThreadStart(myProcess.Start);

Thread thread = new Thread(threadStart);

thread.Start();

然后在事件处理程序中:

private void MyEventHandler(object sender, MyEventArgs e) { 
....
Application.Current.Dispatcher.Invoke(
                DispatcherPriority.Send,
                (DispatcherOperationCallback)(arg =>
                { 
         //do UI updating here ...
        }), null);

我不建议使用这段代码,因为我仍在努力理解WPF线程模型、Dispatcher的工作原理以及为什么在我的情况下即使事件处理程序按预期被调用(按设计?),UI也不会更新直到进程完成。但是目前这对我有用。

我发现以下两个链接很有帮助:

http://www.nbdtech.com/blog/archive/2007/08/01/Passing-Wpf-Objects-Between-Threads-With-Source-Code.aspx

http://srtsolutions.com/blogs/mikewoelmer/archive/2009/04/17/dealing-with-unhandled-exceptions-in-wpf.aspx


0

这更像是对已接受答案的扩展,但我使用我的事件处理程序做到了这一点...

using System.Threading;

private void Handler(object sender, RoutedEventArgs e)
{
    if (Thread.CurrentThread == this.Dispatcher.Thread)
    {
        //do stuff to this
    }
    else
    {
        this.Dispatcher.Invoke(
            new Action<object, RoutedEventArgs>(Handler),
            sender,
            e);
    }
}

0

我在我的ViewModel外处理BackgroundWorker.ReportProgress事件,并将实际的BackgroundWorker实例和ViewModel传递到定义async方法的类中。

然后,async方法调用bgWorker.ReportProgress并将一个包装委托的类作为UserState(作为object)传递。我将要写的委托是匿名方法。

在事件处理程序中,我将它从object强制转换回包装类型,然后在其中调用委托。

所有这些意味着我可以直接从异步运行的代码编写UI更改,但它只有这个包装。

以下更详细地解释了这一点:

http://lukepuplett.blogspot.com/2009/05/updating-ui-from-asynchronous-ops.html


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