79188653

Date: 2024-11-14 12:03:22
Score: 0.5
Natty:
Report link

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.

Reasons:
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): @noseratio's
  • Low reputation (1):
Posted by: Jonas Donhauser