Introduction

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

When I develop a 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 arround it.

Using the code

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

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:

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:

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:

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 embed a BackgroundWorker and wraps its main functionnalities.

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

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 access easily the form members:

    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:

    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:

    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:

    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 wan't 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 return directly the result:

    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:

/// <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 usefull to some of you.

History

Revision 2: Added "How it works" section.

Revision 3: uploaded one more time SampleApplication.zip...

Revision 4: Added ProgressBar property and changed SetProgress functions so that
they call ReportProgress only if needed.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架