Awaiting Asynchronous function inside FormClosing Event
Asked Answered
A

7

52

I'm having a problem where I cannot await an asynchronous function inside of the FormClosing event which will determine whether the form close should continue. I have created a simple example that prompts you to save unsaved changes if you close without saving (much like with notepad or microsoft word). The problem I ran into is that when I await the asynchronous Save function, it proceeds to close the form before the save function has completed, then it comes back to the closing function when it is done and tries to continue. My only solution is to cancel the closing event before calling SaveAsync, then if the save is successful it will call the form.Close() function. I'm hoping there is a cleaner way of handling this situation.

To replicate the scenario, create a form with a text box (txtValue), a checkbox (cbFail), and a button (btnSave). Here is the code for the form.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TestZ
{
public partial class Form1 : Form
{

    string cleanValue = "";

    public Form1()
    {
        InitializeComponent();
    }

    public bool HasChanges()
    {
        return (txtValue.Text != cleanValue);
    }

    public void ResetChangeState()
    {
        cleanValue = txtValue.Text;
    }

    private async void btnSave_Click(object sender, EventArgs e)
    {
        //Save without immediate concern of the result
        await SaveAsync();
    }

    private async Task<bool> SaveAsync()
    {
        this.Cursor = Cursors.WaitCursor; 
        btnSave.Enabled = false;
        txtValue.Enabled = false;
        cbFail.Enabled = false;

        Task<bool> work = Task<bool>.Factory.StartNew(() =>
        {
            //Work to do on a background thread
            System.Threading.Thread.Sleep(3000); //Pretend to work hard.

            if (cbFail.Checked)
            {
                MessageBox.Show("Save Failed.");
                return false;
            }
            else
            {
                //The value is saved into the database, mark current form state as "clean"
                MessageBox.Show("Save Succeeded.");
                ResetChangeState();
                return true;
            }
        });

        bool retval = await work;

        btnSave.Enabled = true;
        txtValue.Enabled = true;
        cbFail.Enabled = true;
        this.Cursor = Cursors.Default;

        return retval;            
    }


    private async void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        if (HasChanges())
        {
            DialogResult result = MessageBox.Show("There are unsaved changes. Do you want to save before closing?", "Unsaved Changes", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
            if (result == System.Windows.Forms.DialogResult.Yes)
            {
                //This is how I want to handle it - But it closes the form while it should be waiting for the Save() to complete.
                //bool SaveSuccessful = await Save();
                //if (!SaveSuccessful)
                //{
                //    e.Cancel = true;
                //}

                //This is how I have to handle it:
                e.Cancel = true; 
                bool SaveSuccessful = await SaveAsync();                    
                if (SaveSuccessful)
                {
                    this.Close();
                }
            }
            else if (result == System.Windows.Forms.DialogResult.Cancel)
            {
                e.Cancel = true;
            }

            //If they hit "No", just close the form.
        }
    }

}
}

Edit 05/23/2013

Its understandable that people would ask me why I would be trying to do this. The data classes in our libraries will often have Save, Load, New, Delete functions that are designed to be run asynchronously (See SaveAsync as an example). I do not actually care that much about running the function asynchronously in the FormClosing Event specifically. But if the user wants to save before closing the form, I need it to wait and see if the save succeds or not. If the save fails, then I want it to cancel the form closing event. I'm just looking for the cleanest way to handle this.

Ammamaria answered 20/5, 2013 at 19:3 Comment(4)
Using await in an event that fires a millisecond before your program terminates is not going to work well. You'll have to keep it alive.Squishy
I think the fact that you haven't gotten an answer by now would seem to indicate that what you're currently doing is probably the best method, or at least good enough. It doesn't seem super pretty, but about the only problem that I can think of as it stands is that someone could click the close button and choose save while it's currently saving the changes; you'll need to handle that and ensure save is only ever called once when that happens.Locomobile
You could be right, maybe there isn't a better way. As for preventing the user from clicking the save button while it is saving, in my real application I am handling that. Good thing to watch out for though when keeping the form responsive.Ammamaria
Related: .NET Async in shutdown methods?. An important detail is to await Task.Yield(); before closing the form programmatically, because otherwise you may get an exception in case the SaveAsync() completes synchronously.Gaucho
B
68

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, programmatically 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 is called always executed asynchronously. However, if the method has any synchronous paths (i.e. null-check and return, etc...) the Window_Closing event will never finish execution and the call to w.Close() will throw an exception.

Boatright answered 13/8, 2013 at 3:20 Comment(7)
This was my original work around for the problem. I was hoping it wasn't the real solution, but it seems that it really is the only way to handle it. I think the question has been up for long enough, so I'm marking this solution as the answer. Thanks for putting together the sample code!Ammamaria
In Windows Forms, setting Form.IsEnabled to true blocks any interaction, so doesn't make any sense to use here.Iscariot
In Windows Forms, disable all top-level controls, instead, to allow basic layout operations on the window while disabling any interaction inside the form.Iscariot
It works for me but I have to unregister Closing event before calling "this.Close()", otherwise Closing is called again and the confirmation window is shown again.Khaki
This does work in WPF. But as others have mentioned, you will need to unregister the Closing event handler (Window_Closing in the example above) before calling Close() or else you'll get infinite feedback.Whipperin
I made an edit to explain why the call to Task.Yield() is necessary. (Even though it would probably work fine in this example without it)Amatory
For a thorough explanation of how this pattern can be implemented in WPF see this answer: https://mcmap.net/q/343951/-awaiting-asynchronous-function-inside-formclosing-eventAmatory
Y
1

Dialogs handle messages while still keeping the current method on the stack.

You could show a "Saving..." Dialog in your FormClosing handler, and run the actual saving-operation in a new task, which programmatically closes the dialog once it's done.

Keep in mind that SaveAsync is running in a non-UI Thread, and needs to marshal any access UI elements via Control.Invoke (see call to decoy.Hide below). The best would probably be to extract any data from controls beforehand, and only use variables in the task.

protected override void OnFormClosing(FormClosingEventArgs e)
{
        Form decoy = new Form()
        {
                ControlBox = false,
                StartPosition = FormStartPosition.CenterParent,
                Size = new Size(300, 100),
                Text = Text, // current window caption
        };
        Label label = new Label()
        {
                Text = "Saving...",
                TextAlign = ContentAlignment.MiddleCenter,
                Dock = DockStyle.Fill,
        };
        decoy.Controls.Add(label);
        var t = Task.Run(async () =>
        {
                try
                {
                        //keep the form open if saving fails
                        e.Cancel = !await SaveAsync();
                }
                finally
                {
                        decoy.Invoke(new MethodInvoker(decoy.Hide));
                }
        });
        decoy.ShowDialog(this);
        t.Wait(); //TODO: handle Exceptions
}
Yurev answered 12/5, 2020 at 9:30 Comment(0)
C
0

You can't keep your form from closing with async/await. And you can get strange results.

What I would do is create a Thread and set its IsBackground property to false (which is false by default) to keep the process alive while the form is closing.

protected override void OnClosing(CancelEventArgs e)
{
    e.Cancel = false;
    new Thread(() => { 
        Thread.Sleep(5000); //replace this line to save some data.....
        MessageBox.Show("EXITED"); 
    }).Start();
    base.OnClosing(e);
}
Conti answered 20/5, 2013 at 19:27 Comment(5)
@Downvoter care to comment so that I can learn what is wrong with my code?Conti
You set e.Cancel to false which is the default value. And you close the form before the thread has a chance to run—if the process that would save the data fails, the form is already closed and the user loses work unless if you want to write code to redisplay and repopulate a new instance of the Form. Exactly what the OP wants to avoid ;-). At least you’re indirectly suggesting to prefer overriding the method instead of registering an event handler.Smallwood
@Smallwood Whether form is closed or not, there is no way for the app to exit before the thread finishes its work. (See it is not a background-thread, I deliberately didn't set it).Conti
So your variant of the Save() method always succeeds. Even when the disk is full or the network connection is unavailable—so of course there’s no need to ever redisplay the data to the user so that it can be memorized or copied to paper or saved a different way. Good to know.Smallwood
@Smallwood OK, you saw you were wrong and add additional comments :) First, User has nothing to do (even with the code you have in your mind), when the disk is full, if there is a solution, you can implement it in that thread too. Second, showing some info and even getting some confirmation is possible in another thread. You can learn how the create another message pump in my other answers.Conti
H
0

Just think simply

private async void _Closing(object sender, FormClosingEventArgs e)
{
    if(!CheckCanClose())
    {
        e.Cancel = true;//cancel close

        ShowUISavingProgress();//optional

        await SavingDataAsync();//and update progress

        AllowClose();

        this.Close();
    }
}
Hochstetler answered 6/12, 2023 at 10:20 Comment(0)
V
-1

I had a similar issue when I tried to handle all of the close event async. I believe it is because there is nothing to block the main thread from moving forward with the actual FormClosingEvents. Just put some inline code after the await and it solves the problem. In my case I save the current state no matter the response (while waiting for the response). You could easily have the task return a current state ready to be saved appropriately once the user responds.

This worked for me: Spin off task, ask exit confirmation, await task, some inline code.

    Task myNewTask = SaveMyCurrentStateTask();  //This takes a little while so I want it async in the background

    DialogResult exitResponse = MessageBox.Show("Are you sure you want to Exit MYAPPNAME? ", "Exit Application?", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);

            await myNewTask;

            if (exitResponse == DialogResult.Yes)
            {
                e.Cancel = false;
            }
            else
            {
                e.Cancel = true;
            }
Verticillaster answered 20/5, 2013 at 20:24 Comment(3)
In this case it will see that Cancel is set to false when it hits the await and stops blocking. At that point the form will be torn down and, if it's the main form, the whole process will be torn down at that point.Locomobile
@Locomobile I think you are right. The MessageBox was what was giving my save function enough time to finish every time. So I am interested if there is another way besides "My only solution is to cancel the closing event..., then if the save is successful it will call the form.Close() function. I'm hoping there is a cleaner way of handling this situation."Verticillaster
I think you could display an unclosable modal “saving” status dialog (you’d have to make your own form and then display it with ShowDialog() and have it close via Task.ContinueWith()). This way, the application’s event pump will keep running without the event having to return, preventing “(Not Responding)” and giving you a chance to set e.Cancel = true in the host form. If I have time I might try testing and posting an alternative answer later…Smallwood
A
-1

I needed to abort closing the form if an exeption was raised during the execution of an async method.

I'm actually using a Task.Run with .Wait()

private void Example_FormClosing(object sender, FormClosingEventArgs e)
{
    try
    {
        Task.Run(async () => await CreateAsync(listDomains)).Wait();
    }
    catch (Exception ex)
    {
        MessageBox.Show($"{ex.Message}", "Attention", MessageBoxButtons.OK, MessageBoxIcon.Error);
        e.Cancel = true;
    }
}
Aniline answered 9/8, 2017 at 10:11 Comment(0)
O
-2

Why does asynchronous behavior have to be involved? It sounds like something that has to happen in a linear fashion.. I find the simplest solution is usually the right one.

Alternatively to my code below, you could have the main thread sleep for a second or two, and have the async thread set a flag in the main thread.

void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    if (HasChanges())
    {
        DialogResult result = MessageBox.Show("There are unsaved changes. Do you want to save before closing?", "Unsaved Changes", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
        if (result == DialogResult.Yes)
        {
            e.Cancel = true; 
            if(!Save())
            {
                MessageBox.Show("Your work could not be saved. Check your input/config and try again");
                e.Cancel = true;
            }
        }
        else if (result == DialogResult.Cancel)
        {
            e.Cancel = true;
        } } }
Overbear answered 20/5, 2013 at 19:51 Comment(3)
This results in the form being marked as not responding, indicating to the user that something has gone wrong and the program has broken, even if it hasn't. They are then possibly going to kill the program, which could be very bad if it happens while saving.Locomobile
My SaveAsync function can also be called by a button click in which case I do want the screen to stay responsive while its working. I am considering the alternative of having the SaveAsync function call a separate non-async function just called Save(). Then I would do exactly what you are saying and just call the non asynchronous save function when inside of the form closing. But I'll hold out for other answers first.Ammamaria
Because if you make it non async, then you have to maintain a new non asynchronous version of your save method.Zavala

© 2022 - 2024 — McMap. All rights reserved.