在Task.Run或TaskFactory.StartNew仍在工作时更新UI

时间:2014-07-02 10:59:26

标签: c# .net wpf task-parallel-library

所以我决定在WPF中重写我的邮件客户端,因为我认为这是关于我从Windows Forms继续运行的时间(我仍然喜欢它),但我遇到了一些问题。

我在我的Windows窗体应用程序中使用BackgroundWorker来执行操作,并在fore {I worker.ReportProgress(currentProgress);中使用,这样我就可以在背景中完成更新UI,这很棒。< / p>

但是刚刚开始一个新的WPF项目之后,我注意到工具箱中没有BackgroundWorker(对于WPF应用程序)所以我去网上搜索,发现有些人在使用时更新了UI WPF BackgroundWorker。所以这让我觉得在WPF应用程序中使用BackgroundWorker有点hacky - 我不希望这样。

在同一页面上,其他用户将其引荐至this页面,告诉他们在WPF中使用Task.Run代替BackgroundWorker。在查看Task.Run文档后,我立即看到它是如何有用的,但我确实有一个问题。我没有看到“报告进度”的方法或者在完成任务时更新UI。我只看到如何运行任务和“await”它;只留下一个选项 - 在长时间运行的任务完成后更新UI。

如果Task.Run / TaskFactory.StartNew仍然有效,我们如何更新WPF桌面应用的用户界面?

1 个答案:

答案 0 :(得分:6)

如果您愿意,可以坚持使用BackroundWorker。虽然这是非常老派,但没有什么真正的hacky。正如其他人所说,如果你无法在工具箱中找到它,你可以直接从代码中声明并初始化它(不要忘记using System.ComponentModel;指令)。

Stephen Cleary在BackgroundWorker vs Task上发布了一系列精彩的博文,强调了每种方法的差异和局限性。如果你只是在围栏上或只是好奇,那绝对值得一读。

http://blog.stephencleary.com/2013/05/taskrun-vs-backgroundworker-intro.html

如果您决定沿着Task + async/await路线走下去,那么您应该记住一些与进度报告特别相关的事情。

通常,您的目标应该是让await Task.Run封装尽可能少的有意义的工作量。然后,您的async方法的其余部分将在调度程序SynchronizationContext上执行(假设它是在调度程序线程上启动的),并且可以直接更新UI,如下所示:

List<object> items = GetItemsToProcess();
int doneSoFar = 0;

foreach (var item in items)
{
    await Task.Run(() => SomeCpuIntensiveWorkAsync(item));

    doneSoFar++;

    int progressPercentage = (int)((double)doneSoFar / items.Count * 100);

    // Update the UI.
    this.ProgressBar.Value = progressPercentage;
}

这是在async世界中实施进度报告的最简单方法。

我唯一想象的是,当您处理非常大量项目时,会在您传递给Task.Run的代理机构内部报告进度。每个项目的处理需要很短的时间(我们每秒说10,000个项目作为粗略的指导)。在这种情况下,创建大量极细粒度Taskawait将会带来巨大的开销。如果是这种情况,您可以回退到.NET 4中引入的进度报告机制:Progress<T> / IProgress<T>。它与BackgroundWorker报告进展的方式(因为它依赖于事件)非常相似,并且在决定何时回发给调度程序上下文时提供了更多的灵活性。

public async Task DoWorkAsync()
{
    // Let's assume we're on the UI thread now.
    // Dummy up some items to process.
    List<object> items = GetItemsToProcess();

    // Wire up progress reporting.
    // Creating a new instance of Progress
    // will capture the SynchronizationContext
    // any any calls to IProgress.Report
    // will be posted to that context.
    Progress<int> progress = new Progress<int>();

    progress.ProgressChanged += (sender, progressPercentage) =>
    {
        // This callback will run on the thread which
        // created the Progress<int> instance.
        // You can update your UI here.
        this.ProgressBar.Value = progressPercentage;
    };

    await Task.Run(() => this.LongRunningCpuBoundOperation(items, progress));
}

private void LongRunningCpuBoundOperation(List<object> items, IProgress<int> progress)
{
    int doneSoFar = 0;
    int lastReportedProgress = -1;

    foreach (var item in items)
    {
        // Process item.
        Thread.Sleep(1);

        // Calculate and report progress.
        doneSoFar++;

        var progressPercentage = (int)((double)doneSoFar / items.Count * 100);

        // Only post back to the dispatcher SynchronizationContext
        // if the progress percentage actually changed.
        if (progressPercentage != lastReportedProgress)
        {
            // Note that progress is IProgress<int>,
            // not Progress<int>. This is important
            // because Progress<int> implements
            // IProgress<int>.Report explicitly.
            progress.Report(progressPercentage);

            lastReportedProgress = progressPercentage;
        }
    }
}