【问题标题】:What Is the appropriate way to report progress to a WPF view from a TPL task?从 TPL 任务向 WPF 视图报告进度的适当方法是什么?
【发布时间】:2011-07-13 19:38:41
【问题描述】:

我正在尝试以小“块”或页面从数据库中读取大量行并将进度报告给用户;即,如果我正在加载 100 个“块”,则在加载每个块时报告进度。

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

这是我为向 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 向 View 报告进度的方式。这是正确的方法吗?我是否正确使用了 TPL,违反了任何主要规则等?此解决方案正在运行,正在按预期报告进度,我只是想确保我没有为失败做好准备。

【问题讨论】:

    标签: c# c#-4.0 progress-bar task-parallel-library


    【解决方案1】:

    执行此操作的“规定”方式是将TaskScheduler 实例从TaskSheduler::FromCurrentSynchronizationContext 传递到您希望确保在WPF 调度程序线程上执行的ContinueWith

    例如:

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

    【讨论】:

      【解决方案2】:

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

      要解决这个问题,您可以让您的后台任务post a UI task to do its update,或者(甚至更好)使用Task-Based Asynchronous Pattern Overview 文档中描述的IProgress&lt;T&gt;/Progress&lt;T&gt; 系统。

      IProgress&lt;T&gt; 方法很好,因为它将后台任务与 ViewModel 更新分开。但是,它有some drawbacks(在后台任务和更新之间共享数据;以及处理来自更新的异常);我希望在 Async CTP 正式发布之前解决这些问题。

      【讨论】:

        【解决方案3】:

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

        在 MVVM 中,有时您需要让 ViewModel “影响” View,但您不能直接这样做,因为 ViewModel 没有对 View 的引用。在这些场景中,MVVMLight 有一个 Messenger 类,它允许我们“侦听”视图中的消息并从视图模型“通知”。

        我相信你应该在你的场景中使用 Messenger 类。

        这里是示例代码的链接:http://chriskoenig.net/2010/07/05/mvvm-light-messaging/

        【讨论】:

        • 我支持这个。您可以向 UI 发送 NotificationMessage 消息进行更新。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多