Sam Pride

Easily implement Asynchronous Programming in your WinForm Apps - The BackgroundWorker component

Discussion created by Sam Pride on Apr 2, 2009
Latest reply on Apr 8, 2009 by Sam Pride

 

I’m sure we’ve all encountered issues with the UI becoming unresponsive whilst an intensive or long-running method is executing. As the method is running on the UI thread, the UI will not respond to updates (such as repainting, moving, on click events) until your method stops blocking. For seasoned application developers such as ourselves, we know not to panic; everything will respond in due course and we’ll happily wait until it does (it’s a great time to refill that cup of coffee). Users on the other hand are often much more impatient and expect better behaviour from their apps.

The solution appears simple; execute the method on a different thread so the UI can respond to user input. Whilst this should be fairly easy to implement for most .Net developers, it does introduce more complexity to your application and it is not always a trivial matter to implement in existing code. Furthermore, providing feedback to the UI is not as straight forward as it was with a single-threaded model; you need to contend with locking and synchronising objects and somehow force the UI to update each time. Most of the time, this is considered too much work for too little results (a “nice to have”) when the application or tool is essentially a throw-away app.

I found myself in the same situation just recently; I was developing a single-use application that would take some time to execute a single method. I was happy to let this run for some time but knew the customer who would use the tool would get impatient, close it down, keep trying and ultimately keep bugging me about it being “broken”. The best solution would be to implement a progress bar or other visual indicator that the application was still working. As mentioned previously, incrementing a progress bar during each iteration of the main loop would not be sufficient because the progress bar would not always update until the method returned. Refactoring the code and making it more APM-friendly was too much work for a throw-away app, so I decided to use the BackgroundWorker component.

BackgroundWorker

 The BackgroundWorker component greatly simplifies programming threads and with a few simple changes to your code, it is quite straight forward to implement in existing WinForms. The BackgroundWorker class is a member of the System.ComponentModel namespace and can be added at design time (from the Toolbox->Components) or instantiated in code. To explain its ease of use, we’ll create a simple program that simulates some intensive/long running code and then we will modify our example to leverage the BackgroundWorker.

1.       Create a new .Net Windows application (I’ll use C#, but you can easily use VB.net if you wish). Ensure the .Net version is 2.0 or higher.

2.       On the main form, add the following items to the form:
From Layout
3.       Add a Using (imports for those VB dudes out there) statement for the System.Threading namespace (this is not needed for the BackgroundWorker, it’s needed for our sleep statement).

4.       Create a new method called IntensiveOperation() as follows:

protected double IntensiveOperation(int numIterations, int pausePerIteration)
        {
            DateTime dtPerformanceIndicator = DateTime.Now; //Used to record how long this operation took to execute.

            for (int i = 0; i < numIterations; i++)
            {
                //Simulate some intensive code
                Thread.Sleep(pausePerIteration);

                //Update the UI to give the user some feedback
                progressBar1.Value = 100 * i / numIterations; //Percentage complete

            }

            //Work out how long has passed since we first called this method.
            TimeSpan totalTime = DateTime.Now.Subtract(dtPerformanceIndicator);

            return totalTime.TotalMilliseconds;
        }


5.       Compile your project, fix any mistakes and run your code. For starters, try setting the number of iterations to 1000 and the pause per iteration to 100. Click the Go button. This should take 100 seconds (or thereabouts) to execute.

Whilst the application is running, you may notice the progress bar updating as expected. This will not always be the case. To test this out, try moving the window. You should notice that the bar stops updating and the application possibly stops responding. Rest assured that the code is still executing in the background, but the UI is not getting a chance to redraw and update.

Try increasing the two values and see how poorly the interface behaves. Implementing an asynchronous model will resolve this. We’ll now modify our example to use the BackgroundWorker component to achieve this.

6.       Stop your project and return to the Main form’s design View. Add another button:
Form Layout

7.       From the Toolbox, drag a BackgroundWorker component from the Component section onto the form surface. This will add the component to the form (and appear in the ‘gutter’ of the design surface).

8.       Select the backgroundWorker1 component on the design surface and, from the properties window, set “WorkerReportsProgress” to true.

9.       Click on the Lightning icon and implement all three events (DoWork, ProgressChanged,RunWorkerCompleted). The DoWork method will contain the actual processing/work we do. ProgressChanged will be fired whenever the BackgroundWorker has indicated that it has done ‘some’ work. Finally, the RunWorkerCompleted is called whenever the worker has completed (successful or otherwise).

10.   Implement the btnGoAsync’s onclick event handler. This method will setup the form (disabling UI elements that we don’t want the user to modify) and then get the BackgroundWorker working.

The first thing to consider is calling backgroundWorker1.RunWorkerAsync() only allows one or no parameters. We have two variables that define the work we are doing, so we need to decide how we get this information to the method that does all the work for us.

You may be tempted to access the values of the text boxes directly, but you will encounter an exception at runtime. UI elements are thread-safe and cannot be accessed from another thread. Another option is to declare the numIterations and pausePerIteration as global variables (members of the class) and reference those from within our worker procedure. Whilst this is simple, it is not the best way of resolving this issue and may restrict you in the future (what if you wanted to run two or more procedures concurrently?). We will get around this by implementing a container class to hold our arguments.

11.   In the same file, but out of the Form’s class block (before the last ‘}’ in C# or just after “End Class” in VB), insert the following code:
public class IntensiveOperationArgs
    {

        public int NumIterations;
        public int PausePerIteration;

        public IntensiveOperationArgs(int _numIterations, int _pausePerIteration)
        {
            NumIterations = _numIterations;
            PausePerIteration = _pausePerIteration;
        }
    }

This code defines a simple container class to hold our parameters and an instance of this class will be passed to our worker to get the job done.

12.   We can now call backgroundWorker1.RunWorkAsync() and pass a new instance of our IntensiveOperationArgs class. The completed btnGoAsync_Click method is as follows:
private void btnGoAsync_Click(object sender, EventArgs e)
        {

            int numIterations, pausePerIteration;

            //Perform validation (Always perform validation :-> )
            if (String.IsNullOrEmpty(txtNumIterations.Text) || !int.TryParse(txtNumIterations.Text, out numIterations))
            {
                MessageBox.Show("Invalid or empty value specified for the number of iterations", "Invalid input");
                return;
            }

            if (String.IsNullOrEmpty(txtPause.Text) || !int.TryParse(txtPause.Text, out pausePerIteration))
            {
                MessageBox.Show("Invalid or empty value specified for the pause time per iteration", "Invalid input");
                return;
            }


            //Disable UI elements that will cause us some grief if they are played with during execution
            btnGoAsync.Enabled = false;
            txtNumIterations.Enabled = false;
            txtPause.Enabled = false;

            //Reset the progress bar
            progressBar1.Value = 0;

            //Do the work
            backgroundWorker1.RunWorkerAsync(new IntensiveOperationArgs(numIterations, pausePerIteration));
        }


13.   As we have modified how we pass the arguments to our intensive procedure, we need to modify its behaviour. We will also need to modify the method to be more BackgroundWorker-friendly. To preserve the functionality of the original method (for comparisons), copy and paste the existing IntensiveOperation method and rename it to IntesiveOperationAsync. Modify the method signature to accept an IntensiveOperationArgs parameter, a BackgroundWorker parameter and a DoWorkEventArgs parameter.

Once again, the IntensiveOperationArgs and BackgroundWorker parameters allow us much more flexibility should we wish to reuse this code. The DoWorkEventArgs parameter represents the arguments passed to our worker object.

We will need to modify our code to reference the IntensiveOperationArgs instead of the parameters. Modify the code to call the args.NumIterations and args.PausePerIteration properties.

This method will be executed in its own thread. As the UI is not explicitly calling this method, we need to be able to report the result back to the user. The DoWorkEventArgs contains a property called Result that we use for this purpose. Modify the method signature to not return anything (void) and, instead of returning the time taken, assign this to the Result property of the DoWorkEventArgs that was passed as a parameter. We will pick up this result in another method.

Finally, as the progress bar is on another thread, we are unable to update its value. The BackgroundWorker can report any changes in progress and, in turn, our UI thread can increment the progress bar for us. To achieve this, replace the code that updates the value of the progress bar with a call to the ReportProgress() method BackgroundWorker that was passed as a parameter. The ReportProgress() method takes an integer value representing the work completed.

The final IntensiveOperationsAsync method is as follows:
protected void IntensiveOperationAsync(IntensiveOperationArgs args, BackgroundWorker worker, DoWorkEventArgs e)
        {
            DateTime dtPerformanceIndicator = DateTime.Now; //Used to record how long this operation took to execute.

            for (int i = 0; i < args.NumIterations; i++)
            {
               
                //Simulate some intensive code
                Thread.Sleep(args.PausePerIteration);

                //Update the UI to give the user some feedback
                worker.ReportProgress(100 * i / args.NumIterations); //Percentage complete

            }

            //Work out how long has passed since we first called this method.
            TimeSpan totalTime = DateTime.Now.Subtract(dtPerformanceIndicator);

            e.Result = totalTime.TotalMilliseconds;
        }


14.   Now we have our ‘worker’ method ready to use with the BackgroundWorker, we need to tell the worker to execute it whenever it is told to do its work. This is achieved through the DoWork event handler we created earlier. All we need to do is create the appropriate parameters for our IntensiveOperationAsync method.

The DoWorkEventArgs passed as a parameter to our DoWork event handler has a property ‘Argument’. This is set to the value that was passed to the RunWorkAsync() method when the user clicked the button. All we need to do is cast this to an IntensiveOperationArgs method and pass it to our IntensiveOperationAsync method. You will also need to cast the sender object to a BackgroundWorker. The resulting method is as follows:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = (BackgroundWorker)sender;
            IntensiveOperationArgs args = (IntensiveOperationArgs)e.Argument;

            this.IntensiveOperationAsync(args, worker, e);

        }

 
15.   This is all we need to achieve to get the code running asynchronously. Before we start testing this application, it would be great if we alerted the user of the progress (like our original example did. To do this, we need to implement the code for our ProgressChanged and RunWorkerCompleted event handlers.

The ProgressChangedEventArgs parameter of the ProgressChanged Event handler has a property ‘PercentageComplete’. We can assign this value to our progressBar1.Value property and update our progress bar.

When the BackgroundWorker has finished processing, it will fire off a RunWorkerCompleted event. Our event handler will alert the user of the result (just like our original code did). The RunWorkerCompletedEventArgs passed as a parameter to our event handler contains the results of the operation. Use the Result property of this parameter and alert the user. You will also want to reset any UI elements you disabled so the user can run the procedure again.

The RunWorkerCompletedEventArgs contains other parameters of interest, but we will investigate them later on.

This ProgressChanged and RunWorkerCompleted event handlers are as follows:
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            this.progressBar1.Value = e.ProgressPercentage;
        }

        private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
         
                MessageBox.Show(String.Format("The intensive operation has completed and took {0}ms", e.Result));
          

            //Reset the form elements.
            btnGoAsync.Enabled = true;
            txtNumIterations.Enabled = true;
            txtPause.Enabled = true;

            //Reset the progress bar
            progressBar1.Value = 0;

        }


16.   Now it’s time to test the code. Compile your project, fix any errors and then run the application. Using the same values we used for testing earlier (1000 and 100), click on the button to launch the code asynchronously. You will notice that you can move the form around, minimize it, click all over the place and the progress bar continues to update smoothly. When it is complete, you should be alerted with the results. Compare this with the original code.

17.   An advantage of using a BackgroundWorker is that we can give the user more control over the process. The BackgroundWorker will allow us to cancel the process. This is quite handy if you’ve specified the wrong parameters or can’t be bothered waiting for it to finish.

To implement this functionality, we need to set the ‘WorkerSupportsCancellation” property of our backgroundworker1 to true. Once we have enabled that, we need to modify our IntensiveOperationAsync code to check to see if the worker has been cancelled before continuing with the next iteration. To do this, we check the CancellationPending property of our BackgroundWorker object and, if it’s set, we set the Cancel property of our DoWorkEventArgs to true.

The following block of code should be added to the IntensiveOperationAsync method just before the Sleep() statement.
if (worker.CancellationPending)
                {
                    //Someone hit the cancel button
                    e.Cancel = true;
                    return;

                }


18.   Now we can support cancellation of the BackgroundWorker, we should consider informing the user that the process was interrupted. The RunWorkerCompletedEventArgs object that is passed to the RunWorkerCompleted event handler contains a property ‘Cancelled’ that is true if the worker has been cancelled. Modify the RunWorkerCompleted event handler to check the value of this property and, if true, inform the user that it was aborted (they should know, they pressed the button!).

Another property of the RunWorkerCompletedEventArgs that may be useful is the Error property. If an error occurs within the DoWork method, the worker will ultimately stop. When this happens, the RunWorkerCompleted event is still triggered, so we should check to see if there was an error before reporting to the user that everything went well. The Error property is an Exception object and is not null (nothing) if an error occurred.

The final code for the RunWorkerCompleted event handler is as follows:
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            //Handle any errors
            if (e.Error != null)
            {
                MessageBox.Show("An Error occurred: " + e.Error.Message);
            }
            else if (e.Cancelled) //Did the user abort the operation
            {
                MessageBox.Show("The operation was aborted by the user");
            }
            else //All went well, we have reults.
            {
                MessageBox.Show(String.Format("The intensive operation has completed and took {0}ms", e.Result));
            }

            //Reset the form elements.
            btnGoAsync.Enabled = true;
            txtNumIterations.Enabled = true;
            txtPause.Enabled = true;

            //Reset the progress bar
            progressBar1.Value = 0;

        }


19.   The final step is to create a button that will cancel the operation. Add a new button on the form and implement its OnClick event handler. In this method, call the BackgroundWorker.CancelAsync method. Modify your RunWorkerCompleted and btnGoAsync Click event handlers to disable this button when the BackgroundWorker is not working.

20.   Build the project, fixing any errors that have occurred and test this new functionality.

Whilst this post has been quite long (I suspect some of you have given up or fallen asleep), it is easy to do. Once you have implemented a BackgroundWorker to handle asynchronously calling your resource expensive operations, you should be able to quickly and easily implement it in future or existing projects. It may only be a small change, but keeping the UI responsive adds a touch of professionalism that customers expect.

I hope this has been helpful to somebody, I am most certainly glad I finally looked into it. If you have any questions, comments, bug reports or want to sling abuse, please feel free to post here. I will try and get a copy of my completed solution online somehow. In the interim, if you want a copy, email me: sam@osisoft.com.au

Outcomes