从TPL任务向WPF视图报告进度的适当方法是什么?

本文关键字:方法 是什么 任务 TPL WPF 报告 视图 | 更新日期: 2023-09-27 18:01:54

我试图在小"块"或页面从数据库中读取大量行,并向用户报告进度;例如,如果我加载100个"块",报告每个块加载的进度。

我正在使用c# 4.0中的TPL从数据库中读取这些块,然后将完整的结果集交给另一个可以使用它的任务。我觉得TPL给了我比BackgroundWorker更好的控制任务取消和移交等,但是似乎没有一个内置的方式来报告任务的进度。

这是我实现的解决方案,用于向WPF进度条报告进度,我想确保这是合适的,并且没有更好的方法我应该采取。

我首先创建了一个简单的界面来表示变化的进度:

public interface INotifyProgressChanged
{
  int Maximum { get; set; }
  int Progress { get; set; }
  bool IsIndeterminate { get; set; }
}

这些属性可以绑定到WPF视图中的ProgressBar,接口由后台ViewModel实现,它负责初始化数据加载并最终报告总体进度(为本例进行了简化):

public class ContactsViewModel : INotifyProgressChanged
{
  private IContactRepository repository;
  ...
  private void LoadContacts()
  {
    Task.Factory.StartNew(() => this.contactRepository.LoadWithProgress(this))
      .ContinueWith(o => this.UseResult(o));
  }
}

你会注意到,我将ViewModel传递给存储库方法作为INotifyProgressChanged,这就是我想确保我没有做错的地方。

我在这里的想法是,为了报告进度,实际执行工作的方法(即存储库方法)需要访问INotifyProgressChanged接口,以便报告进度,最终更新视图。下面快速查看一下存储库方法(在本例中缩短了):

public class ContactRepository : IContactRepository
{
  ...
  public IEnumberable<Contact> LoadWithProgress(INotifyProgressChanged indicator)
  {
    var resultSet = new List<Contact>();
    var query = ... // This is a LINQ to Entities query
    // Set the maximum to the number of "pages" that will be iterated
    indicator.Maximum = this.GetNumberOfPages(query.Count(), this.pageSize);
    for (int i = 0; i < indicator.Maximum; i++)
    {
      resultSet.AddRange(query.Skip(i * this.pageSize).Take(this.pageSize));
      indicator.Progress += 1; // As each "chunk" is loaded, progress is updated
    }
    // The complete list is returned after all "chunks" are loaded
    return resultSet.AsReadOnly();
  }
}

这就是存储库最终通过ViewModel向视图报告进度的方式。这是正确的方法吗?我是否正确使用了TPL,是否违反了任何主要规则等?这个解决方案是有效的,进度是按照预期报告的,我只是想确保我没有让自己失败。

从TPL任务向WPF视图报告进度的适当方法是什么?

这样做的"规定"方式是将TaskScheduler实例从TaskSheduler::FromCurrentSynchronizationContext传递到ContinueWith,您希望确保在WPF调度程序线程上执行。

例如:

 public void DoSomeLongRunningOperation()
 {
    // this is called from the WPF dispatcher thread
    Task.Factory.StartNew(() =>
    {
        // this will execute on a thread pool thread
    })
    .ContinueWith(t =>
    {
        // this will execute back on the WPF dispatcher thread
    },
    TaskScheduler.FromCurrentSynchronizationContext());
 }

我建议你避免从后台线程更新数据绑定属性。

为了解决这个问题,你可以让你的后台任务发布一个UI任务来完成它的更新,或者(甚至更好),使用IProgress<T>/Progress<T>系统,在基于任务的异步模式概述文档中描述。

IProgress<T>方法很好,因为它将后台任务与ViewModel更新分开。然而,它也有一些缺点(在后台任务和更新之间共享数据;和处理异常从更新);我希望这些问题能在Async CTP正式发布之前得到解决。

我不认为你应该直接从后台线程直接更新ViewModel。我编写了很多Silverlight应用程序,我喜欢使用MVVMLight工具包来实现MVVM模式。

在MVVM中,有时你需要让ViewModel"影响"视图,这是你不能直接做的,因为ViewModel没有对视图的引用。在这些场景中,MVVMLight有一个Messenger类,允许我们在视图中"监听"消息,并从ViewModel中"通知"消息。

我认为您应该在您的场景中使用Messenger类。

下面是一个示例代码链接:http://chriskoenig.net/2010/07/05/mvvm-light-messaging/