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.
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.
ProgressForm form = new ProgressForm();
form.DoWork += new ProgressForm.DoWorkEventHandler(form_DoWork);
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)
{
}
else if (result == DialogResult.Abort)
{
MessageBox.Show(form.Result.Error.Message);
}
else if (result == DialogResult.OK)
{
}
Finally, the worker method will look like this:
void form_DoWork(ProgressForm sender, DoWorkEventArgs e)
{
object myArgument = e.Argument;
for (int i = 0; i < 100; i++)
{
sender.SetProgress(i, "Step " + i.ToString() + " / 100...");
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 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:
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:
public delegate void DoWorkEventHandler(ProgressForm sender, DoWorkEventArgs e);
public event DoWorkEventHandler DoWork;
void worker_DoWork(object sender, DoWorkEventArgs e)
{
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)
{
worker.CancelAsync();
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:
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
DialogResult = DialogResult.Abort;
else if (e.Cancelled)
DialogResult = DialogResult.Cancel;
else
DialogResult = DialogResult.OK;
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:
public partial class ProgressForm : Form
{
public ProgressBar ProgressBar { get { return progressBar; } }
public object Argument { get; set; }
public RunWorkerCompletedEventArgs Result { get; private set; }
public bool CancellationPending
{
get { return worker.CancellationPending; }
}
public string CancellingText { get; set; }
public string DefaultStatusText { get; set; }
public delegate void DoWorkEventHandler(ProgressForm sender, DoWorkEventArgs e);
public event DoWorkEventHandler DoWork;
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);
}
public void SetProgress(string status)
{
if (status != lastStatus && !worker.CancellationPending)
{
lastStatus = status;
worker.ReportProgress(progressBar.Minimum - 1, status);
}
}
public void SetProgress(int percent)
{
if (percent != lastPercent)
{
lastPercent = percent;
worker.ReportProgress(percent);
}
}
public void SetProgress(int percent, string status)
{
if (percent != lastPercent || (status != lastStatus && !worker.CancellationPending))
{
lastPercent = percent;
lastStatus = status;
worker.ReportProgress(percent, status);
}
}
private void ProgressForm_Load(object sender, EventArgs e)
{
Result = null;
buttonCancel.Enabled = true;
progressBar.Value = progressBar.Minimum;
labelStatus.Text = DefaultStatusText;
lastStatus = DefaultStatusText;
lastPercent = progressBar.Minimum;
worker.RunWorkerAsync(Argument);
}
private void buttonCancel_Click(object sender, EventArgs e)
{
worker.CancelAsync();
buttonCancel.Enabled = false;
labelStatus.Text = CancellingText;
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
if (DoWork != null)
DoWork(this, e);
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (e.ProgressPercentage >= progressBar.Minimum &&
e.ProgressPercentage <= progressBar.Maximum)
{
progressBar.Value = e.ProgressPercentage;
}
if (e.UserState != null && !worker.CancellationPending)
labelStatus.Text = e.UserState.ToString();
}
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
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.