简体   繁体   English

在 FormClosing 事件中等待异步函数

[英]Awaiting Asynchronous function inside FormClosing Event

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.我遇到了一个问题,我无法在 FormClosing 事件中等待异步函数,该函数将确定表单关闭是否应该继续。 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).我创建了一个简单的示例,如果您关闭而不保存(很像记事本或 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.我遇到的问题是,当我等待异步 Save 函数时,它会在 save 函数完成之前关闭表单,然后在完成后返回关闭函数并尝试继续。 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.我唯一的解决方案是在调用 SaveAsync 之前取消关闭事件,然后如果保存成功它将调用 form.Close() 函数。 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).要复制该场景,请创建一个带有文本框 (txtValue)、复选框 (cbFail) 和按钮 (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编辑 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).我们库中的数据类通常具有 Save、Load、New、Delete 函数,这些函数旨在异步运行(参见 SaveAsync 作为示例)。 I do not actually care that much about running the function asynchronously in the FormClosing Event specifically.我实际上并不特别关心在 FormClosing 事件中异步运行函数。 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.我只是在寻找最干净的方法来处理这个问题。

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() .请务必注意对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.但是,如果该方法具有任何同步路径(即空检查和返回等),则 Window_Closing 事件将永远不会完成执行并且对w.Close()的调用将引发异常。

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.您可以在 FormClosing 处理程序中显示“正在保存...”对话框,并在新任务中运行实际的保存操作,该任务在完成后以编程方式关闭对话框。

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).请记住, SaveAsync在非 UI 线程中运行,并且需要通过Control.Invoke编组任何访问 UI 元素(请参阅下面对decoy.Hide调用)。 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 form open if saving fails
                        e.Cancel = !await SaveAsync();
                }
                finally
                {
                        decoy.Invoke(new MethodInvoker(decoy.Hide));
                }
        });
        decoy.ShowDialog(this);
        t.Wait(); //TODO: handle Exceptions
}

You can't keep your form from closing with async/await.您无法通过 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.我要做的是创建一个Thread并将其IsBackground属性设置为 false(默认情况下为 false),以在表单关闭时保持进程处于活动状态。

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

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.我相信这是因为没有什么可以阻止主线程与实际的 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;
            }

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()我实际上正在使用Task.RunTask.Run .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;
    }
}

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM