Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Generic Background Worker

4.84/5 (33 votes)
8 Sep 2009CPOL6 min read 100.4K   5.6K  
No more unboxing/casting! Use generic type parameters with this background worker. Source code for C# and VB.NET.

screenshot.png

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 lightbulb.png... 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).

dataflow.png

'Genericifying' the BackgroundWorker

So, the first thing is to create a new class that takes three generic type parameters.

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

VB:

VB
' 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.

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

VB:

VB
' 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.

C#
// 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#
// C#
public event EventHandler<DoWorkEventArgs<TArgument, TResult>> DoWork;

VB:

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
' VB
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.

C#
// C#
public bool ReportProgress(int percentProgress, TProgress userState)
{
}
C#
// 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#
// C#
public event EventHandler<ProgressChangedEventArgs<TProgress>> ProgressChanged;

VB:

VB
' VB
Public Function ReportProgress(ByVal percentProgress As Int32, _
                ByVal userState As TProgress) As Boolean
End Function
VB
' 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
' VB
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.

C#
// 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#
// C#
public event EventHandler<RunWorkerCompletedEventArgs<TResult>> RunWorkerCompleted;

VB:

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
' VB
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.

Bonus Extra!

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.

C#
// 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
' 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.

C#
// 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
' 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

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.

C#
// 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
' 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)