Click here to Skip to main content
15,884,885 members
Articles / Desktop Programming / Windows Forms

Using Another Thread in a Winform to do a Computation

Rate me:
Please Sign up or sign in to vote.
2.82/5 (9 votes)
14 Oct 2007CPOL7 min read 43.5K   348   36   10
Shows how to update the UI of a WinForm by using a different thread
Screenshot - thread_gif.gif

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

C#
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

C#
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.

C#
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:

C#
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:

C#
private HoldStatus currentstatus;
private bool cancel = false;

The main method of this class is DoComputation and it takes one parameter.

C#
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.

C#
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():

C#
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:

C#
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.

C#
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
 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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
Currently working as an application/web developer for a Hospital in New York City.
I also do consulting work on the side.

Comments and Discussions

 
GeneralIncorrect implementation Pin
Sitten Spynne15-Oct-07 4:31
Sitten Spynne15-Oct-07 4:31 
GeneralRe: Incorrect implementation Pin
Hector Pacheco15-Oct-07 12:49
Hector Pacheco15-Oct-07 12:49 
GeneralRe: Incorrect implementation Pin
Sitten Spynne15-Oct-07 16:32
Sitten Spynne15-Oct-07 16:32 
GeneralRe: Incorrect implementation Pin
Hector Pacheco16-Oct-07 2:55
Hector Pacheco16-Oct-07 2:55 
GeneralThanks Pin
ooxoo1114-Oct-07 23:54
professionalooxoo1114-Oct-07 23:54 
QuestionWhy not just using the BackgroundWorker? Pin
Itay Sagui14-Oct-07 10:55
Itay Sagui14-Oct-07 10:55 
AnswerRe: Why not just using the BackgroundWorker? Pin
Hector Pacheco14-Oct-07 14:38
Hector Pacheco14-Oct-07 14:38 
GeneralRe: Why not just using the BackgroundWorker? Pin
Itay Sagui14-Oct-07 21:34
Itay Sagui14-Oct-07 21:34 
As I was saying, the BackgroundWorker component uses QueueUserWorkItem too. You just created something that comes out-of-the-box in the .Net framework.

What does your code gives the developer that is not already available?

Itay Sagui  |  Tzunami Inc
Tel: +972-9-9507479  |  Mobile: +972-54-5343800  |  Email: itay@tzunami.com

GeneralRe: Why not just using the BackgroundWorker? Pin
Hector Pacheco15-Oct-07 6:23
Hector Pacheco15-Oct-07 6:23 
GeneralRe: Why not just using the BackgroundWorker? Pin
Rei Miyasaka16-Oct-07 11:32
Rei Miyasaka16-Oct-07 11:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.