Click here to Skip to main content
Click here to Skip to main content

Generic Background Worker

By , 8 Sep 2009
 

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#
namespace System.ComponentModel.Custom.Generic
{
    public class BackgroundWorker<TArgument, TProgress, TResult>
    {
    }
}

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#
public bool RunWorkerAsync(TArgument argument)
{
}

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#
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))

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#
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))

Finally, we need to return our result. For this, we need a new event args class and the event.

// 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))

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#
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

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#
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

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#
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

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)

About the Author

DaveyM69
United Kingdom United Kingdom
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberpip0102 Nov '12 - 3:52 
feels a gap in the CLR
GeneralMy vote of 5memberChristian Amado28 Aug '12 - 8:11 
Great idea! I love BackgroundWorker. This is a way to improve it! Well done!
QuestionThread Priority on Background Worker TaskmemberNicholas De La Haye3 Nov '11 - 11:19 
So how do I change the thread priority using your class?
AnswerRe: Thread Priority on Background Worker TaskmentorDaveyM693 Nov '11 - 12:41 
If you wish to control a thread's priority you shouldn't use this or any other BackgroundWorker. Instead create and control your own ThreadPool thread.
 
I haven't tried it but you could set System.Threading.Thread.CurrentThread.Priority in the DoWork handler.
Dave

Binging is like googling, it just feels dirtier.
Please take your VB.NET out of our nice case sensitive forum.
Astonish us. Be exceptional. (Pete O'Hanlon)

BTW, in software, hope and pray is not a viable strategy. (Luc Pattyn)



GeneralAnother way...memberedge942111 Jun '10 - 15:00 
I found your article because I was similarly annoyed by the lack of strongly typed arguments in BackgroundWorker and was looking around to see what google would come up with.   I am curious why you chose to re-implement all of the behavior rather than inherit from the existing BackgroundWorker class and EventArg classes and replace/hide only the events & methods that are involved with the strong typing.   I was curious if it could be done and put together exactly that and it seems to be working nicely, although I can't say I've exercised it all that much.   What are your thoughts?
 
--Keith
GeneralRe: Another way...mvpDaveyM6913 Jun '10 - 1:50 
It can be done by subclassing the background worker (or wrapping one). That is more untidy though as if subclassing you would have to hide all the non relavent properties/methods/events (plus hiding is against OOP principles!). Also, as mentioned at the start of the article, it's not possible to use generics with the designer - as System.ComponentModel.BackgroundWorker is a component it is usable in the VS designer so not compatible with the purpose of the article.
Dave

If this helped, please vote & accept answer!

Binging is like googling, it just feels dirtier. (Pete O'Hanlon)

BTW, in software, hope and pray is not a viable strategy. (Luc Pattyn)

GeneralRe: Another way...memberedge942114 Jun '10 - 16:05 
Thanks, those are good reasons.   I'm not as strict on OOP principles, so I don't mind hiding in this case since all they really do is forward to the base implementation.   In fact, the hidden stuff would still work if someone went to all the trouble of casting down to the base type to bypass my implementation.   As for the control designer aspect, I won't miss that because I always created the instances in code as I need them.   For some reason the BackgroundWorker class just doesn't seem like something that needs to be drag & drop anyway LOL.   I do like the idea of not having to re-invent/debug/maintain all the logic of the BackgroundWorker, although what you did appears to be quite good.
 
--Keith
GeneralRe: Another way...memberalexsuh20 May '11 - 22:06 
You better use the designer , unless you want to dispose the backgroundworker yourself.
see http://stackoverflow.com/questions/2542326/proper-way-to-dispose-of-a-backgroundworker[^]
GeneralRe: Another way...mentorDaveyM6920 May '11 - 23:28 
As the thread you linked to discusses, there is no need as the background worker itself holds no resources that need disposing. The only reason the original has dispose is because it derives from component so the container requires it's contents to be disposable. As this implementation cannot be added to a container, it is not needed or required.
Dave

Binging is like googling, it just feels dirtier.
Please take your VB.NET out of our nice case sensitive forum.
Astonish us. Be exceptional. (Pete O'Hanlon)

BTW, in software, hope and pray is not a viable strategy. (Luc Pattyn)



GeneralGreat jobmemberDecodedSolutions.co.uk26 May '10 - 22:18 
Very nice, wasa great read and top article
GeneralGreatmvpLuc Pattyn12 Feb '10 - 12:54 
Yep, BGW is one of those types badly needing genericification. Poke tongue | ;-P
Waiting for DD to turn that into a CCC...
Luc Pattyn [Forum Guidelines] [Why QA sucks] [My Articles]

I only read code that is properly formatted, adding PRE tags is the easiest way to obtain that.
All Toronto weekends should be extremely wet until we get it automated in regular forums, not just QA.

GeneralRe: GreatmvpDaveyM6912 Feb '10 - 13:28 
Luc Pattyn wrote:
Waiting for DD to turn that into a CCC

 
Laugh | :laugh:
 

Luc Pattyn wrote:
one of those types badly needing genericification

 
There are probably several others. The decision to make the designer display components so their instanciation and property/event use was available to the 'drag and drop' programmer is where MS made a mistake in my opinion.
Dave

Tip: Passing values between objects using events (C#)
BTW, in software, hope and pray is not a viable strategy. (Luc Pattyn)
Why are you using VB6? Do you hate yourself? (Christian Graus)

GeneralBasicDelegate == ActionmvpDaveyM6915 Sep '09 - 4:27 
It's not worth updating the article for this minor change...
 
Another CP member has pointed me to the System.Action delegate. This is identical to the 'BasicDelegate' I use in the code, so feel free to remove the BasicDelegate and replace all references to it with System.Action Thumbs Up | :thumbsup:
 
Dave

Generic BackgroundWorker - My latest article!
BTW, in software, hope and pray is not a viable strategy. (Luc Pattyn)
Why are you using VB6? Do you hate yourself? (Christian Graus)

GeneralCool ideamemberShane Story10 Sep '09 - 6:39 
Well, casting has never really killed me, but it is a good way to avoid it. The article seems nicely written.
 
Shane
Jesus loves you! John 3:16 · Rom 3:23 · Rom 6:23 · Rom 10:9

GeneralNicememberstevenlauwers2210 Sep '09 - 6:20 
Nice, this should have been part of the .NET Framework Smile | :) .

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 8 Sep 2009
Article Copyright 2009 by DaveyM69
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid