Keep Client Applications Responsive with the BackgroundWorker

Many applications need to perform some kind of long-running task like downloading a file, performing a complex calculation, or retrieving data from a database. Executing these tasks can make your application become unresponsive and end up making your users anxious. Take too long performing the task and the user will be looking for the Task Manager to kill your application.

The solution to this problem is to execute the long-running task on a second (or background) thread. This allows the user interface thread to continue working keeping your application responsive, but it presents you with a new problem. Creating multi-threaded applications is difficult - well actually it’s easy - it’s doing so properly, following all the rules, and not coding yourself into an all night debugging session that’s truly hard.

To help address this issue Microsoft added the BackgroundWorker component to .NET 2.0. This component is easy to use and understand and makes programming multi-threaded applications (without explicitly creating and managing threads) much safer for the developer. The exploration of the BackgroundWorker will be done through the creation of a demo application.

For a more comprehensive discussion of the topics covered in this article and to see how to achieve the same results using previous versions of .NET, see Chris Sells three-part series entitled Safe, Simple Multithreading in Windows Forms (http://shrinkster.com/ddm).

Let's assume you have a simple application with a Button and a ProgressBar. The code below will simulate a long-running task by putting the user interface thread to sleep, one half second at a time, in a loop. The loop is used to allow the program to report progress back to the user via the ProgressBar.

Private Sub Button1_Click(...) Handles Button1.Click
  Button1.Enabled = False
  For i As Integer = 1 To 10
    ProgressBar1.Value = i * 10
    Threading.Thread.Sleep(500)
  Next
  MessageBox.Show("Done", "Demo")
  Button1.Enabled = True
End Sub

As it stands, clicking the Start Button will begin the long-running task and, while the ProgressBar will be updated as we iterate through the loop, the rest of the application will be unresponsive. You will not be able to move or resize the form until the loop completes. The remainder of this post will discuss how to convert this code to run the loop on a background thread via the BackgroundWorker component. It will also show how to upgrade the application to allow the long-running task to be cancelled.

First, declare a private field for the BackgroundWorker. Then in the Form’s Load event handler, create the instance of the BackgroundWorker and set its WorkerReportsProgress and WorkerSupportsCancellation properties to True. These properties are set to False by default as a performance optimization.

Private WithEvents _worker As BackgroundWorker
Private _working As Boolean
Private Sub Form1_Load(...) Handles MyBase.Load
  _worker = New BackgroundWorker
  _worker.WorkerReportsProgress = True
  _worker. WorkerSupportsCancellation = True
End Sub

Create an event hander for the BackgroundWorker’s DoWork event and copy the existing code from the Button’s Click event hander to the newly created method. The Button is going to be used to both start the long-running task as well as cancel it. In the Click event handler check if the BackgroundWorker is currently busy; if it is signal a request to cancel the task by calling CancelAsync otherwise begin the task by calling RunWorkerAsync. It is important to note that the call to CancelAsync does not immediately terminate the task, it just signals a request to cancel. The code running the task must check for the request and handle it appropriately (see below).


Private Sub Button1_Click(...) Handles Button1.Click
  If _worker.IsBusy Then
    _worker.CancelAsync()
  Else
    Button1.Text = "Cancel"
    _worker.RunWorkerAsync()
  End If
End Sub

The code copied to the DoWork event handler needs to be modified slightly. The code in this event handler executes on a background thread so it cannot safely update the user interface, thus the line that originally set the Value property of the ProgressBar has to be changed. Replace this line with a call to the ReportProgress method of the BackgroundWorker passing the percent completed as a parameter. This will trigger the ProgressChanged event where we can safely update the ProgressBar. In addition, this code has to check for a request from the user to cancel the task which can be done by inspecting the CancellationPending property of the BackgroundWorker.

Private Sub _worker_DoWork(...) Handles _worker.DoWork
  For i As Integer = 1 To 10
    _worker.ReportProgress(i * 10)
    Threading.Thread.Sleep(500)
    If _worker.CancellationPending Then
      e.Cancel = True
      Exit For
    End If
  Next
End Sub

Private Sub _worker_ProgressChanged(...) Handles _worker.ProgressChanged
  ProgressBar1.Value = e.ProgressPercentage
End Sub

Finally, the BackgroundWorker raises the RunWorkerCompleted event to notify the user interface that long-running task is done, either because it completed normally or because it was cancelled.

Private Sub _worker_RunWorkerCompleted(...) Handles _worker.RunWorkerCompleted
  Dim msg As String = "Done"
  If e.Cancelled Then
    msg = "Cancelled"
  End If
  Button1.Text = "Start"
  MessageBox.Show(msg, "Demo")
End Sub

This short example demonstrates the basic use of the BackgroundWorker. That is executing a long-running task on a background thread, showing progress, and allowing the user to cancel.

Published Thu, Nov 9 2006 9:58 by windsor