Introduction
When developing Windows Forms applications, you always notice that the user interface will freeze when your application is doing a time consuming operation, for example processing a large file or requesting data from a remote server. This is because your application is running on a single thread. This thread is responsible for rendering the user interface to the user, and also for handling all of your application events and methods. Thus, a lengthy operation will block your user interface till it is done. Today, what we will be doing is to move these lengthy operations to a different thread, thus keeping the user interface running smoothly while your operation is working on the other side.
Background
For this, we will be using the Microsoft BackgroundWorker
class, more information on this class can be found here.
We will be creating a simple application that will be performing a time consuming operation, and displaying the final result to the user. This heavy operation will be carried on a different thread, and during its operation it will be constantly updating the user interface with its progress. We will also allow the user to cancel the operation at any time.
Please keep in mind that only the main thread will have access to the user interface, in other words, you cannot access any user control from the other thread. We will see more on this later.
Using the Code
I will be showing the code used in the application as we progress, and at the end, I will attach the final source code.
Creating the Solution
We will begin by creating a simple Windows Forms application form Microsoft Visual Studio, I will be using Visual Studio 2010. Create a new Windows Forms Application as in the below figure, I prefer to use C#, but you can use VB.NET if you like.
Set up your form designer as below. I personally like to use table layout panels to organize my controls. This will also make it a lot easier for me to keep the controls in order if the form is expanded or resized. What we will need to add is a text box (set to multiline mode) to show the results coming from our working thread, a numeric box to allow us to choose a number, a start button and a cancel button.
From the toolbox menu, under the Menus & Toolbars section, add a "Status Strip". This will allow us to add a status label, where we will be showing progress to the end user.
Inside the status strip, click the small arrow on the left corner and add a "Status Label". Rename the label to lblStaus
, and set its Text
property to an empty string
.
Inside the code class, declare an object of type BackgroundWorker
:
private BackgroundWorker myWorker = new BackgroundWorker();
In the Form Constructor, initialize the following properties of the worker we just created:
- The
DoWork
event handler, which will be called when the background worker is instructed to begin its asynchronous work. It is here inside this event where we do our lengthy operations, for example, call a remote server, query a database, process a file... This event is called on the new thread, which means that we cannot access the user controls from inside this method. - The
RunWorkerCompleted
event handler, which occurs when the background worker has finished execution has been canceled or has raised an exception.This event is called on the main thread, which means that we can access the user controls from inside this method. - The
ProgressChanged
event handler which occurs when the ReportProgress
method of the background worker has been called. We use this method to write the progress to the user interface. This event is called on the main thread, which means that we can access the user controls from inside this method. - The
WorkerReportsProgress
property, which is needed to instruct the worker that it can report progress to the main thread. - The
WorkerSupportsCancellation
property, which is needed to instruct the worker that it can be canceled upon the user request.
Below is the complete code for the constructor after the declarations:
public Form1()
{
InitializeComponent();
myWorker.DoWork+=new DoWorkEventHandler(myWorker_DoWork);
myWorker.RunWorkerCompleted+=new RunWorkerCompletedEventHandler(myWorker_RunWorkerCompleted);
myWorker.ProgressChanged+=new ProgressChangedEventHandler(myWorker_ProgressChanged);
myWorker.WorkerReportsProgress = true;
myWorker.WorkerSupportsCancellation = true;
}
Now let us declare the event handlers for our worker:
- The
DoWork
event handler will take two parameters, a sender object, and a DoWorkEventArgs
argument:
protected void myWorker_DoWork(object sender, DoWorkEventArgs e)
{
}
- The
RunWorkerCompeleted
event handler will take two parameters, a sender
object, and a RunWorkerCompletedEventArgs
argument:?
protected void myWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
}
- The
ProgressChanged
event handler will take two parameters, a sender object, and a ProgressChangedEventArgs
argument:?
protected void myWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
}
Now we will create a helper method that will simply accept an integer, multiply this integer by 1000, sleep for 250 milliseconds, and then return the result. This is just a simulation for a heavy operation that your application might do, you can change the sleep duration to any value you want. Please note that since this method will be called inside the DoWork
event, it is the Background
thread that will sleep for the given duration, and not the main thread. The function will be as follows:
private int PerformHeavyOperation(int i)
{
System.Threading.Thread.Sleep(250);
return i * 1000;
}
Switch to the Designer mode, double click the Start button to begin handling its click event. What we will do is to capture the numeric value from the numeric up down control, pass this value to the asynchronous thread, and instruct the background worker to start working. We need to capture the numeric value at this stage, because once we are inside the new thread, we will not be able to capture any input from the user controls. To start the execution of the background thread, we will call the RunWorkerAsync
method. This method can accept an object argument, this object will be passed to the background thread. Inside this object, we can put any values that we captured from the user controls. To pass more than one value, we can use an array of objects. Below is the complete code for the btnStart_Click
event handler, Please note that a worker that is already in progress cannot be called again, you will get a runtime error if you attempt to do so.
private void btnStart_Click(object sender, EventArgs e)
{
int numericValue = (int)numericUpDownMax.Value;
object[] arrObjects = new object[] { numericValue };
if (!myWorker.IsBusy)
{
btnStart.Enabled = false;
myWorker.RunWorkerAsync(arrObjects);
}
}
Now inside the DoWork
event handler, we will do all of our heavy work. First, we will capture the objects we received from the main thread, then we will process them, finally we will pass the result back to the main thread, to be able to display it to the user. Keep in mind that only the main thread has access to the user controls. Also while we are processing the values, we will be continuously doing two things:
- Reporting progress to the main thread using the
ReportProgress
method, to show the user where we are now. - Checking the
CancellationPending
property of the background worker, to check it the user has issued a cancellation command.
Finally, we will be putting our result inside the Result
property of the DoWorkEventArgs
argument to be captured by the main thread inside the RunWorkerCompleted
event. Below is the complete code for our DoWork
handler:
protected void myWorker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker sendingWorker =
(BackgroundWorker)sender;
object[] arrObjects =
(object[])e.Argument;
int maxValue = (int)arrObjects[0];
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= maxValue; i++)
{
if (!sendingWorker.CancellationPending)
{
sb.Append(string.Format("Counting number: {0}{1}",
PerformHeavyOperation(i), Environment.NewLine));
sendingWorker.ReportProgress(i);
}
else
{
e.Cancel = true;
break;
}
}
e.Result = sb.ToString();
}
Now we will handle the ProgressChanged
event. Here, we will just capture the integer value we have sent from the background thread when we called the ReportProgress
method. Please note that you can pass objects of any datatype
, by using the UserState
of the ProgressChangedEventArgs
argument. One thing you can do here other than showing a status message is to use a progress bar to show the current progress.
protected void myWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
lblStatus.Text = string.Format("Counting number: {0}...", e.ProgressPercentage);
}
Now for the RunWokerCompleted
event. Here, we first need to check if the worker has been canceled, or if an error had occurred. After that, we will collect the result that the background worker has calculated for us, and display it to the user:
protected void myWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (!e.Cancelled &&
e.Error == null)
{
string result = (string)e.Result;
txtResult.Text = result;
lblStatus.Text = "Done";
}
else if (e.Cancelled)
{
lblStatus.Text = "User Canceled";
}
else
{
lblStatus.Text = "An error has occurred";
}
btnStart.Enabled = true;
}
One last thing remains, we need to implement the cancellation button. Double click the cancel button, and inside the code class, call the CancelAsync
method of the background worker. This will set the CancellationPending
flag to true
. We were checking this flag at each loop iteration back in the DoWork
event handler. From this, we can conclude that terminating a backgroundworker
currently in progress will not happen immediately, if the background worker is doing something, we have to wait for the worker to finish it before being able to cancel the operation. Below is the code for the btnCancel_Click
:
private void btnCancel_Click(object sender, EventArgs e)
{
myWorker.CancelAsync();
}
Finally, here is a snapshot of the application in progress:
And here is a snapshot when the operation completed:
You can download the source code from the link at the top of this article, thanks for following up. :)
Next: Check my updated basic sample article on the new C# Task Factory:
Multithreading using Task Factory, C#, Basic Sample