【问题标题】:Awaiting Asynchronous function inside FormClosing Event在 FormClosing 事件中等待异步函数
【发布时间】:2013-05-20 19:03:17
【问题描述】:

我遇到了一个问题,我无法在 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

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

【问题讨论】:

  • 在程序终止前一毫秒触发的事件中使用 await 将无法正常工作。你必须让它活着。
  • 我认为您现在还没有得到答案的事实似乎表明您目前正在做的可能是最好的方法,或者至少足够好。它看起来不太漂亮,但我能想到的唯一问题是有人可以在当前保存更改时单击关闭按钮并选择保存;您需要处理它并确保在发生这种情况时只调用一次 save。
  • 你可能是对的,也许没有更好的方法。至于防止用户在保存时单击保存按钮,在我的真实应用程序中我正在处理它。保持表单响应时要注意的好事情。
  • 相关:.NET Async in shutdown methods?。一个重要的细节是在以编程方式关闭表单之前await Task.Yield();,否则如果SaveAsync() 同步完成,您可能会遇到异常。

标签: c# .net-4.5 async-await formclosing


【解决方案1】:

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

这是我的工作:

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

【讨论】:

  • 这是我解决这个问题的原始方法。我希望这不是真正的解决方案,但似乎它确实是处理它的唯一方法。我认为这个问题已经存在了很长时间,所以我将此解决方案标记为答案。感谢您整理示例代码!
  • 在 Windows 窗体中,将 Form.IsEnabled 设置为 true 会阻止任何交互,因此在这里使用没有任何意义。
  • 它对我有用,但我必须在调用“this.Close()”之前取消注册关闭事件,否则再次调用关闭并再次显示确认窗口。
  • 这在 WPF 中确实有效。但正如其他人所提到的,您需要在调用 Close() 之前取消注册 Closing 事件处理程序(上例中的 Window_Closing),否则您将获得无限反馈。
  • 我做了一个编辑来解释为什么需要调用Task.Yield()。 (即使没有它在这个例子中它可能会正常工作)
【解决方案2】:

对话框在处理消息的同时仍将当前方法保留在堆栈中。

您可以在 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 form open if saving fails
                        e.Cancel = !await SaveAsync();
                }
                finally
                {
                        decoy.Invoke(new MethodInvoker(decoy.Hide));
                }
        });
        decoy.ShowDialog(this);
        t.Wait(); //TODO: handle Exceptions
}

【讨论】:

    【解决方案3】:

    您无法通过 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);
    }
    

    【讨论】:

    • @Downvoter 愿意发表评论,以便我了解我的代码有什么问题?
    • 您将e.Cancel 设置为false,这是默认值。并且您在线程有机会运行之前关闭表单——如果保存数据的进程失败,则表单已经关闭并且用户失去工作,除非您想编写代码来重新显示和重新填充Form。正是 OP 想要避免的 ;-)。至少你间接建议更喜欢覆盖方法而不是注册事件处理程序。
    • @binki 无论表单是否关闭,应用程序都无法在线程完成工作之前退出。 (看它不是一个background-thread,我故意没有设置它)。
    • 所以Save() 方法的变体总是成功的。即使磁盘已满或网络连接不可用,当然也无需向用户重新显示数据,以便将其记忆或复制到纸上或以其他方式保存。很高兴知道。
    • @binki 好的,你看到你错了,然后添加额外的 cmets :) 首先,用户无事可做(即使你脑子里有代码),当磁盘已满时,如果有是一个解决方案,您也可以在该线程中实现它。其次,可以在另一个线程中显示一些信息甚至得到一些确认。您可以在我的其他答案中了解如何创建另一个消息泵。
    【解决方案4】:

    当我尝试异步处理所有关闭事件时,我遇到了类似的问题。我相信这是因为没有什么可以阻止主线程与实际的 FormClosingEvents 一起前进。只需在等待之后添加一些内联代码即可解决问题。就我而言,无论响应如何(在等待响应时),我都会保存当前状态。您可以轻松地让任务返回当前状态,以便在用户响应后适当保存。

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

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

    【讨论】:

    • 在这种情况下,它会看到Cancel 在遇到await 并停止阻塞时设置为false。到时候表格会被拆掉,如果是主表格,整个流程都会在那个时候拆掉。
    • @Servy 我认为你是对的。 MessageBox 让我的保存功能每次都有足够的时间完成。所以我很感兴趣除了“我唯一的解决方案是取消关闭事件......,然后如果保存成功它将调用 form.Close() 函数。我希望有一种更清洁的方法处理这种情况。”
    • 我认为您可以显示一个不可关闭的模态“保存”状态对话框(您必须制作自己的表单,然后使用ShowDialog() 显示它并通过Task.ContinueWith() 将其关闭)。这样,应用程序的事件泵将继续运行,而不必返回事件,防止“(未响应)”并让您有机会在主机表单中设置 e.Cancel = true。如果我有时间,我可能会尝试测试并稍后发布替代答案……
    【解决方案5】:

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

    我实际上是在使用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;
        }
    }
    

    【讨论】:

      【解决方案6】:

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

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

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

      【讨论】:

      • 这会导致表单被标记为无响应,向用户表明出现问题并且程序已损坏,即使它没有。然后他们可能会杀死程序,如果在保存时发生,这可能会非常糟糕。
      • 我的 SaveAsync 函数也可以通过单击按钮来调用,在这种情况下,我确实希望屏幕在其工作时保持响应。我正在考虑让 SaveAsync 函数调用一个名为 Save() 的单独非异步函数的替代方法。然后我会完全按照您所说的去做,并在表单关闭时调用非异步保存函数。但我会先等待其他答案。
      • 因为如果你让它非异步,那么你必须维护你的保存方法的一个新的非异步版本。
      猜你喜欢
      • 2019-03-21
      • 1970-01-01
      • 1970-01-01
      • 2018-12-27
      • 2023-03-13
      • 1970-01-01
      • 2017-04-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多