Awaiting Asynchronous Function Inside Formclosing Event

Awaiting Asynchronous function inside FormClosing Event

The best answer, in my opinion, is to cancel the Form from closing. Always. Cancel it, display your dialog however you want, and once the user is done with the dialog, programatically close the Form.

Here's what I do:

async void Window_Closing(object sender, CancelEventArgs args)
{
var w = (Window)sender;
var h = (ObjectViewModelHost)w.Content;
var v = h.ViewModel;

if (v != null &&
v.IsDirty)
{
args.Cancel = true;
w.IsEnabled = false;

// caller returns and window stays open
await Task.Yield();

var c = await interaction.ConfirmAsync(
"Close",
"You have unsaved changes in this window. If you exit they will be discarded.",
w);
if (c)
w.Close();

// doesn't matter if it's closed
w.IsEnabled = true;
}
}

It is important to note the call to await Task.Yield(). It would not be necessary if the async method being called always executed asynchronously. However, if the method has any synchronous paths (ie. null-check and return, etc...) the Window_Closing event will never finish execution and the call to w.Close() will throw an exception.

Winform is disposed while still awaiting a task during the FormClosing event

what's actually going on

When you await an async method, the control flow returns to the caller of your Form1_FormClosing method. And when you didn't set the e.Cancel flag at this time, the form is closed.

The execution of your handler is resumed when the Task returned by DoCleanupAsync completed. But then the form is already closed.

Waiting for task to complete on form close

Because your event handler for form close is an async void, it returns back to the caller once you await, which results in the form continuing its close process and finishing. You will need to cancel the closing event and then call close again after doing your update. You will also want to ensure that the user cannot do anything between your cancelled close and your real close later. Something like:

private bool asyncCloseHack = true;
private async void frmSupportDetails_FormClosing(object sender, FormClosingEventArgs e)
{
if (asyncCloseHack)
{
e.Cancelled = true;
try
{
LockUserUI();
await AsyncTask();
}
finally
{
UnlockUserUI();
}
asyncCloseHack = false;
Close();
}
}

What happens when the user closes a form performing an async task in an async event handler?

When an async method such as your event handler hits an await, it will return to the caller. Once the awaitable method (IsSomethingValid or SendEmail) has returned, the remainder of the async method (btn_ButtonClick) will be executed.

The fact that you close the form before the awaitable method (IsSomethingValid or SendEmail) has returned doesn't stop the remainder of the async method (btn_ButtonClick) from being executed.

If you don't want to do anything after the form has been closed, you could use a flag that keeps track of whether it has been closed, e.g.:

public Form1()
{
InitializeComponent();
FormClosed += Form1_FormClosed;
}

private async void btn_ButtonClick(object sender, EventArgs e)
{
var isValid = await IsSomethingValid();
if (isValid && !_isClosed) //<--
{
MessageBox.Show("!");
}
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e) => _isClosed = true;

private async Task<bool> IsSomethingValid()
{
await Task.Delay(5000);
return true;
}

Would an async query also be executed even after closing its parent process?

No, or at least not the continuation. If you send an email and exit the application, this doesn't necessarily stop the email from being sent.

onload event listener is not invoked after awaiting on an asynchronous function

The problem is that by delaying setting up the event handler until the JSON is loaded, you're missing the load event.

In the vast majority of use cases (but from the comments, not yours), just remove the event handler entirely:

async function fetchJSON (url) {
const response = await fetch(url, {
headers: { accept: "application/json" }
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
}

const config = await fetchJSON("./config.json");
serverTest(0); // <==== Not wrapped in a `load` event handler

const serverList = new Map(Object.entries(config.servers));

function serverTest(index) {
//Code including serverList
}

If you make sure your script tag uses type="module" (which yours must do, if you're using top-level await like that), it won't be run until the page HTML is fully loaded and the DOM has been built. (Unless you also have the async attribute on it; details.)

In your use case, waiting for the load event genuinely does make sense, so you'll have to wait until both load and the fetch are done:

async function fetchJSON (url) {
const response = await fetch(url, {
headers: { accept: "application/json" }
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
}

// Hook up the `load` event *before* loading `config.json`,
// get a promise for when it fires
const loadPromise = new Promise(resolve => {
window.addEventListener("load", resolve);
});
// Load and wait for `config.json`
const config = await fetchJSON("./config.json");
// Wait for the `load` event (waits only ***very*** briefly
// if the event has already fired, otherwise waits for it
// to fire)
await loadPromise();
serverTest(0);

const serverList = new Map(Object.entries(config.servers));

function serverTest(index) {
//Code including serverList
}

Side note: Notice I added a check for ok before calling json. fetch only rejects its promise on network errors, not HTTP errors. This is (IMHO) a footgun in the fetch API I cover on my old anemic blog here.

Waiting for task to finish before closing form

The right way to deal with this is to cancel the Close event, and then close the form for real when the task completes. That might look something like this:

private async void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
cancelUpdater.Cancel(); // CancellationTokenSource
if (!updater.IsCompleted)
{
this.Hide();
e.Cancel = true;
await updater;
this.Close();
}
}

Now, in your comments, you wrote:

the Close() method called by a form B will return immediately and the changes that form B will make will crash the updater

Since you did not post any code related to "form B", it is not clear why or how it has anything to do with the current Form1 code. It's likely that there's a good way to fix this "form B" so that it cooperates better with the Form1 class and the object that's closing. But without seeing an actual good, minimal, complete code example that clearly shows this interaction, it's not possible to suggest how that would work.



Frankly, it is beyond being a really bad idea to block in any UI event handler. It is really important for the UI thread to be permitted to continue running unabated, and to do otherwise is to invite deadlock. You have of course here found one example of deadlock. But it is far from assured that even if you addressed this particular example, you could avoid all other instances of deadlock.

Blocking the UI thread is just asking for deadlock, among other problems.

That said, if you cannot resolve this issue with "form B" and really feel you must block the thread, you could make the cross-thread invoke use BeginInvoke() instead of Invoke() (which makes the invoke itself asynchronous, so that your "update thread" will be able to continue running and then terminate). Of course, should you do that, you will have to change the code to deal with the fact that by the time your invoked code is running, the form has been closed. That may or may not be a simple fix.



All that said, while I can't be sure lacking a good code example, I strongly suspect that you really should not have this updater task in the first place, and instead should be using the System.Windows.Forms.Timer class. That class is specifically designed for dealing with periodic events that have to be executed in the UI thread.

For example:

First, drag and drop a Timer object onto your form in the designer. By default, the name will be timer1. Then set the Interval property to the 1000 millisecond delay you're using in your task. Also, change your Updater() method so it's declared as timer1_Tick(object sender, EventArgs e) and use that as the event handler for the timer's Tick event.

Then, change your code so it looks something like this:

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
timer1.Stop();
}

private void Form1_Shown(object sender, EventArgs e)
{
timer1.Start();
}

void timer1_Tick(object sender, EventArgs e)
{
// All that will be left here is whatever you were executing in the
// anonymous method you invoked. All that other stuff goes away.
...
}

Since the System.Windows.Forms.Timer class raises its Tick event on the UI thread, there are no thread race conditions. If you stop the timer in the FormClosing event, that's it. The timer's stopped. And of course, since the timer's Tick event is raised on the UI thread, there's no need to use Invoke() to execute your code.



IMHO, the above is the very best answer that can be offered given the information in the question. If you feel none of the above is useful or applies, please edit your question to provide all of the relevant details, including a good code example.

Wait for eventhandlers, if it is declared async

How does the Window - if it actually does - wait for this code to finish to know if I set the Cancel member, and actually cancel the close of the window.

It doesn't.

The normal pattern there is to always cancel the close of the window (possibly replacing it with a "hide" instead of close), do the (asynchronous) operation, and then do the actual close.

How could I await for all event handlers to really finish even if they are declared as async?

Well, there are ways to do this, as noted on my blog. But here's the thing: your event-invoking code must await for all the handlers to complete one way or another. And since you can't await in a property setter, awaiting the event handlers won't do you any good.

Like the WPF workaround, the best solution is probably a broader re-design. In this case, you can introduce the idea of (a queue of) pending changes that are only applied after all asynchronous checks have been done.

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

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();
}
}
}


Related Topics



Leave a reply



Submit