Awaiting Asynchronous function inside FormClosing Event

你说的曾经没有我的故事 提交于 2020-01-08 21:51:54

问题


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.


回答1:


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.




回答2:


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

What I would do is creating a Thread and setting its IsBackground property to false (which is false by default) to keep the process alive while 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);
}



回答3:


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



回答4:


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



回答5:


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


来源:https://stackoverflow.com/questions/16656523/awaiting-asynchronous-function-inside-formclosing-event

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!