Table of Contents
Motivation and Background
In case you've never used it, the System.ComponentModel.BackgroundWorker
is an extremely useful component that makes it easy to process operations on a different thread, but it has one major drawback. All data in and out of it is passed as System.Object
. When .NET Framework 2.0 was released in November 2005, Generics were introduced. The advantage of Generics is well explained by Microsoft here:
"Generics provide the solution to a limitation in earlier versions of the Common Language Runtime and the C# language [and VB.NET] in which generalization is accomplished by casting types to and from the universal base type Object
. By creating a generic class, you can create a collection that is type-safe at compile-time."
With the BackgroundWorker
component, this limitation still exists. The primary reason is that it is a Component. As components can be used at design time, they have to play nice with the designer and there is no real way for the designer to cope with Generics! In my opinion, the benefits provided by using Generics greatly outweigh those of being designer compatible. After all, a component is not a visible control, so its place in a designer is questionable at best anyway.
In a project I'm currently working on, I had the need for several background operations at various stages. I figured a BackgroundWorker
was as easy as any other way, so I started coding using it. By the time I got to the third one, I noticed that I was repeatedly having to unbox/cast to the correct data type - a light bulb just above my head illuminated ... an obvious candidate for Generics. This article and the accompanying class library is the result.
What This Isn't
This article isn't a study of the implementation of a background worker. I may in future do an article on that subject, but this article focuses purely on the generic aspect.
Data and the BackgroundWorker
With the framework's background worker, there are three places where data gets exposed to the outside world. When we call RunWorkerAsync(object argument)
, the argument parameter is passed through to the DoWork
event (which runs on the worker thread) via the DoWorkEventArgs.Argument
property. DoWorkEventArgs
also has a Result
property that is also of type object
, which gets passed to the RunWorkerCompleted
event (on the original thread) via the RunWorkerCompletedEventArgs.Result
property. Before the worker completes, however, we have the option of calling ReportProgress(int percentProgress, object userState)
at any point(s) which raises the ProgressChanged
event, and the userState
is available in ProgressChangedEventArgs.UserState
(also an object
).
'Genericifying' the BackgroundWorker
So, the first thing is to create a new class that takes three generic type parameters.
namespace System.ComponentModel.Custom.Generic
{
public class BackgroundWorker<TArgument, TProgress, TResult>
{
}
}
VB:
Public Class BackgroundWorker(Of TArgument, TProgress, TResult)
End Class
Although this implementation is not (and can't be at the time of writing) a component, I've stuck with the same base namespace as the original so I know where to find it, but appended .Custom.Generic
, so hopefully, it won't conflict with any namespace Microsoft may decide to use in future, If you don't like it here, feel free to change it!
Now, we need a RunWorkerAsync
method that takes a TArgument
instead of an object
.
public bool RunWorkerAsync(TArgument argument)
{
}
VB:
Public Function RunWorkerAsync(ByVal argument As TArgument) As Boolean
End Function
Once the thread is launched, we need to raise a DoWork
event with generic arguments, so a new class is needed, and also the event.
public class DoWorkEventArgs<TArgument, TResult> : CancelEventArgs
{
public DoWorkEventArgs(TArgument argument)
{
Argument = argument;
}
public TArgument Argument
{
get;
private set;
}
public TResult Result
{
get;
set;
}
}
public event EventHandler<DoWorkEventArgs<TArgument, TResult>> DoWork;
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
Public Event DoWork As EventHandler(Of DoWorkEventArgs(Of TArgument, TResult))
Whilst in the DoWork
event handler, we need to be able to report progress, so we need a suitable method to call, a new event argument class, and the event, of course.
public bool ReportProgress(int percentProgress, TProgress userState)
{
}
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;
}
}
public event EventHandler<ProgressChangedEventArgs<TProgress>> ProgressChanged;
VB:
Public Function ReportProgress(ByVal percentProgress As Int32, _
ByVal userState As TProgress) As Boolean
End Function
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
Public Event ProgressChanged As EventHandler(Of ProgressChangedEventArgs(Of TProgress))
Finally, we need to return our result. For this, we need a new event args class and the event.
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;
}
}
public event EventHandler<RunWorkerCompletedEventArgs<TResult>> RunWorkerCompleted;
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
Public Event RunWorkerCompleted As EventHandler(Of RunWorkerCompletedEventArgs(Of TResult))
Confession Time!
Internally, not everything is generic. I have used the System.ComponentModel.AsyncOperation
class to handle the marshalling of data across threads with its Post
and PostOperationCompleted
methods. Both of these use a delegate System.Threading.SendOrPostCallback
which takes an object
as a parameter. ProgressChangedEventArgs
and RunWorkerCompletedEventArgs
are both boxed by this delegate and unboxed again in the methods called. Still an improvement on the existing situation though, and invisible to the consumer of the class.
In Use / Changes to the Original
This background worker can be used exactly the same as the original but with the benefit of Generics. All the same properties, methods, and events are there, and no extra ones. You can't (as I explained earlier) drop it into a designer, but instantiating the class and subscribing to the needed events in code is trivial. I have made a few changes though as there are a few things I don't like about the original.
RunWorkerAsync
returns a bool
instead of void
. If the worker is busy, it returns false
instead of throwing an exception.CancelAsync
returns a bool
instead of void
. If the worker doesn't support cancellation, it returns false
instead of throwing an exception.ReportProgress
returns a bool
instead of void
. If the worker doesn't report progress, it returns false
instead of throwing an exception.
- The
WorkerReportsProgress
and WorkerSupportsCancellation
properties default to true
, not false
.
In my implementation, no exceptions should be thrown. If one is thrown in the DoWork
event handler, it is caught and passed to the Error
property of RunWorkerCompletedEventArgs
.
There are many times when the data type you pass in and out of a background worker are the same. To facilitate that without needing to declare three identical type parameters, I have included BackgroundWorker<T>
along with DoWorkEventArgs<T>
at no extra charge!
The Demo
The demo simulates a file operation. It uses a BackgroundWorker<string[], string, List<FileData>>
. The first parameter (TArgument
) is an array of filenames to be processed. The second (TProgress
) is the filename that is being processed when progress is reported. The third is a List
of a simple class FileData
that holds the filename and the timestamp of when it was processed.
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:
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
It begins by passing a string
array of files
to the RunWorkerAsync(TArgument)
method when the Start button is clicked. In the DoWork
event handler, it iterates over this array. On each iteration of the array, the worker reports progress. The ProgressChangedEventArgs.ProgressPercentage
is used to update a ProgressBar
and ProgressChangedEventArgs.UserState
which is a string
(TProgress
) used to update a Label
's Text
so we can see which file is being processed.
private void fileWorker_DoWork(object sender,
DoWorkEventArgs<string[], List<FileData>> e)
{
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)
{
labelProgress.Text = e.UserState;
progressBar.Value = e.ProgressPercentage;
}
VB:
Public Sub fileWorker_DoWorkHandler _
(ByVal sender As Object, ByVal e As DoWorkEventArgs(Of String(), List(Of FileData)))
// We
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
There are 50 files, so I increment the progress by 2 each time so it's at 100 when finished. Notice, we also check for CancellationPending
. If the Cancel button is clicked, it calls our CancelAsync
method which sets this property, so we set the DoWorkEventArgs.Cancel
property and exit. I have included a 50ms delay so we can see the worker working!
In the above code, we have also been adding the file data to DoWorkEventArgs.Result
(TResult
). This is automatically passed to the RunWorkerCompleted
event handler, where the List
(TResult
) is used as the data source for a ListBox
.
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:
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
Conclusion
If you're like me and hate casting / unboxing and you want to utilise the simplicity of a background worker, then you should find this class library useful - I'm sure now that I've written it, I'll be using it a lot. If you find any bugs or gremlins, then please let me know in the forum below.
References
The background worker implementation is based upon a non generic version published here.
History
- 5th September, 2009: Version 1.
- 7th September, 2009: Version 2, Added VB.NET article text and solution download.