Blog Archives

Using BackgroundWorker to get around non-responsive UI

Think about this scenario: you are developing windows forms application, there is a button, when clicked, the whole application stopped responding, because it is doing something heavy in the background. Sounds familiar? This is most typical when user demands the application to show status message when a process is running, or to show progress status when a process is running, while keeping the UI responsive.

As we all know it, the easiest way of implementing logic behind a button is to use the OnClick event handler, but if we expect a process to take more than a second, then we might want to rethink whether or not it should be executed on the UI thread. Yes, thread – and first reaction I often get is “oh no! that can of worms!”. OK let me give you a mentality standpoint here, developers who run away from learning core concepts and new technologies should just get another job and click on get off my lawn.

If you are still here, congratulations, you are about to find out a very simple way of implementing threading in WinForm. For the purpose of this, I will be using a very simple Winform app. This is not the evil Application.DoEvents, which I stay away from.

When the Go button is clicked, it will simulate a time-consuming routine by counting from 1 to 10, pausing every second in between counts. As it counts, it will report its progress on the progress bar.
When the Add button is clicked, it simply increments the value next to it.

What we notice is that when the go button is clicked, clicking on the Add button will not increment the value, which shows that the UI is not responsive.

illustration

What we want is a responsive UI, so when the Go button is clicked, it does not affect other parts of the form. So let’s get our hands dirty by first creating this simple app in few simple steps:

  1. Open Visual Studio, create a Winform app
  2. Put a button, name it btnAdd, double click on it to allow Visual Studio to generate a click event handler method for you
  3. Put a label next to btnAdd, name it lblValue
  4. Put a button, name it btnGo, double click on it to allow Visual Studio to generate a click event handler method for you
  5. Put a progress bar, name it progressBar, set the maximum to 10
  6. Go to source view and paste in the following code
private void btnAdd_Click(object sender, EventArgs e)
{
    lblValue.Text = (int.Parse(lblValue.Text) + 1).ToString();
}
private void btnGo_Click(object sender, EventArgs e)
{
    btnGo.Enabled = false;
    TimeConsuming(progressBar.Maximum);
    btnGo.Enabled = true;
}

private void TimeConsuming(int maxValue)
{
    for (int i = 1; i     {
        progressBar.Value = (int) ((i / (double)maxValue) * progressBar.Maximum);
        Thread.Sleep(new TimeSpan(0, 0, 1));
    }
}

And Voila! we have just created an unresponsive app 🙂

Now let us enhance this. Drag a Backgroundworker (BackgroundWorker) component from the toolbox onto the form, name it bgw.
Pay attention to the properties of the bsw control. This is what we will use to execute our time-consuming code. Because I want to report back to the UI thread during processing, I need to set the bgw.WorkerReportsProgress property to true.
Now switch to the events for the bsw component. Double click on each of the event (DoWork, ProgressChanged and RunWorkerCompleted) to let Visual Studio generate the event handler methods for you. We will implement these methods later, but for now, here is a brief overview:

private void bgw_DoWork(object sender, DoWorkEventArgs e)
{
    //your time consuming process goes here
}

private void bgw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    //thigs which should happen when the worker thread reports a progress
}

private void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    //things which should happen when the worker thread completes
}

Let us refactor the code that handles the button click event. We will make it run a worker thread instead of calling the time consuming routine, and make the DoWork method call the time consuming routine. Also remember to take out the line that enables the button.

private void btnGo_Click(object sender, EventArgs e)
{
    btnGo.Enabled = false;
    if (!bgw.IsBusy)
        bgw.RunWorkerAsync(progressBar.Maximum);
}

private void bgw_DoWork(object sender, DoWorkEventArgs e)
{
    TimeConsuming(bgw, (int)e.Argument);
}

Because the TimeConsuming routine now does not report progress directly to the UI, we will have to modify it a little bit, making it to report its progress to the worker thread instead:

private void TimeConsuming(BackgroundWorker worker, int maxValue)
{
    for (int i = 1; i     {
        worker.ReportProgress((int)((i / (double)maxValue) * 100));
        Thread.Sleep(new TimeSpan(0, 0, 1));
    }
}

The rest is simple: Whenever the progress changed is reported, we simply update the value of progress bar based on percentage reported and when the worker thread completes, we enable the go button:

private void bgw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    progressBar.Value = (int)((e.ProgressPercentage / 100.0) * progressBar.Maximum);
}

private void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    btnGo.Enabled = true;
}

Here is the complete source code of the form, the original TimeConsuming method have been renamed to TimeConsumingOnUIThread for the ease of testing in case we need to switch back to the old scenario:

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

    private void btnAdd_Click(object sender, EventArgs e)
    {
        lblValue.Text = (int.Parse(lblValue.Text) + 1).ToString();
    }

    private void btnGo_Click(object sender, EventArgs e)
    {
        btnGo.Enabled = false;
        if (!bgw.IsBusy)
            bgw.RunWorkerAsync(progressBar.Maximum);
        //TimeConsumingOnUIThread(progressBar.Maximum);
        btnGo.Enabled = true;
    }

    private void TimeConsumingOnUIThread(int maxValue)
    {
        for (int i = 1; i         {
            progressBar.Value = (int)((i / (double)maxValue) * progressBar.Maximum);
            Thread.Sleep(new TimeSpan(0, 0, 1));
        }
    }

    private void TimeConsuming(BackgroundWorker worker, int maxValue)
    {
        for (int i = 1; i         {
            worker.ReportProgress((int)((i / (double)maxValue) * 100));
            Thread.Sleep(new TimeSpan(0, 0, 1));
        }
    }

    private void bgw_DoWork(object sender, DoWorkEventArgs e)
    {
        TimeConsuming(bgw, (int)e.Argument);
    }

    private void bgw_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        progressBar.Value = (int)((e.ProgressPercentage / 100.0) * progressBar.Maximum);
    }

    private void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        btnGo.Enabled = true;
    }

}
%d bloggers like this: