Smolakian
Smolakian

Reputation: 414

Cancelling window closing with a task. How can I detect if task returned synchronously?

I am following a fairly common pattern for confirming/canceling my main window closing with an async dialog method. However, in the async task that I call to present the dialog, there are conditions where I return a boolean value immediately instead awaiting the return of a dialog task method. In those cases an exception is thrown:

System.InvalidOperationException: 'Cannot set Visibility to Visible or call Show, ShowDialog, Close, or WindowInteropHelper.EnsureHandle while a Window is closing.'

It seems that this is because the async task is returning synchronously and calling Close() on the window instead of calling the rest of the code as a continuation. Besides just wrapping Close() in a try/catch or adding a Task.Delay() in my function before returning my bool, is there a way to detect if I should call Close() on my window? (ie. if the task returned synchronously)

Or...am I conceptually missing something in the async/await pattern?

Here is my code:

private bool _closeConfirmed;

private async void MainWindow_OnClosing(object sender, CancelEventArgs e)
{
    //check if flag set
    if(!_closeConfirmed)
    {
        //use flag and always cancel first closing event (in order to allow making OnClosing work as as an async function)
        e.Cancel = true;

        var cancelClose = await mainViewModel.ShouldCancelClose();

        if(!cancelClose)
        {
            _closeConfirmed = true;
            this.Close();
        }
    }
}

Here's what the async function looks like:

public async Task<bool> ShouldCancelClose()
{
    if(something)
    {
        var canExit = await (CurrentMainViewModel as AnalysisViewModel).TryExit();

        if (!canExit) //if user cancels exit
            return true;

        //no exception
        return false;
    }

    //this causes exception
    return false;
}

Upvotes: 2

Views: 1762

Answers (1)

Gabriel Luci
Gabriel Luci

Reputation: 40928

The exception is saying that you cannot call Close() while the OnClosing event is in the process of running. I think you understand that.

There are two ways to handle this.

First, the answer mentioned by Herohtar in the comments used await Task.Yield().

More specifically, the key is awaiting any incomplete Task.

The reason is because async methods start running synchronously, just like any other method. The await keyword only does anything significant if it is given an incomplete Task. If it is given a Task that is already completed, the method continues synchronously.

So let's walk through your code. First let's assume that something is true:

  1. MainWindow_OnClosing starts running, synchronously.
  2. ShouldCancelClose starts running synchronously.
  3. TryExit() is called and returns an incomplete Task.
  4. The await keyword sees the incomplete Task and returns an incomplete Task. Control is returned to MainWindow_OnClosing.
  5. The await in MainWindow_OnClosing sees an incomplete Task, so it returns. Since the return type is void, it returns nothing.
  6. Control is returned to the form, and since it cannot await the rest of MainWindow_OnClosing, it assumes the event handler is finished.
  7. Whenever TryExit() finishes, the rest of ShouldCancelClose and MainWindow_OnClosing runs.
  8. If Close() is called now, it works, because as far as the form knows, the event handler finished at step 6.

Now let's assume that something is false:

  1. MainWindow_OnClosing starts running, synchronously.
  2. ShouldCancelClose starts running synchronously.
  3. ShouldCancelClose returns a completed Task with a value of false.
  4. The await keyword in MainWindow_OnClosing sees the completed Task and continues running the method synchronously.
  5. When Close() is called, it throws the exception because the event handler has not finished running.

So using await Task.Yield() is just a way to await something incomplete so that control is returned to the form so it thinks the event handler has finished.

Second, if you know that no asynchronous code has run, then you can rely on e.Cancel to cancel the closing or not. You can check by not awaiting the Task until you know if it's complete or not. That could look something like this:

private bool _closeConfirmed;

private async void MainWindow_OnClosing(object sender, CancelEventArgs e)
{
    //check if flag set
    if(!_closeConfirmed)
    {

        var cancelCloseTask = mainViewModel.ShouldCancelClose();

        //Check if we were given a completed Task, in which case nothing
        //asynchronous happened.
        if (cancelCloseTask.IsCompleted)
        {
            if (await cancelCloseTask)
            {
                e.Cancel = true;
            }
            else
            {
                _closeConfirmed = true;
            }
            return;
        }

        //use flag and always cancel first closing event (in order to allow making OnClosing work as as an async function)
        e.Cancel = true;

        if(!await cancelCloseTask)
        {
            _closeConfirmed = true;
            this.Close();
        }
    }
}

Upvotes: 7

Related Questions