Introduction
One of the advantages of using a different thread to do a long computation in a WinForm application is the benefit of having a UI that does not freeze while the long computation completes. Furthermore, by using this technique, it is possible to have other WinForm controls be responsive while the computation takes place; this is not possible in a single threaded application because once a long operation takes place on a single threaded application, the UI will become un-responsive to the user.
Background
In order to make this as easy as possible to understand; I decided to use a for
loop that with each iteration would send the current value to the UI; I also included a cancel button to stop or break the for
loop.
I decided to use three classes to separate the logic and to make it easy to manage the status of the application.
Class HoldStatus
is used to hold status of the for
loop; the class has two private
variables to hold the current number of the for
loop and the number of times the loop is supposed to loop; the class also includes two getters to retrieve these values.
The HoldStatus Class
class HoldStatus
{
private readonly int currentValue;
private readonly int numOfIteration;
public HoldStatus(int currentValue, int numOfIteration)
{
this.currentValue = currentValue;
this.numOfIteration = numOfIteration;
}
public int GetNumOfIteration
{
get{ return numOfIteration;}
}
public int GetCurrentVAlue
{
get { return currentValue; }
}
}
The Second Class
The next class is called ReportProgress
and it inherits EventArgs
since updating the UI implies that this type of operation is an event. The class only has one private
variable of type HoldStatus
, and one getter; which returns an object of type HoldStatus.
The getter will return current value of the for
loop.
The ReportProgress Class
class ReportProgress : EventArgs
{
private readonly HoldStatus currentStatus;
public ReportProgress(HoldStatus status)
{
currentStatus = status;
}
public HoldStatus CurrentStatus
{
get { return currentStatus; }
}
}
The Third Class
The next class is called PerformLongComputation
; this class will hold all the methods that the main form will need to update the UI.
class PerformLongComputation
{
public event EventHandler <ReportProgress> OnUpdateProgress;
private HoldStatus currentstatus;
private bool cancel = false;
public HoldStatus CurrentStatus
{
get { return currentstatus; }
}
public PerformLongComputation() { }
public void DoComputation(int num)
{
currentstatus = new HoldStatus(0, num);
for (int i = 0; i < num; i++)
{
if (!cancel)
{
reportProgress(i + 1, num);
raiseprogress();
}
else
{
reportProgress(i + 1, num);
raiseprogress();
break;
}
}
}
private void reportProgress(int i,int num)
{
HoldStatus new_status = new HoldStatus(i, num);
System.Threading.Interlocked.Exchange(ref currentstatus, new_status);
}
public void raiseprogress()
{
if (OnUpdateProgress != null)
{
ReportProgress args = new ReportProgress(currentstatus);
OnUpdateProgress(this, args);
}
}
public void stopComputation(bool cancel)
{
this.cancel = cancel;
}
}
Let's go over this class.
Even though we will be using a different thread to do the long process; we still need to notify the main thread whenever there is an update in the for
loop. Thus, we need to use Events
. If you are not familiar with events, don't panic; events could be seen anywhere in almost all applications; for example; moving the mouse or clicking a button are examples of widely used events.
This type of programming is usually called, "Publisher and Subscribers", where the publisher does something and notifies the subscribers that something has happened. Thus, PerformLongComputation
class is the publisher in this example because all the methods that we need will live in this class; the subscriber will be the main form.
What makes the PerformLongComputation
a publisher is the following declaration:
public event EventHandler <reportProgress> OnUpdateProgress;
Thus any class that wishes to be notified on any changes in our for
loop must use the variable name OnUpdateProgress
.
The following variables are used for record keeping:
private HoldStatus currentstatus;
private bool cancel = false;
The main method of this class is DoComputation
and it takes one parameter.
public void DoComputation(int num)
{
currentstatus = new HoldStatus(0, num);
for (int i = 0; i < num; i++)
{
if (!cancel)
{
reportProgress(i + 1, num);
raiseprogress();
}
else
{
reportProgress(i + 1, num);
raiseprogress();
break;
}
}
}
Here is where we initialized the variable called currentstatus
. It is important to initialize this variable here; which will be obvious once we call method reportProgress
. this method takes two parameters, which is the current value of the for
loop, and the number of times the for
loop will loop.
private void reportProgress(int i,int num)
{
HoldStatus new_status = new HoldStatus(i, num);
System.Threading.Interlocked.Exchange(ref currentstatus, new_status);
}
Here we create a new instance of class HoldStatus
, HoldStatus
class holds values of the for
loop. Once we have a new class with the new values, the method System.Threading.Interlocked.Exchange
will swap the new_status
variable to currentstatus
variable; remember that the currentstatus
variable was initialized on the DoComputation
method.
The next method raiseprogress()
:
public void raiseprogress()
{
if (OnUpdateProgress != null)
{
ReportProgress args = new ReportProgress(currentstatus);
OnUpdateProgress(this, args);
}
}
This method is responsible for sending the current values of the for
loop to the class that has subscribed to it; in this case it would be the main form.
The if
statement just checks to see if there are any subscribers attached to this class. If there are subscribers, then the class ReportProgress
gets initialized with the values of type HoldStatus
, we then call the variable OnUpdateProgress
and pass the main object and the variable args
, which holds the current values.
There is only one more method to explain for this class; the method called stopComputation
; this method takes a bool
value as a parameter. This method is used to set the variable cancel
to true
; if the user clicks the cancel button on the main application, and it breaks the for
loop in the method DoComputation
. Also notice that before we break the loop, the methods reportProgress, raiseprogress
get called one more time to safely signal the user where the application stops the for
loop.
Now let's explain the main class that will use the three classes explained above:
public partial class Form1 : Form
{
PerformLongComputation per = new PerformLongComputation();
private delegate void callUIUpdater(ReportProgress args);
public Form1()
{
InitializeComponent();
per.OnUpdateProgress += new EventHandler<reportprogress />
(per_OnUpdateProgress);
}
public void goDoWork(object sender)
{
if (txt_num.Text != "")
{
per.DoComputation(int.Parse(txt_num.Text.ToString()));
}
else
{
MessageBox.Show("Text box is empty");
}
}
private void btn_compute_Click(object sender, EventArgs e)
{
per.stopComputation(false);
btn_cancel.Focus();
System.Threading.ThreadPool.QueueUserWorkItem
(new System.Threading.WaitCallback(this.goDoWork));
}
private void per_OnUpdateProgress(object sender , ReportProgress e)
{
if (InvokeRequired)
Invoke(new callUIUpdater(UpdateUI), e);
else
UpdateUI(e);
}
private void UpdateUI(ReportProgress e)
{
label1.Text = e.CurrentStatus.GetCurrentVAlue.ToString() +
" OF " + e.CurrentStatus.GetNumOfIteration.ToString();
Invalidate(true);
Update();
}
private void btn_cancel_Click(object sender, EventArgs e)
{
per.stopComputation(true);
}
}
In order to make this work; the main class will create an instance of class PerformLongComputation
and the class will also need to declare a delegate
to update the UI.
PerformLongComputation per = new PerformLongComputation();
private delegate void callUIUpdater(ReportProgress args);
Now that the class PerformLongComputation
has being initialized; we need to subscribe to this class. We do this in the constructor of the main class; and this is the syntax:
per.OnUpdateProgress += new EventHandler<ReportProgress>(per_OnUpdateProgress);
By doing the above, the main application will be able to listen and use the methods of class PerformLongComputation
; also notice that the signature of the above declaration has one argument, per_OnUpdateProgress
. What this means is that we must use this name to create a method in the main class that will be used to listen to any changes on the class PerformLongComputation
.
Let's now look at this method:
private void per_OnUpdateProgress(object sender , ReportProgress e)
{
if (InvokeRequired)
Invoke(new callUIUpdater(UpdateUI), e);
else
UpdateUI(e);
}
Since the main application will be using a different thread to do the work, and the main application is aware of that, we need to use an if
statement of InvokeRequired
property of the .NET Framework. This property is often used when a different thread wishes to use a method on the main thread. If the if
statement returns true
; then Invoke
method will be called, and it takes as arguments the delegate
that we declare at the beginning of the class and the value of the class ReportProgress
which in turn will call the local method UpdateUI
. UpdateUI
takes one parameter of type ReportProgress
, which hold the current values of the for
loop. If the statement returns false
, then it implies that the call took place in the same thread.
Let's see the UpdateUI
method:
private void UpdateUI(ReportProgress e)
{
label1.Text = e.CurrentStatus.GetCurrentVAlue.ToString() +
" OF " + e.CurrentStatus.GetNumOfIteration.ToString();
Invalidate(true);
Update();
}
Notice that we are showing the current values of the for
loop by getting them from the ReportProgress
class; the next thing that we need to be aware of is that since this method would be called from a different thread; we need to call the Invalidate(true)
method of the .NET Framework to repaint the control; and also the Update()
method of the .NET Framework to re-draw the control.
The next method to explain is goDoWork
:
public void goDoWork(object sender)
{
if (txt_num.Text != "")
{
per.DoComputation(int.Parse(txt_num.Text.ToString()));
}
else
{
MessageBox.Show("Text box is empty");
}
}
This method just calls the DoComputation
method of the class PerformLongComputation
and we pass a number to be used in the for
loop.
In order to make this application use a different thread to call to the DoComputation
method of PerformLongComputation
, I decided to use the Thread Pool
since it is the easy one to use; also there are advantages and disadvantages in using this pool. Advantages are that threads from the thread pool are re-usable; what this means is that as soon as a job gets done; the .NET Framework will recycle this thread and place it in the thread pool to be later re-used. The disadvantages are that threads from the thread pool cannot be paused, joined (wait for a thread to complete); but for the above example, it does not required custom type threads.
Below is the method that will use a thread from the thread pool:
private void btn_compute_Click(object sender, EventArgs e)
{
per.stopComputation(false);
System.Threading.ThreadPool.QueueUserWorkItem
(new System.Threading.WaitCallback(this.goDoWork));
}
I decided to use the QueueUserWorkItem
of the thread pool, the parameter inside new System.Threading.WaitCallback(this.goDoWork)
is where we pass the method to be executed by the external thread.
The last method btn_cancel_Click
:
private void btn_cancel_Click(object sender, EventArgs e)
{
per.stopComputation(true);
}
This just calls the stopComputation
method of the class PerformLongComputation
to set variable cancel
to true
; setting a flag to true
which will then force the for
loop to exit.
Enjoy!
History
- 14th October, 2007: Initial post