Reputation: 414
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
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
:
MainWindow_OnClosing
starts running, synchronously.ShouldCancelClose
starts running synchronously.TryExit()
is called and returns an incomplete Task
.await
keyword sees the incomplete Task
and returns an incomplete Task
. Control is returned to MainWindow_OnClosing
.await
in MainWindow_OnClosing
sees an incomplete Task
, so it returns. Since the return type is void
, it returns nothing.MainWindow_OnClosing
, it assumes the event handler is finished.TryExit()
finishes, the rest of ShouldCancelClose
and MainWindow_OnClosing
runs.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
:
MainWindow_OnClosing
starts running, synchronously.ShouldCancelClose
starts running synchronously.ShouldCancelClose
returns a completed Task
with a value of false
.await
keyword in MainWindow_OnClosing
sees the completed Task
and continues running the method synchronously.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