通用后台工作者
告别手动装箱/类型转换!在此后台工作者中使用泛型类型参数。提供 C# 和 VB.NET 的源代码。
目录
- 动机和背景 / 这不是什么
- 数据和 BackgroundWorker
- '泛型化' BackgroundWorker (
BackgroundWorker<TArgument, TProgress, TResult>
) - 使用方式 / 对原始版本的更改
- 额外奖励! (
BackgroundWorker<T>
) - 演示
- 结论
- 参考文献
- 历史
动机和背景
如果您从未使用过 `System.ComponentModel.BackgroundWorker` 组件,它是一个极其有用的组件,可以轻松地在另一个线程上处理操作,但它有一个主要缺点。所有进出它的数据都以 `System.Object` 类型传递。当 .NET Framework 2.0 于2005年11月发布时,引入了泛型。微软在这里 很好地解释了泛型的优势。
"泛型解决了公共语言运行库和 C# [以及 VB.NET] 早期版本中的一个限制,在这些版本中,通过将类型转换为通用基类型 `Object` 和从通用基类型转换回来来实现泛化。通过创建泛型类,您可以创建一个在编译时类型安全的集合。"
使用 `BackgroundWorker` 组件时,此限制仍然存在。主要原因是它是一个组件。由于组件可以在设计时使用,因此它们必须与设计器配合良好,而设计器实际上无法处理泛型!在我看来,使用泛型带来的好处远远超过了与设计器兼容的好处。毕竟,组件不是可见控件,因此它在设计器中的位置充其量是可疑的。
在我目前的一个项目中,我在不同阶段需要进行几项后台操作。我认为 `BackgroundWorker` 是最简单的方法之一,所以我开始用它进行编码。当我处理到第三个时,我注意到我不得不反复地将类型转换回正确的数据类型——我头顶上的一个灯泡亮了起来 ……一个明显的泛型候选对象。本文及配套的类库就是为此而生。
这不是什么
本文不是对后台工作者实现的深入研究。我将来可能会写一篇关于这个主题的文章,但本文纯粹侧重于泛型方面。
数据和 BackgroundWorker
在框架的后台工作者中,有三个地方可以将数据暴露给外部世界。当我们调用 `RunWorkerAsync(object argument)` 时,`argument` 参数通过 `DoWorkEventArgs.Argument` 属性传递到 `DoWork` 事件(在工作线程上运行)。`DoWorkEventArgs` 还有一个 `Result` 属性,它也是 `object` 类型,该属性通过 `RunWorkerCompletedEventArgs.Result` 属性传递到 `RunWorkerCompleted` 事件(在原始线程上)。然而,在工作者完成之前,我们可以选择随时调用 `ReportProgress(int percentProgress, object userState)`,这会引发 `ProgressChanged` 事件,`userState` 可在 `ProgressChangedEventArgs.UserState`(也是 `object` 类型)中获取。
'泛型化' 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 文章文本和解决方案下载。