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

通用后台工作者

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (32投票s)

2009年9月5日

CPOL

6分钟阅读

viewsIcon

103182

downloadIcon

5634

告别手动装箱/类型转换!在此后台工作者中使用泛型类型参数。提供 C# 和 VB.NET 的源代码。

screenshot.png

目录

动机和背景

如果您从未使用过 `System.ComponentModel.BackgroundWorker` 组件,它是一个极其有用的组件,可以轻松地在另一个线程上处理操作,但它有一个主要缺点。所有进出它的数据都以 `System.Object` 类型传递。当 .NET Framework 2.0 于2005年11月发布时,引入了泛型。微软在这里 很好地解释了泛型的优势

"泛型解决了公共语言运行库和 C# [以及 VB.NET] 早期版本中的一个限制,在这些版本中,通过将类型转换为通用基类型 `Object` 和从通用基类型转换回来来实现泛化。通过创建泛型类,您可以创建一个在编译时类型安全的集合。"

使用 `BackgroundWorker` 组件时,此限制仍然存在。主要原因是它是一个组件。由于组件可以在设计时使用,因此它们必须与设计器配合良好,而设计器实际上无法处理泛型!在我看来,使用泛型带来的好处远远超过了与设计器兼容的好处。毕竟,组件不是可见控件,因此它在设计器中的位置充其量是可疑的。

在我目前的一个项目中,我在不同阶段需要进行几项后台操作。我认为 `BackgroundWorker` 是最简单的方法之一,所以我开始用它进行编码。当我处理到第三个时,我注意到我不得不反复地将类型转换回正确的数据类型——我头顶上的一个灯泡亮了起来 lightbulb.png……一个明显的泛型候选对象。本文及配套的类库就是为此而生。

这不是什么

本文不是对后台工作者实现的深入研究。我将来可能会写一篇关于这个主题的文章,但本文纯粹侧重于泛型方面。

数据和 BackgroundWorker

在框架的后台工作者中,有三个地方可以将数据暴露给外部世界。当我们调用 `RunWorkerAsync(object argument)` 时,`argument` 参数通过 `DoWorkEventArgs.Argument` 属性传递到 `DoWork` 事件(在工作线程上运行)。`DoWorkEventArgs` 还有一个 `Result` 属性,它也是 `object` 类型,该属性通过 `RunWorkerCompletedEventArgs.Result` 属性传递到 `RunWorkerCompleted` 事件(在原始线程上)。然而,在工作者完成之前,我们可以选择随时调用 `ReportProgress(int percentProgress, object userState)`,这会引发 `ProgressChanged` 事件,`userState` 可在 `ProgressChangedEventArgs.UserState`(也是 `object` 类型)中获取。

dataflow.png

'泛型化' BackgroundWorker

所以,第一步是创建一个接受三个泛型类型参数的新类。

// C#
namespace System.ComponentModel.Custom.Generic
{
    public class BackgroundWorker<TArgument, TProgress, TResult>
    {
    }
}

VB

' VB
Public Class BackgroundWorker(Of TArgument, TProgress, TResult)
End Class

尽管这个实现(在撰写本文时)不是(也不能是)一个组件,但我坚持使用与原始版本相同的基本命名空间,以便我知道在哪里找到它,但附加了 `.Custom.Generic`,所以希望它不会与微软将来可能使用的任何命名空间冲突。如果您不喜欢它在这里,请随时更改!

现在,我们需要一个接受 `TArgument` 而不是 `object` 的 `RunWorkerAsync` 方法。

// C#
public bool RunWorkerAsync(TArgument argument)
{
}

VB

' VB
Public Function RunWorkerAsync(ByVal argument As TArgument) As Boolean
End Function

一旦线程启动,我们需要引发一个具有泛型参数的 `DoWork` 事件,因此需要一个新的类以及该事件。

// C#
public class DoWorkEventArgs<TArgument, TResult> : CancelEventArgs
{
    public DoWorkEventArgs(TArgument argument)
    {
        Argument = argument;
    }
 
    public TArgument Argument
    {
        get;
        private set;
    }
    public TResult Result
    {
        get;
        set;
    }
}
// C#
public event EventHandler<DoWorkEventArgs<TArgument, TResult>> DoWork;

VB

' VB
Public Class DoWorkEventArgs(Of TArgument, TResult)
    Inherits System.ComponentModel.CancelEventArgs

    Private _Argument As TArgument
    Private _Result As TResult

    Public Sub New(ByVal argument As TArgument)
        _Argument = argument
    End Sub

    Public Property Argument() As TArgument
        Get
            Return _Argument
        End Get
        Private Set(ByVal value As TArgument)
            _Argument = value
        End Set
    End Property

    Public Property Result() As TResult
        Get
            Return _Result
        End Get
        Set(ByVal value As TResult)
            _Result = value
        End Set
    End Property

End Class
' VB
Public Event DoWork As EventHandler(Of DoWorkEventArgs(Of TArgument, TResult))

在 `DoWork` 事件处理程序中,我们需要能够报告进度,因此我们需要一个合适的方法来调用,一个名为的新的事件参数类,当然还有该事件。

// C#
public bool ReportProgress(int percentProgress, TProgress userState)
{
}
// C#
public class ProgressChangedEventArgs<T> : EventArgs
{
    public ProgressChangedEventArgs(int progressPercentage, T userState)
    {
        ProgressPercentage = progressPercentage;
        UserState = userState;
    }
    
    public int ProgressPercentage
    {
        get;
        private set;
    }
    public T UserState
    {
        get;
        private set;
    }
}
// C#
public event EventHandler<ProgressChangedEventArgs<TProgress>> ProgressChanged;

VB

' VB
Public Function ReportProgress(ByVal percentProgress As Int32, _
                ByVal userState As TProgress) As Boolean
End Function
' VB
Public Class ProgressChangedEventArgs(Of T)
    Inherits System.EventArgs

    Private _ProgressPercentage As Int32
    Private _UserState As T

    Public Sub New(ByVal progressPercentage As Int32, ByVal userState As T)
        _ProgressPercentage = progressPercentage
        _UserState = userState
    End Sub

    Public Property ProgressPercentage() As Int32
        Get
            Return _ProgressPercentage
        End Get
        Private Set(ByVal value As Int32)
            _ProgressPercentage = value
        End Set
    End Property

    Public Property UserState() As T
        Get
            Return _UserState
        End Get
        Private Set(ByVal value As T)
            _UserState = value
        End Set
    End Property
End Class
' VB
Public Event ProgressChanged As EventHandler(Of ProgressChangedEventArgs(Of TProgress))

最后,我们需要返回我们的结果。为此,我们需要一个名为的新的事件参数类以及该事件。

// C#
public sealed class RunWorkerCompletedEventArgs<T> : EventArgs
{
    public RunWorkerCompletedEventArgs(T result, Exception error, bool cancelled)
    {
        Result = result;
        Error = error;
        Cancelled = cancelled;
    }
    
    public bool Cancelled
    {
        get;
        private set;
    }
    public Exception Error
    {
        get;
        private set;
    }
    public T Result
    {
        get;
        private set;
    }
}
// C#
public event EventHandler<RunWorkerCompletedEventArgs<TResult>> RunWorkerCompleted;

VB

' VB
Public NotInheritable Class RunWorkerCompletedEventArgs(Of T)
    Inherits System.EventArgs

    Private _Cancelled As Boolean
    Private _Err As Exception
    Private _Result As T

    Public Sub New(ByVal result As T, ByVal err As Exception, ByVal cancelled As Boolean)
        _Cancelled = cancelled
        _Err = err
        _Result = result
    End Sub

    Public Shared Widening Operator CType(ByVal e As RunWorkerCompletedEventArgs(Of T)) _
                                          As AsyncCompletedEventArgs
        Return New AsyncCompletedEventArgs(e.Err, e.Cancelled, e.Result)
    End Operator

    Public Property Cancelled() As Boolean
        Get
            Return _Cancelled
        End Get
        Private Set(ByVal value As Boolean)
            _Cancelled = value
        End Set
    End Property

    Public Property Err() As Exception
        Get
            Return _Err
        End Get
        Private Set(ByVal value As Exception)
            _Err = value
        End Set
    End Property

    Public Property Result() As T
        Get
            Return _Result
        End Get
        Private Set(ByVal value As T)
            _Result = value
        End Set
    End Property

End Class
' VB
Public Event RunWorkerCompleted As EventHandler(Of RunWorkerCompletedEventArgs(Of TResult))

坦白时间!

内部并非一切都是泛型的。我使用了 `System.ComponentModel.AsyncOperation` 类来处理跨线程数据的封送,其方法是 `Post` 和 `PostOperationCompleted`。这两个方法都使用一个委托 `System.Threading.SendOrPostCallback`,该委托接受一个 `object` 作为参数。`ProgressChangedEventArgs` 和 `RunWorkerCompletedEventArgs` 都被此委托装箱,并在调用的方法中再次拆箱。与现有情况相比仍有改进,并且对类使用者来说是不可见的。

使用方式 / 对原始版本的更改

此后台工作者可以与原始版本完全相同地使用,但具有泛型的优势。所有相同的属性、方法和事件都存在,没有额外的。正如我之前解释过的,您无法将其拖放到设计器中,但在代码中实例化类并订阅所需的事件非常简单。不过,我做了一些更改,因为我对原始版本有一些不喜欢的地方。

  • `RunWorkerAsync` 返回一个 `bool` 而不是 `void`。如果工作者正在忙碌,它返回 `false` 而不是抛出异常。
  • `CancelAsync` 返回一个 `bool` 而不是 `void`。如果工作者不支持取消,它返回 `false` 而不是抛出异常。
  • `ReportProgress` 返回一个 `bool` 而不是 `void`。如果工作者不报告进度,它返回 `false` 而不是抛出异常。
  • `WorkerReportsProgress` 和 `WorkerSupportsCancellation` 属性默认为 `true`,而不是 `false`。

在我的实现中,不应该抛出任何异常。如果在 `DoWork` 事件处理程序中抛出了异常,它将被捕获并传递到 `RunWorkerCompletedEventArgs` 的 `Error` 属性。

额外奖励!

很多时候,您在后台工作者中传入和传出的数据类型是相同的。为了在不需要声明三个相同的类型参数的情况下实现这一点,我额外包含了 `BackgroundWorker<T>` 和 `DoWorkEventArgs<T>`!

演示

该演示模拟了一个文件操作。它使用了一个 `BackgroundWorker<string[], string, List<FileData>>`。第一个参数(`TArgument`)是要处理的文件名数组。第二个(`TProgress`)是在报告进度时正在处理的文件名。第三个是 `FileData` 的简单类的 `List`,该类保存文件名和处理时间戳。

// C#
public class FileData
{
    public FileData(string filename, DateTime timestamp)
    {
        Filename = filename;
        Timestamp = timestamp;
    }
 
    public string Filename
    {
        get;
        private set;
    }
    public DateTime Timestamp
    {
        get;
        private set;
    }
 
    public override string ToString()
    {
        return string.Format("File: {0} Timestamp: {1}", Filename, Timestamp.Ticks);
    }
}

VB

' VB
Public Class FileData

    Private _Filename As String
    Private _Timestamp As DateTime

    Public Sub New(ByVal filename As String, ByVal timestamp As DateTime)
        _Filename = filename
        _Timestamp = timestamp
    End Sub

    Public Property Filename() As String
        Get
            Return _Filename
        End Get
        Private Set(ByVal value As String)
            _Filename = value
        End Set
    End Property

    Public Property Timestamp() As DateTime
        Get
            Return _Timestamp
        End Get
        Private Set(ByVal value As DateTime)
            _Timestamp = value
        End Set
    End Property

    Public Overrides Function ToString() As String
        Return String.Format("File: {0} Timestamp: {1}", Filename, Timestamp.Ticks)
    End Function

End Class

它通过在单击“开始”按钮时将 `files` 的 `string` 数组传递给 `RunWorkerAsync(TArgument)` 方法开始。在 `DoWork` 事件处理程序中,它迭代这个数组。在数组的每次迭代中,工作者都会报告进度。`ProgressChangedEventArgs.ProgressPercentage` 用于更新 `ProgressBar`,而 `ProgressChangedEventArgs.UserState`(一个 `string`,即 `TProgress`)用于更新 `Label` 的 `Text`,以便我们可以看到正在处理哪个文件。

// C#
private void fileWorker_DoWork(object sender, 
        DoWorkEventArgs<string[], List<FileData>> e)
{
    // We're not on the UI thread here so we can't update UI controls directly
    int progress = 0;
    e.Result = new List<filedata>(e.Argument.Length);
    foreach (string file in e.Argument)
    {
        if (fileWorker.CancellationPending)
        {
            e.Cancel = true;
            return;
        }
        fileWorker.ReportProgress(progress, file);
        Thread.Sleep(50);
        e.Result.Add(new FileData(file, DateTime.Now));
        progress += 2;
    }
    fileWorker.ReportProgress(progress, string.Empty);
}

private void fileWorker_ProgressChanged(object sender, 
                        ProgressChangedEventArgs<string> e)
{
    // Back on the UI thread for this
    labelProgress.Text = e.UserState;
    progressBar.Value = e.ProgressPercentage;
}

VB

' VB
Public Sub fileWorker_DoWorkHandler _
    (ByVal sender As Object, ByVal e As DoWorkEventArgs(Of String(), List(Of FileData)))
    // We're not on the UI thread here so we can't update UI controls directly
    Dim progress As Int32 = 0
    e.Result = New List(Of FileData)(e.Argument.Length)
    For Each file As String In e.Argument
        If fileWorker.CancellationPending Then
            e.Cancel = True
            Return
        End If
        fileWorker.ReportProgress(progress, file)
        Thread.Sleep(50)
        e.Result.Add(New FileData(file, DateTime.Now))
        progress += 2
    Next
    fileWorker.ReportProgress(progress, String.Empty)
End Sub

Public Sub fileWorker_ProgressChangedHandler _
    (ByVal sender As Object, ByVal e As ProgressChangedEventArgs(Of String))
    // Back on the UI thread for this
    labelProgress.Text = e.UserState
    progressBar.Value = e.ProgressPercentage
End Sub

有50个文件,所以我每次将进度增加2,这样在完成时就是100。注意,我们还检查 `CancellationPending`。如果单击“取消”按钮,它会调用我们的 `CancelAsync` 方法,该方法会设置此属性,因此我们设置 `DoWorkEventArgs.Cancel` 属性并退出。我包含了一个50毫秒的延迟,以便我们可以看到工作者正在工作!

在上面的代码中,我们还一直在将文件数据添加到 `DoWorkEventArgs.Result`(`TResult`)。这会自动传递到 `RunWorkerCompleted` 事件处理程序,其中 `List`(`TResult`)被用作 `ListBox` 的数据源。

// C#
private void fileWorker_RunWorkerCompleted(object sender, 
        RunWorkerCompletedEventArgs<List<FileData>> e)
{
    if (e.Cancelled)
    {
        labelProgress.Text = "Cancelled";
        progressBar.Value = 0;
    }
    else
        labelProgress.Text = "Done!";
    listBox.DataSource = e.Result;
    // ...
}

VB

' VB
Public Sub fileWorker_RunWorkerCompletedHandler _
    (ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs(Of List(Of FileData)))
    If e.Cancelled Then
        labelProgress.Text = "Cancelled"
        progressBar.Value = 0
    Else
        labelProgress.Text = "Done!"
    End If
    listBox.DataSource = e.Result
    ' ...
End Sub

结论

如果您像我一样讨厌类型转换/拆箱,并且想利用后台工作者的简洁性,那么您应该会发现这个类库很有用——我现在确信我将经常使用它。如果您发现任何错误或问题,请在下方的论坛中告知我。

参考文献

后台工作者实现基于一个已在此处 发布的非泛型版本

历史

  • 2009年9月5日:版本1。
  • 2009年9月7日:版本2,添加了 VB.NET 文章文本和解决方案下载。
© . All rights reserved.