Introduction:
For a project I'm working on I used a BackGroundWorker
to lift some heavy tasks away from the UI thread and report progress. For this I created some sort of modal dialog with a label and progress bar which were updated from the ProgressChanged
method of the backgroundworker.
I read a very interesting article on async/await
and I thought I might update my code, since the BackGroundWorker is, despite being handy, getting outdated anyway. After some more reading on this matter I found an article with code that had the backgroundworker instance inside the modal dialog form and not on the caller form.
This gave me the idea to do the same but using async/await instead. This is a first result, which is working, and I was wondering if I am doing things right. Any tips and/or improvements are much appreciated, certainly regarding scalability/extensibility and the use of async/await.
The form:
The code:
public partial class CancellableProgress : Form
{
private readonly IEnumerable<KeyValuePair<string, Action>> _tasks;
private CancellationTokenSource _cancelTokenSource;
private bool _isTaskCompleted;
public CancellableProgress(string message, Action action)
: this(new[] { new KeyValuePair<string, Action>(message, action) })
{
}
public CancellableProgress(IEnumerable<KeyValuePair<string, Action>> tasks)
{
InitializeComponent();
Load += CancellableProgress_Load;
_tasks = tasks;
}
private void CancellableProgress_Load(object sender, EventArgs e)
{
PerformTasks(_tasks);
}
private async void PerformTasks(IEnumerable<KeyValuePair<string, Action>> tasks)
{
_cancelTokenSource = new CancellationTokenSource();
var token = _cancelTokenSource.Token;
var progressHandler = new Progress<string>(value => MessageLabel.Text = value);
var progress = progressHandler as IProgress<string>;
try
{
await Task.Run(() =>
{
foreach (var task in tasks)
{
if (token.IsCancellationRequested)
{
token.ThrowIfCancellationRequested();
}
progress.Report(task.Key);
task.Value();
}
}, token);
_isTaskCompleted = true;
}
catch (OperationCanceledException)
{
MessageLabel.Text = @"Cancelled!";
}
catch (Exception ex)
{
MessageLabel.Text = ex.Message;
}
finally
{
_cancelTokenSource.Dispose();
DialogResult = _isTaskCompleted ? DialogResult.OK : DialogResult.Cancel;
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
CancelTaskButton.Enabled = false;
_cancelTokenSource.Cancel();
}
}
Example usage:
This is some code on how I use the modal form.
private void TaskButton_Click(object sender, EventArgs e)
{
var operations = new[]
{
new KeyValuePair<string, Action>("Operation 1", LongOperation),
new KeyValuePair<string, Action>("Second operation", LongOperationTwo)
};
using (var progress = new CancellableProgress(operations))
{
progress.ShowDialog();
}
}
private static void LongOperation()
{
for (var i = 1; i <= 5; i++)
System.Threading.Thread.Sleep(1000);
}
private static void LongOperationTwo()
{
for (var i = 1; i <= 5; i++)
System.Threading.Thread.Sleep(1000);
}
token.ThrowIfCancellationRequested
is only called between tasks. Given that you are using this in a modal dialog, that means when the user cancels, they will still have to wait until the current task finishes. An improvement would be to pass the cancellation token as a parameter to eachLongOperation
, which would test for cancel in each loop iteration,token.IsCancellationRequested
. Then the longest delay would be the time it takes for one iteration. \$\endgroup\$