等待 FormClosing 事件中的异步函数

问题描述 投票:0回答:6

我遇到一个问题,我无法等待 FormClosing 事件中的异步函数,该函数将确定表单关闭是否应该继续。我创建了一个简单的示例,如果您关闭而不保存(很像记事本或 Microsoft Word),它会提示您保存未保存的更改。我遇到的问题是,当我等待异步保存函数时,它会在保存函数完成之前继续关闭表单,然后在完成后返回关闭函数并尝试继续。我唯一的解决方案是在调用 SaveAsync 之前取消关闭事件,然后如果保存成功它将调用 form.Close() 函数。我希望有一种更清洁的方法来处理这种情况。

要复制该场景,请创建一个带有文本框 (txtValue)、复选框 (cbFail) 和按钮 (btnSave) 的表单。这是表格的代码。

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

}
}

编辑 05/23/2013

人们会问我为什么要尝试这样做是可以理解的 做这个。我们库中的数据类通常会有 Save, 设计为异步运行的 Load、New、Delete 函数 (请参阅 SaveAsync 作为示例)。我其实并没有那么在意 具体在 FormClosing 事件中异步运行该函数。但是如果 用户想在关闭表单之前保存,我需要等待 查看保存是否成功。如果保存失败,那么我希望它 取消表单关闭事件。我只是在寻找最干净的方法 处理这个。

c# .net-4.5 async-await formclosing
6个回答
62
投票

我认为最好的答案是取消关闭表格。总是。取消它,根据需要显示对话框,一旦用户完成对话框,以编程方式关闭表单。

我是这样做的:

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

重要的是要注意对

await Task.Yield()
的调用。如果被调用的异步方法 always 异步执行,则没有必要。但是,如果该方法有任何同步路径(即 null 检查和返回等),Window_Closing 事件将永远不会完成执行,并且对
w.Close()
的调用将引发异常。


1
投票

Dialogs 处理消息,同时仍将当前方法保留在堆栈中。

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

请记住,

SaveAsync
在非 UI 线程中运行,并且需要通过
Control.Invoke
编组任何访问 UI 元素(请参阅下面对
decoy.Hide
的调用)。最好的办法可能是事先从控件中提取任何数据,并且只在任务中使用变量。

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
}

0
投票

你不能用 async/await 来阻止你的表单关闭。你会得到奇怪的结果。

我要做的是创建一个

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

-1
投票

我在尝试处理所有关闭事件异步时遇到了类似的问题。我相信这是因为没有什么可以阻止主线程继续使用实际的 FormClosingEvents。只需在 await 之后放置一些内联代码即可解决问题。在我的例子中,无论响应如何(在等待响应时),我都会保存当前状态。你可以很容易地让任务返回一个当前状态,一旦用户响应就可以适当地保存。

这对我有用:分拆任务,询问退出确认,等待任务,一些内联代码。

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

-1
投票

如果在执行异步方法期间出现异常,我需要中止关闭表单。

我实际上正在使用

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

-2
投票

为什么要涉及异步行为?这听起来像是必须以线性方式发生的事情。我发现最简单的解决方案通常是正确的。

替代我下面的代码,您可以让主线程休眠一两秒钟,并让异步线程在主线程中设置一个标志。

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;
        } } }
© www.soinside.com 2019 - 2024. All rights reserved.