DevCity.NET - http://devcity.net
Multithreading The Easy Way: The BackgroundWorker
http://devcity.net/Articles/352/1/article.aspx
Ged Mead

Ged Mead (XTab) is a Microsoft Visual Basic MVP who has been working on computer software and design for more than 25 years. His journey has taken him through many different facets of IT. These include training as a Systems Analyst, working in a mainframe software development environment, creating financial management systems and a short time spent on military laptop systems in the days when it took two strong men to carry a 'mobile' system.

Based in an idyllic lochside location in the West of Scotland, he is currently involved in an ever-widening range of VB.NET, WPF and Silverlight development projects. Now working in a consultancy environment, his passion however still remains helping students and professional developers to take advantage of the ever increasing range of sophisticated tools available to them.

Ged is a regular contributor to forums on vbCity and authors articles for DevCity. He is a moderator on VBCity and the MSDN Tech Forums and spends a lot of time answering technical questions there and in several other VB forum sites. Senior Editor for DevCity.NET, vbCity Developer Community Leader and Admin, and DevCity.NET Newsletter Editor. He has written and continues to tutor a number of free online courses for VB.NET developers.

 
by Ged Mead
Published on 6/1/2008
 

  Sometimes you may need  your application to work on a slow or time- consuming task in such a way that your user isn't kept waiting unnecessarily while this is happening.

   The answer to this problem is to use multithreading.   Having seen many forum questions about multithreading, I know that many people are a bit apprehensive about tackling it.  However,  as it turns out, the BackgroundWorker component is versatile  and easy to use, providing a painless solution to this requirement. 

   In this article I will show you how you can  incorporate the BackgroundWorker into your projects to give your users a more professional and less frustrating experience.

 

 


The Task

Introduction
  This article came about because I had seen several posts on vbCity recently where I thought the best approach might be to use a BackgroundWorker.  As you probably know, this is a helper component that wraps away some of the down-and-dirty stuff you need if you want to use multithreading. The BackgroundWorker won't meet every possible scenario where you have to get stuck into multithreading, but it's a very useful little tool in many basic situations.

  For this example I am going to run a slow, somewhat resource intensive process on a background thread and will test that it is working as it should by making another form containing other controls available to the user while the slow background task is running.

  The actual task is a (kinda) real world one where the code makes a search for specific files  in directories on a user's hard drive.   Just for interest I have extended this basic scenario so that files that match a particular file type (e.g. text files) are further searched to see if they contain a specific string.

  This makes it a potentially long running task and therefore one that is very suitable to carry out on a separate asynchronous thread; the result being that the user can continue to use other parts of the application without the whole thing hanging while the searches take place.

  The code below is the skeleton of the background task:  

Code Copy
  Sub FileFinder(ByVal dir As String)
        Try
            ' Display all files in a directory that match file type
            For Each fname As String In Directory.GetFiles(dir)
        If fname.EndsWith("txt") Then
                    'TODO:  pass back progress message
              End If
            Next
           ' A recursive call for all the subdirectories in this directory.
            For Each subdir As String In Directory.GetDirectories(dir)
                FileFinder(subdir)
            Next
        Catch ex As Exception
            MessageBox.Show(ex.Message.ToString)
        End Try
    End Sub


 

Getting Started - DoWork

  To follow along with this example, start a new Windows Forms project and drag a BackgroundWorker component from the Toolbox on to the form.

  Copy and paste the code from the previous page into the form's code file.  Note that you will also need to add an Imports statement at the top of the file:

 Code Copy

Imports System.IO

   I have named my demo BackgroundWorker:  bWkrFileCheck.

   In the Properties Window for this BackgroundWorker you will see that there are only a few properties.  Two of them - WorkerReportsProgress and WorkerReportsCancellation -  have a default setting of False.

  In order for you to be able to write working code that gives user feedback and allows the background process to be aborted, you need to set both these properties to True.

Properties Window

   For this first stage you will also need to add a button to the form.   I have named this button:   btnStart.

   When you click on this button, code will be fired which instructs the BackgroundWorker to begin carrying out whatever task(s) it has been allotted.

   So the code to place in the button's click event is:

Code Copy
Private Sub btnStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStart.Click

        bWkrFileCheck.RunWorkerAsync()

End Sub

     The RunWorkerAsync method is a fairly self-explanatory one which means that it goes off and finds the task that bWkrFileCheck has been assigned and carries it out asynchronously.  That is, it works separately.  And in this context working separately means that the task is undertaken on a separate thread.

   If you add the button and enter the above code and run the project, you should see .... well, nothing, actually!   Hopefully, not an Exception message, but also no sign of any activity either.  

   And in fact there is no activity to monitor, because we haven't yet told the BackgroundWorker specifically what its task is.   That task of course will be to run the file search routine we entered previously, the "FileFinder" procedure.

  The place where we specifically tell the BackgroundWorker what task it has been assigned is in its DoWork event.  As with the list of properties you saw above, the list of events of a BackgroundWorker is fairly sparse; DoWork being the most fundamental one.

 

Properties Window

 

  In this first stage, the code for the DoWork event will be:

 

Code Copy
  Private Sub bWkrFileCheck_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bWkrFileCheck.DoWork

        FileFinder("C:\Program Files\")

  End Sub
 
      which is basic code that calls the FileFinder procedure and passes in a target folder as its argument.  
 
  If you want to  check that this works so far, then change the code of the FileFinder sub so that it writes the file names to the output window:
 
 
 
Code Copy
For Each fname As String In Directory.GetFiles(dir)
       If fname.EndsWith("txt") Then
            Console.WriteLine(fname)
        End If
Next
 
     If you run this, you will see that the task has been properly handed off to the BackgroundWorker and while that task is visibly running you can still do other things with your form, such as move it or resize it.   We'll improve the demo in the next few steps.
 
   So, even at this early stage we now have a second process running on a separate thread, so allowing the user to continue doing other things.  But of course it would be much better if we were to give some kind of feedback on how much progress the task has made.
 
   That will be our next step.
 
 

Giving User Feedback - ProgressChanged

  Because we took the step of setting the WorkerReportsProgress property to True, we can harness this to, well, report progress actually! 

  There are several ways we can keep the user informed.  Often a ProgressBar will be used and updated as the task reaches various stages.  Although we could take this route for the current project, calculating an exact count or time estimate for the task can sometimes be tricky, so I decided just to go feedback that displayed the files in a listbox as they are found.   Later I'll add some simple code that gives a clearer indication that the process is still working. 

  For additional user clarity I thought that what I would do is to create a new form as soon as the BackgroundWorker starts its task and get it to report progress directly back to this form.  (As opposed to displaying on the form I used to create and control the BGW ).

  This way, the user can minimise the form that shows progress if they want to so that the progress state isn't constantly in their face.

  So the next step will be to create a second form, called  frmProgress and to place a large ListBox on it.  (You could fill the whole form with the ListBox if you prefer, but I plan to add a label later to give the user more feedback, so have left some space at the bottom of the form).  

  Outside any of the form's procedures declare a variable for a frmProgress:

Code Copy
Dim fp As frmProgress

 

  The reason for doing it this way is that you will want to refer to this frmProgress  instance from elsewhere in Form1, so you need the scope to be form wide.

  Now you can instantiate a new frmProgress from the click event of the start button:

Code Copy
Private Sub btnStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStart.Click
        '  Show progress form
        fp = New frmProgress
        fp.Show()

        bWkrFileCheck.RunWorkerAsync()

End Sub

 

   I found that I really wanted the progress form to be placed neatly to the right hand side of Form1, so I also added this optional code to the button's click event:

Code Copy
fp.Left = Me.Left + Me.Width + 1
fp.Top = Me.Top

 

 which takes care of that.  Well, it should, but if you find that the progress form stubbornly insists of plonking itself on top of the calling form then you'll  need to either change the StartPosition property in the Properties Window or add this further line of code:

Code Copy
fp.StartPosition = FormStartPosition.Manual

  Next we need the code that will actually send the feedback to this form.

  If you refer back to the screenshot that showed the events available for the BGW you will see there is one named ProgressChanged.  This is the procedure in which we will place the code that actually updates the ListBox. 

Code Copy
Private Sub bWkrFileCheck_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles bWkrFileCheck.ProgressChanged
        fp.ListBox1.Items.Add(e.UserState)
End Sub

 

   However if you make this edit and run the project however you won't be rewarded with a display of files in the ListBox;  a result which may or may not surprise you.  

    The reason is that the above event will only fire when it is informed that there has in fact been a change of some kind that is classifed as a change in progress.  So what is needed is a trigger elsewhere which will cause this event to fire.  That trigger is the ProgressChanged method of the BackgroundWorker which in this example we will place inside the FileFinder procedure.

  Here is the amended code for this requirement:

Code Copy
For Each fname As String In Directory.GetFiles(dir)
   If fname.EndsWith("txt") Then
     '  pass back progress message
        bWkrFileCheck.ReportProgress(0, fname)
   End If
Next

 

  You will see that the ReportProgress method has two parameters - an Integer and also what appears to be a String.  The second parameter is in fact an Object, but as we have already declared fname as a string, this is the type that will be passed to the event.

  The first parameter is PercentProgress and you can assign a value to this to represent how far the process has moved through the task.  As I mentioned earlier, I've chosen not to implement this feature so I pass back a value of zero each time.   If you decide that you do need this for a project of your own, then you simply calculate the percentage at any given moment, assign this value to PercentProgress and use it in the ProgressChanged event as you wish (e.g. as a means of updating a progress bar).   

  The second parameter is UserState.  In this example I have chosen to assign the string that is the name of each file found to UserState.   Because ReportProgress runs each time the FileFinder succeeds in finding a file that meets the criteria, this of course means that all matching file names will be passed back in turn and can be listed in the ListBox.

   I hope that all makes sense.  You may be wondering at this point why it is necessary to take this long way round the task.  Why not simply add code such as:

Code Copy
  For Each fname As String In Directory.GetFiles(dir)
    If fname.EndsWith("txt") Then
     '  pass back progress message directly?
        fp.ListBox1.Items.Add(fname)
    End If
  Next

 

to the FileFinder procedure in order to update the ListBox?

  If you are curious I suggest you try it.   As you will see, the problem is that because you are now running the application using two separate threads the background thread can't "see" the main thread and so has no direct knowledge of the progress form and its ListBox.   The BackgroundWorker has to play that middleman role in order for the two threads to be able to communicate data between each other.

   As a matter of usability, one thing I don't particularly like about the way the ListBox is populated is that once its visible height is filled with file names, there is little indication that something is still happening.  In fact, the vertical scrollbar changes size as the list lengthens but this isn't particularly obvious.   So I will make some minor improvements to deal with this.

  First, try adding a new line to the ProgressChanged event as shown below:

Code Copy
    Private Sub bWkrFileCheck_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles bWkrFileCheck.ProgressChanged
        fp.ListBox1.Items.Add(e.UserState)
        fp.ListBox1.SetSelected(fp.ListBox1.Items.Count - 1, True)
    End Sub

Then run the project again.

   This time it's very obvious that the process is working away.   In fact, you might even think it's too obvious and might dislike the way the highlight flashes on and off as it updates.  If so, you can add yet another line:

 

Code Copy
    Private Sub bWkrFileCheck_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles bWkrFileCheck.ProgressChanged
        fp.ListBox1.Items.Add(e.UserState)
        fp.ListBox1.SetSelected(fp.ListBox1.Items.Count - 1, True)
        fp.ListBox1.SetSelected(fp.ListBox1.Items.Count - 1, False)
    End Sub

 

and this will fix that issue.  If you download the sample solution that I have included with this article you'll see that I have also added a label that gives the user some additional feedback.

  When you run this project now, you can check that the main thread (and your Form1) is fully accessible to the user by, for example, simply moving the form around the screen in the usual way - something you wouldn't be able to do if the project was choked up on the long running task.  

  Again, in the sample solution I have included a better means of checking that everything is running smoothly.  At the click of the Start Button, a third form is instantiated and shown and this form contains a calendar and a label.  The user can access the calendar and will see feedback from their actions.  This will happen while the ListBox on the progress form still continues to be updated.

  

 

 

 

 

Task Complete - RunWorkerCompleted or Cancelled

   Although in our simple example, it is obvious when the task has been completed you may well have scenarios where this isn't so.  For example you may prefer in some cases to minimize the progress form and only have it appear back at its default size once the task is complete.

  The place for the code to do this kind of task is in the RunWorkerCompleted event.  Unlike the ProgressChanged event, the BGW will fire its RunWorkerCompleted event once it effectively runs out of task - in our example, therefore, once it has enumerated through every folder and file in its task.

  With the ListBox, one thing you could do would be to reset the selected file back up to the first one in the list.  (Again in the demo solution I have made changes to a progress label - making it totally clear to the user that the task has finished).

Code Copy
    Private Sub bWkrFileCheck_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles bWkrFileCheck.RunWorkerCompleted
        If fp.ListBox1.Items.Count > 0 Then fp.ListBox1.SelectedIndex = 0
    End Sub

 

Note the If Then statement which ensures that the app won't crash if the search had resulted in no files being found (and therefore no first item in the ListBox to select).

Task Cancellation

   There is of course another situation which may cause  the task to end and that is where the user decides they want to cancel it.  

   When you are testing this kind of project in the Visual Studio IDE you can end it by closing down the startup form or hitting the Stop Debugging button.  However it might not be so good to force your users to close down the whole application if they only want to halt the background task!  Happily there is another property which makes this very easy - the ReadOnly CancellationPending property  - and a cancellation method of the BackgroundWorker - CancelAsync.   In combination, these will enable you to offer users a get-out clause.

  Add a second button to your project and name it btnStop.  Insert a call to the CancelAsync method of the BGW:

Code Copy
   Private Sub btnStop_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStop.Click
        bWkrFileCheck.CancelAsync()
    End Sub

 

  All that remains now is some way of actually terminating the running background process.  In our example, exiting the FileFinder sub will do nicely:

Code Copy
Sub FileFinder(ByVal dir As String)
        Try
            If bWkrFileCheck.CancellationPending Then Exit Sub
            ' Display all files in a directory that match file type
            For Each fname As String In Directory.GetFiles(dir)

                If fname.EndsWith("txt") Then
                    '  pass back progress message
                    bWkrFileCheck.ReportProgress(0, fname)
                End If
            Next

            ' A recursive call for all the subdirectories in this directory.
            For Each subdir As String In Directory.GetDirectories(dir)
                FileFinder(subdir)
            Next

        Catch ex As Exception
            MessageBox.Show(ex.Message.ToString)

        End Try
    End Sub

 

  Try running the project and hit the Stop button at some point in the run.  This will gracefully terminate the task the very next time the FileFinder procedure runs the If CancellationPending check.  

 

 

 

Summary

  This demonstration has used a very simple background task as its example, but you can use the same approach for more complex tasks.

   The BackgroundWorker makes it very easy to keep your application flowing while a time consuming, possibly CPU intensive, task is running in the background.  With the ReportProgress method you can give your users feedback as to what percentage of the task has completed (and optionally could of course use this value as a straightforward numeric value), together with visual notification, usually in the form of a text message .

  This very useful component shields you from the more technical intricacies and potential difficulties of using multithreading in your projects.  (However, if you do want more technical detail of multithreading, check out John Spano's article which you can read here.)

   I hope you'll find this introduction useful and will be able to use the techniques shown in projects of your own in the future.

  A sample application created using the Express Edition of VB 2008 is attached.  (If you prefer to use VB 2005 Express Edition or a full version of Visual Studio 2005, you can create a new project in VB 2005 and import the code files using the "Add Existing Item" menu choice.)