Tuesday, 16 January 2007

Background workers

Im going to explain how to use background workers - but first lets start with an example.
We have a class appropriately named SlowClass (shown below)


public class SlowClass

{

int m_iWorkToDo = 0;

int m_iWorkDone = 0;

public SlowClass(int iWorkToDo)

{

m_iWorkToDo = iWorkToDo;

}

public void DoWork()

{

if (m_iWorkDone < m_iWorkToDo)

{

System.Threading.Thread.Sleep(1000);

m_iWorkDone++;

}

}

public int PercentComplete()

{

return (int)Math.Round(((double)m_iWorkDone / (double)m_iWorkToDo) * 100);

}

}
As you can see it will take 1 second for every bit of work it has to do. This is going to be called from this form:

via the following code:

public partial class frmDoWork : Form

{

public frmDoWork()

{

InitializeComponent();

}



private void btnDoWork_Click(object sender, EventArgs e)

{

int iWorkToDo = 0;

if (int.TryParse(txtWorkToDo.Text, out iWorkToDo))

{

SlowClass oSlowClass = new SlowClass(iWorkToDo);

for (int i = 0; i < iWorkToDo; i++)

{

oSlowClass.DoWork();

progressBar1.Value = oSlowClass.PercentComplete();

}

MessageBox.Show("Complete");

}

else

{

MessageBox.Show("Please enter a valid number");

}

}

}
What happens when you type a number and click do work is that the screen freezes for the number of seconds - the progress bar does not get updated, no feedback to the user, plus screen frozen might as well equal a crash!

This could be fixed by using threads, however threads do not allow for the update of visual items in other threads, so this is out of the questions as we need a progress bar

Say hello to background workers (if you are using .net 2 - if you are using less than this then you need to use aysnc threads... this is not going to get covered just yet I'm afraid).

Simply drag your background worker onto your form (shown below)



Now go into the properties and change WorkerReportsProgress to equal true. new we move the code from the button event to the Do work event of the worker process, and finally you need to modify the progress bar update code - probably best to just show you how the code should look:)


public partial class frmDoWork : Form
{
public frmDoWork()
{
InitializeComponent();
}

private void btnDoWork_Click(object sender, EventArgs e)
{
backgroundWorker1.RunWorkerAsync();
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
int iWorkToDo = 0;
if (int.TryParse(txtWorkToDo.Text, out iWorkToDo))
{
SlowClass oSlowClass = new SlowClass(iWorkToDo);
for (int i = 0; i < iWorkToDo; i++)
{
oSlowClass.DoWork();
backgroundWorker1.ReportProgress(oSlowClass.PercentComplete());
}
MessageBox.Show("Complete");
}
else
{
MessageBox.Show("Please enter a valid number");
}
}

private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
}
}
Problem Solved:)

You can also easily add in a cancel button, simply change the property "workerSupportsCancel" on the background worker to true and break out of the for loop doing the work if
backgroundWorker1.CancellationPending = true.

Far easier than all that async nonsense of .net 1:)

Any questions then leave a comment.

Ross

15 comments:

Anonymous said...

What a simple but nice example of how to use background worker threads. Thanks!

Farrakh said...

very nice article cuts throught he bumf to the juicy goodness.

Ross Dargan said...

Glad it helped:)

Anonymous said...

It is a very good article, makes this clear. I'm having a slight problem though. The calculation to get the current status consistently returns 0 until the end when it suddenly comes back with 100. If I replace it with a multiplication it works fine. (i.e. Hard code of m_iWorkDone * 20 for 5 seconds). Am I missing something?

Ross Dargan said...

Have you remembered to cast it as a double before doing the calculation - this often leads to the sort of issue you have described.

Anonymous said...

Hi Ross,

Yes I am using the following -
return (int)Math.Round((double)(m_iWorkDone / m_iWorkToDo) * 100);

I think it's the same as the example, but perhaps I've missed something out?

Ross Dargan said...

Try changing it to this:-

(int)Math.Round(((double)m_iWorkDone /(double) m_iWorkToDo) * 100);

lets see if that fixes it - if not send me an email (mail[at]the-dargans.co.uk)

Anonymous said...

That worked - thanks very much for the article and the help.

All the best
Andy

Ross Dargan said...

I have updated the post to reflect the change - thanks for the feedback Andy

Ross:)

saneeeha said...

What I understand is that this code is usefull if I know the duration of function in advance or i calculate with the help of loop. What if the function doesnt have a loop and i need to show the progress of the function...

Can I use a progress bar or backgroundworker to show the duration of my function??

Nicholas said...

Nice example. Was trying to blend some backgroundworker code from another example into my file uploader and couldn't get it to work. Your stripped version has helped big time. Added the following for the Cancel (if anyone is wondering how)

private void btnCancel_Click(object sender, EventArgs e)
{
backgroundWorker1.CancelAsync();
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
int iWorkToDo = 0;
bool cancelled = false;
if (int.TryParse(txtWorkToDo.Text, out iWorkToDo))
{
SlowClass oSlowClass = new SlowClass(iWorkToDo);
for (int i = 0; i < iWorkToDo; i++)
{
oSlowClass.DoWork();
backgroundWorker1.ReportProgress(oSlowClass.PercentComplete());
if (backgroundWorker1.CancellationPending == true)
{
backgroundWorker1.ReportProgress(0);
cancelled = true;
break;
}
}
if (!(cancelled))
{
MessageBox.Show("Complete");
}
else
{
MessageBox.Show("Cancelled");
}
}
else
{
MessageBox.Show("Please enter a valid number");
}
}

MoNo said...

Hello,

My program has encountered a problem using your approach! In the backgroundworker_DoWork I'm reading the value of a TextBox and calling a function from another class to process it. But I get an exception at this point. The exception says that the value of TextBox1.Text cannot be accessed because it's being accessed from another thread than the one it was created on! I don't really know how to handle this. Because in your code it seems like you can access the Text of a textbox smoothly and easily. Where does this problem come from? Any idea?

/MoNo

Ross Dargan said...

Hi Mono

This worked when I wrote it but that was a while ago! I believe a background worker has the option of passing in an argument to runworker async: http://msdn.microsoft.com/en-us/library/f00zz5b2(v=VS.85).aspx

Here you could pass in the value of your textbox.text, and simply cast it inside the dowork portion (e.Argument as string will give you your textbox value then).

Thanks

Ross

MoNo said...

Ok thank you for your response. However my problem is not as easy as that! I'm not only processing the value of a textBox. The process is being done on a number of textBoxes and a ComboBox.SelectedItem. Also I had implemented a couple of Try Catches in order to show an error message to the user and focus on the field that is causing the error message. Now that I've copied everything in the backgroundworker_Dowork I'm getting this exception and it cannot simply be fixed by passing a parameter! Any ideas?

Ross Dargan said...

Can you paste the code in, and show exactly where the error occurs.

Also you may be better posting a question to stack overflow - it's easier to see code you will post, and you will probably get an answer quicker!

Ross