Whenever I talk about multithreading and asynchronous and parallel programming, someone asks about task cancellation; specifically, Windows Forms applications that support a Cancel button and a progress bar and perform the actual work separate from the UI thread so the application doesn’t appear to hang. I can understand why this question comes up a lot.

While .NET contains a built-in component (the BackgroundWorker class) for performing this work, the documentation is not very clear. I’ve had the “pleasure” of implementing this pattern, and it’s complex enough that I find myself doing a copy/paste from other projects to save myself the effort.

I’ll walk you through the process of creating a simple form application with this functionality. You’re more than welcome to skip ahead and grab the source code, which is distributed under the MIT License.

Let’s start by creating a basic form application. Add to the form two buttons (runButton and cancelButton), a progress bar (progressBar), a label (resultLabel), and a BackgroundWorker object (backgroundWorker). Give the buttons friendly names and clear the Text property of resultLabel. Also, look at the properties of backgroundWorker in the designer, and set “WorkerReportsProgress” and “WorkerSupportsCancellation” to True. Your form should look something like this:

Now we’ll create some very basic logic to get the application up and running. Keep in mind that the asynchronous processing performed by BackgroundWorker has no way of getting parameters in or out. To compensate for this weakness, we’ll create a worker (or processor) class that will have public properties that represent the input values for the work and the output of the work. This worker class will be instantiated as a private variable in the form application (called “_processor”) so the input can be set by the calling routine, and the results can be pulled out by the post execution code. Here’s what a simple worker class looks like:

using System;

using System.ComponentModel;

using System.Security.Cryptography;

using System.Threading;
namespace AsynchronousProcessingDemo

{

class Processor

{

public int Results { get; private set; }

public void PerformWorkAsync(BackgroundWorker worker, DoWorkEventArgs workerArgs)

{

worker.ReportProgress(0);
///TODO: fill in your own work, cancellation logic, and percent completion calculations
for (int counter = 0; counter <= 100; counter++)
{
if (worker.CancellationPending)
{
//Cancellation requested, cleanly exit
worker.ReportProgress(100);
workerArgs.Cancel = true;
break;
}
Thread.Sleep(250);

worker.ReportProgress(counter);

}
var randomNumber = new byte[1];
var rng = new RNGCryptoServiceProvider();
rng.GetBytes(randomNumber);
Results = Convert.ToInt32(randomNumber[0]);
}
}
}

This is a very basic class. If you need input parameters for the work, create public properties for each parameter and read them as needed in the PerformWorkAsync method. (Note that I am stashing the output in a public property as well; you’ll see why when we return to our main form.)

The next thing we need to do is to write the functions that encapsulate the concepts of “starting work,” “cancelling work,” and “cleaning up after work.” We also need a way to update the progress bar. For this, we have four methods in the main form class:

private void Execute()

{

runButton.Enabled = false;

cancelButton.Enabled = true;

Cursor = Cursors.WaitCursor;

cancelButton.Cursor = Cursors.Default;

backgroundWorker.RunWorkerAsync();

}
private void CancelExecution()

{

backgroundWorker.CancelAsync();

cancelButton.Enabled = false;

}
private void PostExecutionCleanup(bool cancelled)

{

cancelButton.Enabled = false;

runButton.Enabled = true;

Cursor = Cursors.Default;
if (cancelled)

{

resultLabel.Text = "Cancelled";

}

else

{

resultLabel.Text = _processor.Results.ToString();

}

}
private void UpdateProgressBar(int progress)

{

progressBar.Value = progress;

}

Let’s take a closer look at what is happening in each method. In Execute(), we disable the form (except for the Cancel button) while the work is occurring. Then we call the RunWorkerAsync() method of the backgroundWorker component. When RunWorkerAsync() is called, it fires the DoWork event, which we will handle in a moment. This is where we trigger the actual processing logic. The CancelExecution() method signals backgroundWorker to perform a cancellation and disables the Cancel button on the screen (to prevent double-clicking and to let the user know that something is occurring). PostExecutionCleanup() is called whenever the work is finished, regardless of whether it was allowed to run until the end or cancelled by the user. The UpdateProgressBar() method just sets the value of the onscreen progress bar.

Our final task is to wire event handlers to the on-form components and have those event handlers call the appropriate methods. All of these event handlers are the default event handlers (created by double clicking the component in the designer), except for the ProgressChanged and RunWorkerComplete events attached to backgroundWorker. To easily create those events, select the component in the designer, click the events “lightning bolt” in the “Properties” box, type the name of the method you want to use for the event handler, and press “Enter.” This will create the empty method and attach it to the component’s event in the designer code for you.

private void runButton_Click(object sender, EventArgs e)

{

Execute();

}
private void cancelButton_Click(object sender, EventArgs e)

{

CancelExecution();

}
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{

_processor = new Processor();

_processor.PerformWorkAsync(sender as BackgroundWorker, e);

}
private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
UpdateProgressBar(e.ProgressPercentage);
}
private void backgroundWorker_RunWorkerComplete(object sender, RunWorkerCompletedEventArgs e)

{

PostExecutionCleanup(e.Cancelled);

}

Now you have a fully functional application that acts like a modern desktop application. This particular application’s functionality isn’t terribly useful; all it does is waste time before producing a random number. All the same, it is a snap to take this sample project, adapt the Processor class to do what you need, and modify the form to suit your needs.

J.Ja

Disclosure of Justin’s industry affiliations: Justin James has a contract with Spiceworks to write product buying guides. He is also under contract to OpenAmplify, which is owned by Hapax, to write a series of blogs, tutorials, and other articles.

—————————————————————————————

Get weekly development tips in your inbox
Keep your developer skills sharp by signing up for TechRepublic’s free Web Developer newsletter, delivered each Tuesday. Automatically subscribe today!