Implementing ShowDialogAsync()
as in @noseratio's answer is fine for many scenarios.
Still, a drawback for time-consuming computational background-work is that cancellation of the dialog (i.e. form.Close()
) will just close the dialog right away, before the background-work can be completed or cancelled.
In my scenario, this caused non-respondance of the main application for a few seconds, as cancelling the background-work took a while.
To solve this, the dialog cancellation must be delayed/refused until the work is really done/cancelled yet (i.e. finally
is reached). On the other hand, a closing attempt should notify observers about the desired cancellation, which can be realized with a CancellationToken
. The token can then be used to cancel the work by ThrowIfCancellationRequested()
, throwing an OperationCanceledException
, which can be handled within a dedicated catch
statement.
There are a few ways how this can be realized, however I preferred using(Disposable)
over the roughly equivalent try
/finally
.
public static class AsyncFormExtensions
{
/// <summary>
/// Asynchronously shows the form as non-blocking dialog box
/// </summary>
/// <param name="form">Form</param>
/// <returns>One of the DialogResult values</returns>
public static async Task<DialogResult> ShowDialogAsync(this Form form)
{
// ensure being asynchronous (important!)
await Task.Yield();
if (form.IsDisposed)
{
return DialogResult.Cancel;
}
return form.ShowDialog();
}
/// <summary>
/// Show a non-blocking dialog box with cancellation support while other work is done.
/// </summary>
/// <param name="form">Form</param>
/// <returns>Non-blocking disposable dialog</returns>
public static DisposableDialog DisposableDialog(this Form form)
{
return new DisposableDialog(form);
}
}
/// <summary>
/// Non-blocking disposable dialog box with cancellation support
/// </summary>
public class DisposableDialog : IAsyncDisposable
{
private Form _form;
private FormClosingEventHandler _closingHandler;
private CancellationTokenSource _cancellationTokenSource;
/// <summary>
/// Propagates notification that dialog cancelling was requested
/// </summary>
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
/// <summary>
/// Awaitable result of ShowDialogAsync
/// </summary>
protected Task<DialogResult> ResultAsync { get; }
/// <summary>
/// Indicates the return value of the dialog box
/// </summary>
public DialogResult Result { get; set; } = DialogResult.None;
/// <summary>
/// Show a non-blocking dialog box with cancellation support while other work is done.
///
/// Form.ShowDialogAsync() is used to yield a non-blocking async task for the dialog.
/// Closing the form directly with Form.Close() is prevented (by cancelling the event).
/// Instead, a closing attempt will notify the CancellationToken about the desired cancellation.
/// By utilizing the token to throw an OperationCanceledException, the work can be terminated.
/// This then causes the desired (delayed) closing of the dialog through disposing.
/// </summary>
public DisposableDialog(Form form)
{
_form = form;
_cancellationTokenSource = new CancellationTokenSource();
_closingHandler = new FormClosingEventHandler((object sender, FormClosingEventArgs e) => {
// prevent closing the form
e.Cancel = true;
// Store the desired result as the form withdraws it because of "e.Cancel=true"
Result = form.DialogResult;
// notify about the cancel request
_cancellationTokenSource.Cancel();
});
form.FormClosing += _closingHandler;
ResultAsync = _form.ShowDialogAsync();
}
/// <summary>
/// Disposes/closes the dialog box
/// </summary>
/// <returns>Awaitable task</returns>
public async ValueTask DisposeAsync()
{
if (Result == DialogResult.None)
{
// default result on sucessful completion (would become DialogResult.Cancel otherwise)
Result = DialogResult.OK;
}
// Restore the dialog result as set in the closing attempt
_form.DialogResult = Result;
_form.FormClosing -= _closingHandler;
_form.Close();
await ResultAsync;
}
}
Usage example:
private async Task<int> LoadDataAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
// do some work
await Task.Delay(500);
// if required, this will raise OperationCanceledException to quit the dialog after each work step
cancellationToken.ThrowIfCancellationRequested();
}
return 42;
}
private async void ExampleEventHandler(object sender, EventArgs e)
{
var progressForm = new Form();
var dialog = progressForm.DisposableDialog();
// show the dialog asynchronously while another task is performed
await using (dialog)
{
try
{
// do some work, the token must be used to cancel the dialog by throwing OperationCanceledException
var data = await LoadDataAsync(dialog.CancellationToken);
}
catch (OperationCanceledException ex)
{
// Cancelled
}
}
}
I've created a Github Gist for the code with a full example.