Hi, I'm Ray

I'm a software developer, full-time nerd and occasional human (while heavily caffeinated).

C#: Quick and easy multi-threading on Windows Forms App with BackgroundWorker

When I need to write a quick (and possibly dirty) utility for doing a task, I always find myself defaulting to a Windows Forms App (WinForms). I appreciate that its not the best platform, it’s showing its age and smells a little like stale biscuits. However, its the system I’m most comfortably with. My history with WinForms goes all the way back to .NET Framework 1.1 in 2005 but that’s a story for another time.

Today let’s talk about a common problem, you have code running and the application becomes unresponsive. If you’re running something that’s outputting to a textbox or using a control, the application takes considerably more time to run and locks up the form. How are you supposed to use the progress bar if the form just turns grey all the time?

Well, you’ve just come across the problem of having all your code running on one thread. Your application can’t update your form controls because its too business doing the thing you’re telling it to do. It’s all your fault, is what I’m telling you. lol

The solution is multi-threading. There are a few ways to add more threads to an application, the laziest of ways is the BackgroundWorker. For all practicality you need a few things for your thread to do:

  • start
  • stop
  • do-a-thing
  • done-the-thing

These are very technical terms, there will be a quiz later.

The BackgroundWorker gives your all of that:

  • RunWorkerAsync
  • CancelAsync
  • DoWork
  • RunWorkerCompleted

This is reasonably simple, the only inconvenience is that the code running in your thread can’t directly change the properties of controls in another thread. This is known as illegal cross thread calls but there’s an easy solution known as the InvokeRequired pattern.


if (control.InvokeRequired)
{
  // invoke method
}
else
{
  // method
}

Why doesn’t WinForms do this automatically? I have no idea. It’s just something we’re learned to deal with.

 

Let’s get started.

 

The example I’m using shows a randomly generated number every 500 milliseconds. The button1 starts and stops the thread. The running thread updates label1 with a randy number.

example-randy-form

 

1. Setup

This is the basic declaration and setup for the BackgroundWorker.


using System.Windows.Forms;

protected BackgroundWorker backgroundWorker1 = null;

backgroundWorker1 = new BackgroundWorker();
backgroundWorker1.WorkerReportsProgress = backgroundWorker1.WorkerSupportsCancellation = true;
backgroundWorker1.DoWork += backgroundWorker1_DoWork;
backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;

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

private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
}

 

2. Start/Stop Button


private void button1_Click(object sender, EventArgs e)
{
  if (backgroundWorker1.IsBusy)
  {
    backgroundWorker1.CancelAsync();
    return;
  }

  backgroundWorker1.RunWorkerAsync();
}

 

3. InvokeRequired

To avoid illegal cross-threading, we’re checking to see if invoking is required for the control, then invoking code. You can build invoke delegate patterns into controls but we’re doing this one ~dirty~.


protected void setLabelText(string text)
{
  if (label1.InvokeRequired)
  {
    label1.Invoke(new MethodInvoker(() => {
      label1.Text = text;
    }));
  }
  else
  {
    label1.Text = text;
  }
}

 

4. The Whole Things

This is the whole file bringing everything together.


  public partial class Form1 : Form
  {
    protected BackgroundWorker backgroundWorker1 = null;
    protected Random randy = null;

    public Form1()
    {
      InitializeComponent();

      randy = new Random();

      backgroundWorker1 = new BackgroundWorker();
      backgroundWorker1.WorkerReportsProgress = backgroundWorker1.WorkerSupportsCancellation = true;
      backgroundWorker1.DoWork += backgroundWorker1_DoWork;
      backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
    }

    private void button1_Click(object sender, EventArgs e)
    {
      if (backgroundWorker1.IsBusy)
      {
        backgroundWorker1.CancelAsync();
        return;
      }

      backgroundWorker1.RunWorkerAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
      while (true)
      {
        if (backgroundWorker1.CancellationPending)
        {
          break;
        }

        setLabelText(randy.Next().ToString());

        Thread.Sleep(500);
      }
    }

    private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
      label1.Text = "Fini";
    }

    protected void setLabelText(string text)
    {
      if (label1.InvokeRequired)
      {
        label1.Invoke(new MethodInvoker(() => {
          label1.Text = text;
        }));
      }
      else
      {
        label1.Text = text;
      }
    }

  }

 

I hope someone finds this useful.

The Author

Ray
  • Hi, I'm Ray. I'm a software developer, full-time nerd and occasional human (while heavily caffeinated).

Copyright © 2014-2019 Ray Lam. All Rights Reserved.