65.9K
CodeProject 正在变化。 阅读更多。
Home

与 Whidbey 的一次邂逅:Windows 窗体中的后台操作

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (20投票s)

2004 年 11 月 11 日

6分钟阅读

viewsIcon

89788

downloadIcon

986

本文介绍了如何在 Whidbey 中使用 Background Worker 组件来异步执行耗时操作。还介绍了如何支持进度报告和取消操作。

Sample Image - backgroundworker.jpg

引言

Whidbey 带来了许多让开发人员喜爱的激动人心的新功能。其中最热门的新特性之一就是 Background Worker 组件。如果您曾经编写过在 Windows Forms 中执行异步操作的代码,您就会同意,我们需要处理许多细微之处——其中最突出的一点是,我们不应该从工作线程访问 UI 控件。多线程编程时,bug 常见且难以追踪。

Background Worker 组件在实现异步操作时,可以为我们省去所有这些麻烦。它提供了一种便捷的基于事件的机制,可以轻松调用耗时方法。本文将探讨如何在简单的 Windows Forms 项目中使用该组件。

一点历史

在 Whidbey 发布之前,处理耗时操作最常见和最简单的方法是使用异步委托调用。这基本上涉及调用委托的 BeginInvoke 方法。调用 BeginInvoke 会将方法执行加入队列,以便从系统线程池中运行,并立即返回,而无需等待方法执行完成。这确保了调用者不必等待方法完成其任务。

异步调用方法后,您通常会使用 Control.InvokeRequired 属性和 Control.BeginInvoke 方法来促进 UI-工作线程之间的通信。这通常是我们向 UI 线程发送进度状态以更新 UI 中的进度条/状态栏控件的方式。

这种方法的缺点是,我们必须跟踪我们所在的线程,并确保不在工作线程中调用任何 UI 控件成员。通常,此类代码的结构包含以下代码片段。

if (this.InvokeRequired)
{
    this.BeginInvoke(…the target…);
}
else
{
    // proceed performing tasks
}

在 UI-工作线程通信存在的任何地方添加此代码片段,都会使代码难以阅读和维护。一些 .NET 设计模式可以简化此代码;但所有这些对于一个简单的要求来说都过于复杂——您只是想运行一个耗时过程,并告知用户进度。

Whidbey 的方式

进入 Whidbey。引入 BackgroundWorker 组件,它将为我们节省时间和精力。

BackgroundWorker 组件可以从工具箱的组件选项卡拖放到您的窗体上。它会在组件托盘中找到位置,其属性将在属性窗口中可用。

BackgroundWorker 组件公开了三个您会感兴趣的事件和三个方法。以下是其流程概述。

  1. 您调用 BackgroundWorker.RunWorkerAsync 方法,根据需要传递任何参数。这将引发 DoWork 事件。
  2. DoWork 处理程序将包含耗时代码或对耗时方法的调用。您可以通过 DoWork 事件参数检索传递的任何参数。
  3. 当耗时方法完成执行时,您将结果设置到 DoWork 事件参数的 Result 属性。
  4. 将引发 RunWorkerCompleted 事件。
  5. RunWorkerCompleted 事件处理程序中,您将执行后续操作。

详细的分步说明如下。

异步运行进程

RunWorkerAsync 方法异步启动操作。它接受一个可选的 object 参数,该参数可用于将初始化值传递给耗时方法。

private void startAsyncButton_Click(System.Object sender, System.EventArgs e)
{
    // Start the asynchronous operation.
    backgroundWorker1.RunWorkerAsync(someArgument);
}

RunWorkerAsync 方法会引发 DoWork 事件,您应该在该事件的处理程序中编写耗时代码。此事件有一个 DoWorkEventArgs 参数,该参数有两个属性——ArgumentResultArgument 属性的值来自调用 RunWorkerAsync 时设置的可选参数。Result 属性用于设置我们操作的最终结果,该结果将在处理 RunWorkerCompleted 事件时检索。

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = sender as BackgroundWorker;
    e.Result = LongRunningMethod((int)e.Argument, worker, e);
}

我们不是直接引用 backgroundWorker1 实例,而是通过 sender 对象获取对其的引用。这确保了当我们在窗体中有多个 BackgroundWorker 组件实例时,我们会获得实际引发事件的实例。

我们需要将 BackgroundWorker 实例的引用以及事件参数传递给耗时方法,以方便取消和报告进度。

完成后检索状态

RunWorkerCompleted 事件在三种不同情况下会被引发;要么是后台操作已完成、被取消,要么是它抛出了异常。RunWorkerCompletedEventArgs 类包含 ErrorCancelledResult 属性,可用于检索操作的状态及其最终结果。

private void backgroundWorker1_RunWorkerCompleted(object sender, 
                                    RunWorkerCompletedEventArgs e)
{
    // First, handle the case where an exception was thrown.
    if (e.Error != null)
    {
        // Show the error message
    }
    else if (e.Cancelled)
    {
        // Handle the case where the user cancelled the operation.
    }
    else
    {
        // Operation completed successfully. 
        // So display the result.
    }
    // Do post completion operations, like enabling the controls etc.
}

向 UI 通知进度

为了支持进度报告,我们首先将 BackgroundWorker.WorkerReportsProgress 属性设置为 true,然后将事件处理程序附加到 BackgruondWorker.ProgressChanged 事件。ProgressChangedEventArgs 定义了 ProgressPercentage 属性,我们可以使用它来设置 UI 中进度条的值。

long LongRunningMethod(int someArgument, BackgroundWorker worker, DoWorkEventArgs e)
{
    // Do something very slow

    // Calculate and report the progress as a 
    // percentage of the total task
    int percentComplete = (currentValue * 100) / maxValue;
    worker.ReportProgress(percentComplete);

    // Return the result
    return result;
}

private void backgroundWorker1_ProgressChanged(object sender, 
                                  ProgressChangedEventArgs e)
{
    progressBar1.Value = e.ProgressPercentage;
}

请注意,如果您的耗时方法本身是一个递归操作,那么您需要确保您计算的进度百分比是针对整个任务而不是当前迭代。代码将采取以下形式。

long LongRunningMethod(int n, BackgroundWorker worker, DoWorkEventArgs e)
{
    // Do something very slow

    // Calculate and report the progress as a 
    // percentage of the total task. Since this is
    // a recursive operation, check whether the 
    // percent just calculated is the highest - if yes,
    // it directly represents the percent of the total
    // task completed.
    int percentComplete = (currentValue * 100) / maxValue;
    if (percentComplete > highestPercentageReached)
    {
            highestPercentageReached = percentComplete;
            worker.ReportProgress(percentComplete);
    }

    // Return the result
    return result;
}

支持取消

要取消后台工作进程,我们首先应将 BackgroundWorker.WorkerSupportsCancellation 属性设置为 true。当我们想取消操作时,我们调用 BackgroundWorker.CancelAsync 方法。这将把 BackgroundWorker.CancellationPending 属性设置为 true。在工作线程中,您必须定期检查此值是否为 true 以便取消。如果为 true,则将 DoWorkEventArgs.Cancel 属性设置为 true,并跳过执行方法。一个简单的 if 块在这里就足够了。

private void cancelAsyncButton_Click(System.Object sender, System.EventArgs e)
{
    // Cancel the asynchronous operation.
    backgroundWorker1.CancelAsync();

    // Do UI updates like disabling the Cancel button etc.
}

long LongRunningMethod(int n, BackgroundWorker worker, DoWorkEventArgs e)
{
    if (worker.CancellationPending)
    {
        e.Cancel = true;
    }
    else
    {
        // The operation has not been cancelled yet.
        // So proceed doing something very slow.

        // Calculate and report the progress as a 

        // percentage of the total task
        int percentComplete = (currentValue * 100) / 
            maxValue;worker.ReportProgress(percentComplete);
    
        // Return the result
        return result;
    }
}

看,无论我们是否在正确的线程上,都没有麻烦。我们只需将一些处理程序附加到组件公开的事件上,然后专注于我们的任务。这样,我们就建立了一个清晰的实现,可以异步调用方法,获取其进度的频繁更新,支持取消该方法,最后处理该方法的完成。

总结

让我们回顾一下使用 BackgroundWorker 组件实现异步操作所涉及的步骤。

  1. BackgroundWorker 组件从工具箱拖放到窗体上。
  2. 根据需要为 DoWorkProgressChangedRunWorkerCompleted 事件添加处理程序。
  3. 让您的耗时过程定期检查 BackgroundWorker.CancellationPending 属性的状态。
  4. 让您的进程计算已完成任务的百分比,并调用 BackgroundWorker.ReportProgress 方法传递百分比。
  5. 处理 BackgroundWorker.RunWorkerCompleted 事件以检查此过程是成功完成、被取消,还是抛出了异常。

祝好!

关于代码

附带的代码是一个适用于 Windows Forms 项目的 Visual Studio .NET 2005 Beta 解决方案,它演示了带有进度通知和取消支持的 Background Worker 组件。

历史

  • 2004 年 11 月 11 日:刚刚发布。
© . All rights reserved.