Reputation: 27556
I'm trying to construct a progress/cancel form for use within my WinForms application that runs any await-able "operation", while providing the user with some progress information and an opportunity to cancel the operation.
Because the form is shown using ShowDialog()
, it's a modal form which nicely disables the form underneath - so I don't need to mess around with disabling all the controls on that other form.
They way I've implemented it, which I fully expect you to rip to shreds :-), is to await the result of the operation during the Form.Load
event handler, and then close the form once the operation is either completed (whether that be because it ran to completion, was cancelled, or raise an exception).
public partial class ProgressForm<T> : Form
{
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private Progress<string> _progress = new Progress<string>();
private Func<IProgress<string>, CancellationToken, Task<T>> _operation = null;
private Exception _exception = null;
private T _result = default(T);
public static T Execute(Func<IProgress<string>, CancellationToken, Task<T>> operation)
{
using (var progressForm = new ProgressForm<T>())
{
progressForm._operation = operation;
progressForm.ShowDialog();
if (progressForm._exception != null)
throw progressForm._exception;
else
return progressForm._result;
}
}
public ProgressForm()
{
InitializeComponent();
this._progress.ProgressChanged += ((o, i) => this.ProgressLabel.Text = i.ToString());
}
private async void ProgressForm_Load(object sender, EventArgs e)
{
try
{
this._result = await this._operation(this._progress, this._cancellationTokenSource.Token);
}
catch (Exception ex) // Includes OperationCancelledException
{
this._exception = ex;
}
this.Close();
}
private void CancelXButton_Click(object sender, EventArgs e)
{
if (this._cancellationTokenSource != null)
this._cancellationTokenSource.Cancel();
}
}
This is called like this:
int numberOfWidgets = ProgressForm<int>.Execute(CountWidgets);
...where CountWidgets()
is an await-able thing (in this case a function returning Task<int>
, with appropriate IProgress and CancellationToken
parameters).
So far it works pretty well, but there's one "feature" that I'd like to add. Ideally, I'd like the form to remain invisible for (say) one second, so that if the operation completes really quickly there's no "flicker" as the form is shown and then immediately hidden again.
So, my question is how to introduce the 1s delay before the form is shown. Obviously, I still want to start the operation immediately, and yet as soon as I "await" the result of the operation, I'm no longer in control (so to speak) because control will be returned to the caller of the Form.Load
event handler - which will continue the work of showing the form.
I suspect that essentially I really need a second thread, and that I need the operation to execute on that thread while I block the main UI thread. (I know that blocking the UI thread is frowned upon, but in this case I think it's actually what I need).
There are so many different ways of creating threads, etc. that I'm not sure how to do this in the new "async/await" world...
Upvotes: 0
Views: 457
Reputation: 457217
I think you'll have to separate out your "task runner" from your "dialog" in order to do this. First, a dialog that responds to progress and can issue a cancel:
public partial class ProgressForm : Form
{
private readonly CancellationTokenSource _cancellationTokenSource;
public ProgressForm(CancellationTokenSource cancellationTokenSource, IProgress<string> progress)
{
InitializeComponent();
_cancellationTokenSource = cancellationTokenSource;
progress.ProgressChanged += ((o, i) => this.ProgressLabel.Text = i.ToString());
}
public static void ShowDialog(CancellationTokenSource cancellationTokenSource, IProgress<string> progress)
{
using (var progressForm = new ProgressForm(cancellationTokenSource, progress))
{
progressForm.ShowDialog();
}
}
private void CancelXButton_Click(object sender, EventArgs e)
{
if (this._cancellationTokenSource != null)
this._cancellationTokenSource.Cancel();
}
}
Next, the actual "task runner":
public static class FriendlyTaskRunner
{
public static async Task<T> Execute<T>(Func<CancellationToken, IProgress<string>, Task<T>> operation)
{
var cancellationTokenSource = new CancellationTokenSource();
var progress = new Progress<string>();
var timeout = Task.Delay(1000);
var operationTask = operation(cancellationTokenSource.Token, progress);
// Synchronously block for either the operation to complete or a timeout;
// if the operation completes first, just return the result.
var completedTask = Task.WhenAny(timeout, operationTask).Result;
if (completedTask == operationTask)
return await operationTask;
// Kick off a progress form and have it close when the task completes.
using (var progressForm = new ProgressForm(cancellationTokenSource, progress))
{
operationTask.ContinueWith(_ => { progressForm.Close(); });
progressForm.ShowDialog();
}
return await operationTask;
}
}
Please note that synchronously blocking the UI thread may cause deadlocks - in this case, if operation
attempts to sync back to the UI thread, it would be blocked until after the timeout - so it's not a true "deadlock" but quite inefficient.
Upvotes: 1
Reputation: 35869
I wouldn't recommend keeping parts of what you have if you want to delay display of the form. I would invoke the operation independently then create a Timer
whose Tick
event handler checks to see if the task is complete and if it is, does nothing. Otherwise it should create the form, passing the IProgress<T>
and CancellationTokenSource
, and task into the form. You can still await a task that has already be started. For the task to be started, it will need the progress object and the cancellation token before the form is created--so that needs to be created independently...
Upvotes: 1