wpfasync-awaitformclosing

Wpf child form, OnClosing event and await


I have a child form launched form a parent form with:

ConfigForm cfg = new ConfigForm();
cfg.ShowDialog();

This child form is used to configure some application parameters. I want to check if there are some changes not saved, and if so, warn the user. So my On OnClosing event is declared this way:

private async void ChildFormClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
    // Here i call a function that compare the current config with the saved config
    bool isUptated = CheckUnsavedChanges();

    // If updated is false, it means that there are unsaved changes...
    if (!isUpdated)
    {
         e.Cancel = true;

        // At this point i create a MessageDialog (Mahapps) to warn the user about unsaved changes...
        MessageDialogStyle style = MessageDialogStyle.AffirmativeAndNegative;

        var metroDialogSettings = new MetroDialogSettings()
        {
            AffirmativeButtonText = "Close",
            NegativeButtonText = "Cancel"
        };

        var result = await this.ShowMessageAsync("Config", "There are unsaved changes, do you want to exit?", style, metroDialogSettings);

        // If we press Close, we want to close child form and go back to parent...
        if (result == MessageDialogResult.Affirmative)
        {
            e.Cancel = false;
        }
    }
}

My logic says that if i declare e.cancel to false it will continue closing the form, but it doesn't happen, the child form remains open.

My guess is that the async call is doing something i don't understand, because if i declare ChildFormClosing in this way:

private async void ChildFormClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
    bool isUptated = CheckUnsavedChanges();

    e.Cancel = true;

    if (!isUpdated)
    {
        MessageDialogStyle style = MessageDialogStyle.AffirmativeAndNegative;

        var metroDialogSettings = new MetroDialogSettings()
        {
            AffirmativeButtonText = "Close",
            NegativeButtonText = "Cancel"
        };

        var result = await this.ShowMessageAsync("Config", "There are unsaved changes, do you want to exit?", style, metroDialogSettings);

        if (result == MessageDialogResult.Affirmative)
        {
            e.Cancel = false;
        }
    }
    else
    {
        e.Cancel = false;
    }
}

The final else e.Cancel = false works and the child form is closed...

Any clue? Thanks!


Solution

  • Since this method is an event handler for a window, it will be called on the UI thread already, so there is no need to show the message box asynchronously.

    As for the strange behavior that you are seeing, this is related to the await in the event handler. When you await a method call, what is actually happening is that everything up until the await is executed as normal, but once the await statement is reach control returns to the caller. Once the method that is awaited upon returns, then the rest of the original method executes.

    The code that fires the OnClosing event is probably not designed with asynchronous event handlers in mind, so it assumes that if an event handler returns, it has finished whatever work it needs to do. Since your event handler sets CancelEventArgs.Cancel to true before it awaits on a method call, the caller to your event handler sees that it is set to true, so it doesn't close the form.

    This is why showing the message box synchronously works: the entire method is executed before control returns to the caller, so CancelEventArgs.Cancel is always set to its expected value.

    Raymond Chen recently posted two articles about async that might be interesting reading: Crash course in async and await and The perils of async void. The second article describes why async event handlers tend to not work how you expect them to.