Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

ProgressForm: A simple form linked to a BackgroundWorker

4.78/5 (35 votes)
1 Mar 2011CPOL3 min read 102.9K   7.9K  
ProgressForm automatically starts a BackgroundWorker when loaded, and provides a progress bar and a cancel button.

Introduction

This is my first article on CodeProject, I hope you will find it useful.

When I develop software, I often need to ask the user to wait for a long operation to finish, and usually allow him/her to cancel it. Whatever I do in that operation (it can be downloading a file, saving a big file to disk, etc.), I always need the same things:

  • I want the user to wait for the operation to finish through a modal dialog box
  • I want the user to see its progress
  • I want the user to be able to cancel it

Since I couldn't find a "ready to use" form for this purpose on the web (maybe I didn't search well?), I decided to write my own.

ProgressForm.JPG

Background

The BackgroundWorker class contains everything we need to achieve this. We just need to provide a dialog box around it.

Using the code

ProgressForm already contains a BackgroundWorker, all you need is to provide a method to do the work.

C#
ProgressForm form = new ProgressForm();
form.DoWork += new ProgressForm.DoWorkEventHandler(form_DoWork);
//if you want to provide an argument to your background worker
form.Argument = something;

To start the BackgroundWorker, just call ShowDialog. The return value will depend on how the worker finished:

C#
DialogResult result = form.ShowDialog();
if (result == DialogResult.Cancel)
{
     //the user clicked cancel
}
else if (result == DialogResult.Abort)
{
     //an unhandled exception occured in user function
     //you may get the exception information:
     MessageBox.Show(form.Result.Error.Message);
}
else if (result == DialogResult.OK)
{
     //the background worker finished normally
     //the result of the background worker is stored in form.Result
}

Finally, the worker method will look like this:

C#
void form_DoWork(ProgressForm sender, DoWorkEventArgs e)
{
    //get the provided argument as usual
    object myArgument = e.Argument;
 
    //do something long...
    for (int i = 0; i < 100; i++)
    {
        //notify progress to the form
        sender.SetProgress(i, "Step " + i.ToString() + " / 100...");
        
        //...
        
        //check if the user clicked cancel
        if (sender.CancellationPending)
        {
            e.Cancel = true;
            return;
        }
    }
}

If you want to change only the progress bar or only the progress text, SetProgress has several overloads:

C#
public void SetProgress(string status);
public void SetProgress(int percent);
public void SetProgress(int percent, string status);

And the last customizable things: there are two predefined strings: CancellingText and DefaultStatusText. CancellingText is the text that will be displayed if the user clicks Cancel.

How it works

ProgressForm just embeds a BackgroundWorker and wraps its main functionalities.

First, I designed a form as shown on the picture. Then I added the BackgroundWorker with the main event handlers:

C#
public partial class ProgressForm : Form
{
    public ProgressForm()
    {
         InitializeComponent();
 
         worker = new BackgroundWorker();
         worker.WorkerReportsProgress = true;
         worker.WorkerSupportsCancellation = true;
         worker.DoWork += new System.ComponentModel.DoWorkEventHandler(worker_DoWork);
         worker.ProgressChanged += new ProgressChangedEventHandler(
             worker_ProgressChanged);
         worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(
             worker_RunWorkerCompleted);
    }
    void worker_DoWork(object sender, DoWorkEventArgs e)
    {
    }
    void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
    }
    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
    }
    BackgroundWorker worker;
}

Now we must expose to the user the DoWork event. I added a new delegate so we can easily access the form members:

C#
public delegate void DoWorkEventHandler(ProgressForm sender, DoWorkEventArgs e);
public event DoWorkEventHandler DoWork;

void worker_DoWork(object sender, DoWorkEventArgs e)
{
    //the background worker started
    //let's call the user's event handler
    if (DoWork != null)
        DoWork(this, e);
}

OK, we have our worker and the event for the user. Now we want the worker to start as soon as the form is displayed. Let's do this in the Load event:

C#
void ProgressForm_Load(object sender, EventArgs e)
{
    worker.RunWorkerAsync();
}

Now, let's write a method to notify the progress, and add code into our ProgressChanged event handler:

C#
public void SetProgress(int percent, string status)
{
    worker.ReportProgress(percent, status);
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    if (e.ProgressPercentage >= progressBar.Minimum &&
        e.ProgressPercentage <= progressBar.Maximum)
    {
        progressBar.Value = e.ProgressPercentage;
    }
    if (e.UserState != null)
        labelStatus.Text = e.UserState.ToString();
}

We are almost there. Now we just need to handle the Cancel button:

C#
void buttonCancel_Click(object sender, EventArgs e)
{
    //notify the worker we want to cancel
    worker.CancelAsync();
    //disable the cancel button and change the status text
    buttonCancel.Enabled = false;
    labelStatus.Text = "Cancelling..."
}

One last thing: we want to close the form automatically once the worker finishes, and since our worker will be started through the ShowDialog method, it would be nice if it could directly return the result:

C#
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    //ShowDialog return value will inform whether the worker finished properly or not
    if (e.Error != null)
        DialogResult = DialogResult.Abort;
    else if (e.Cancelled)
        DialogResult = DialogResult.Cancel;
    else
        DialogResult = DialogResult.OK;
    //close the form
    Close();
}

The main job is finished! I added a few more stuff like predefined strings for the status, preventing the status to be changed if a cancel is pending, or passing an argument to the worker function.

You will find here the complete source code for that class:

C#
/// <summary>
/// Simple progress form.
/// </summary>
public partial class ProgressForm : Form
{
    /// <summary>
    /// Gets the progress bar so it is possible to customize it
    /// before displaying the form.
    /// Do not use it directly from the background worker function!
    /// </summary>
    public ProgressBar ProgressBar { get { return progressBar; } }
    /// <summary>
    /// Will be passed to the background worker.
    /// </summary>
    public object Argument { get; set; }
    /// <summary>
    /// Background worker's result.
    /// You may also check ShowDialog return value
    /// to know how the background worker finished.
    /// </summary>

    public RunWorkerCompletedEventArgs Result { get; private set; }
    /// <summary>
    /// True if the user clicked the Cancel button
    /// and the background worker is still running.
    /// </summary>
    public bool CancellationPending
    {
        get { return worker.CancellationPending; }
    }

    /// <summary>
    /// Text displayed once the Cancel button is clicked.
    /// </summary>
    public string CancellingText { get; set; }
    /// <summary>
    /// Default status text.
    /// </summary>
    public string DefaultStatusText { get; set; }
    /// <summary>
    /// Delegate for the DoWork event.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">Contains the event data.</param>
    public delegate void DoWorkEventHandler(ProgressForm sender, DoWorkEventArgs e);
    /// <summary>
    /// Occurs when the background worker starts.
    /// </summary>
    public event DoWorkEventHandler DoWork;

    /// <summary>
    /// Constructor.
    /// </summary>
    public ProgressForm()
    {
        InitializeComponent();

        DefaultStatusText = "Please wait...";
        CancellingText = "Cancelling operation...";

        worker = new BackgroundWorker();
        worker.WorkerReportsProgress = true;
        worker.WorkerSupportsCancellation = true;
        worker.DoWork += new System.ComponentModel.DoWorkEventHandler(worker_DoWork);
        worker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
        worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(
            worker_RunWorkerCompleted);
    }

    /// <summary>
    /// Changes the status text only.
    /// </summary>
    /// <param name="status">New status text.</param>
    public void SetProgress(string status)
    {
        //do not update the text if it didn't change
        //or if a cancellation request is pending
        if (status != lastStatus && !worker.CancellationPending)
        {
            lastStatus = status;
            worker.ReportProgress(progressBar.Minimum - 1, status);
        }
    }
    /// <summary>
    /// Changes the progress bar value only.
    /// </summary>
    /// <param name="percent">New value for the progress bar.</param>
    public void SetProgress(int percent)
    {
        //do not update the progress bar if the value didn't change
        if (percent != lastPercent)
        {
            lastPercent = percent;
            worker.ReportProgress(percent);
        }
    }
    /// <summary>
    /// Changes both progress bar value and status text.
    /// </summary>
    /// <param name="percent">New value for the progress bar.</param>
    /// <param name="status">New status text.</param>
    public void SetProgress(int percent, string status)
    {
        //update the form is at least one of the values need to be updated
       if (percent != lastPercent || (status != lastStatus && !worker.CancellationPending))
       {
           lastPercent = percent;
           lastStatus = status;
           worker.ReportProgress(percent, status);
       }
    }

    private void ProgressForm_Load(object sender, EventArgs e)
    {
        //reset to defaults just in case the user wants to reuse the form
        Result = null;
        buttonCancel.Enabled = true;
        progressBar.Value = progressBar.Minimum;
        labelStatus.Text = DefaultStatusText;
        lastStatus = DefaultStatusText;
        lastPercent = progressBar.Minimum;
        //start the background worker as soon as the form is loaded
        worker.RunWorkerAsync(Argument);
   }

    private void buttonCancel_Click(object sender, EventArgs e)
    {
        //notify the background worker we want to cancel
        worker.CancelAsync();
        //disable the cancel button and change the status text
        buttonCancel.Enabled = false;
        labelStatus.Text = CancellingText;
    }

    void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        //the background worker started
        //let's call the user's event handler
        if (DoWork != null)
            DoWork(this, e);
    }

    void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        //make sure the new value is valid for the progress bar and update it
        if (e.ProgressPercentage >= progressBar.Minimum &&
            e.ProgressPercentage <= progressBar.Maximum)
        {
            progressBar.Value = e.ProgressPercentage;
        }
        //do not update the text if a cancellation request is pending
        if (e.UserState != null && !worker.CancellationPending)
            labelStatus.Text = e.UserState.ToString();
    }

    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        //the background worker completed
        //keep the resul and close the form
        Result = e;
        if (e.Error != null)
            DialogResult = DialogResult.Abort;
        else if (e.Cancelled)
            DialogResult = DialogResult.Cancel;
        else
            DialogResult = DialogResult.OK;
        Close();
    }

    BackgroundWorker worker;
    int lastPercent;
    string lastStatus;
}

Conclusion

This form is quite simple but since I use it often, I thought it would be useful to some of you.

History

  • Revision 2: Added the "How it works" section.
  • Revision 3: Uploaded SampleApplication.zip once more.
  • Revision 4: Added the ProgressBar property and changed the SetProgress functions so that they call ReportProgress only if needed.

License

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