Button.PerformClick()不等待异步事件处理程序。

3

在我的Windows Forms应用程序中,我使用PerformClick调用一个async事件处理程序时遇到了问题。看起来事件处理程序没有await,而是立即返回。我创建了这个简单的应用程序来展示这个问题(它只是一个带有3个按钮的表单,容易自行测试):

string message = "Not Started";

private async void button1_Click(object sender, EventArgs e)
{
    await MyMethodAsync();
}

private void button2_Click(object sender, EventArgs e)
{
    button1.PerformClick();
    MessageBox.Show(message);
}

private void button3_Click(object sender, EventArgs e)
{
    MessageBox.Show(message);
}

private async Task MyMethodAsync()
{
    message = "Started";
    await Task.Delay(2000);
    message = "Finished";
}

这里有个有趣的问题,当我点击Button2时,你认为message会显示什么?
令人惊讶的是,它显示"Started"而不是"Finished",这与你预期的不同。换句话说,它并没有等待await MyMethod(),而只是启动了任务,然后继续执行。 编辑: 在这个简单的代码中,我可以通过直接从Button2事件处理程序调用await Method()来使它起作用,像这样:
private async void button2_Click(object sender, EventArgs e)
{
    await MyMethodAsync();
    MessageBox.Show(message);
}

现在,它等待2秒并显示“完成”。

这是怎么回事?为什么使用PerformClick时它不起作用?

结论:

好的,现在我明白了,结论如下:

  1. 如果事件处理程序是async,则永远不要调用PerformClick。它不会await

  2. 永远不要直接调用async事件处理程序。它不会await

剩下的是缺乏关于此的文档:

  1. Button.PerformClick应该在文档页面上有一个警告

Button.PerformClick "调用PerformClick将不会等待异步事件处理程序。"

  1. 调用async void方法(或事件处理程序)应该给出编译器警告:“您正在调用一个async void方法,它将不会被等待!

3
召唤驱魔师! - γηράσκω δ' αεί πολλά διδασκόμε
1
这就是关键,它不是在等待。 - Poul Bak
2
不,它是“等待”的状态,只是不会“阻塞”线程/用户界面,所以你可以继续点击按钮,让它等待任意次数。 - Saeb Amini
1
它按预期工作。当调用await MyMethodAsync();时,系统会检查MyMethodAsync是否已返回。由于Task.Delay(2000);的存在,它尚未返回,因此继续回到button2_Click并打印“Started”。当然,message = "Started";已经执行过了。 - γηράσκω δ' αεί πολλά διδασκόμε
2
这里没有发生任何异常情况。正确的思维模型是,在 await 表达式之后的代码会延后执行,就好像是由一个定时器激活一样。在 UI 线程上没有阻塞方法的机制,除了 ShowDialog() 和令人恐惧的 Application.DoEvents()。由于可能引发讨厌的重入 bug,async/await 并不能解决这个根本性问题。 - Hans Passant
显示剩余5条评论
1个回答

6

您似乎对async/await和/或PerformClick()的工作原理存在一些误解。为了说明问题,请考虑以下代码:
注意:编译器会给我们一个警告,但出于测试的目的,让我们忽略它。

private async Task MyMethodAsync()
{
    await Task.Delay(2000);
    message = "Finished";      // The execution of this line will be delayed by 2 seconds.
}

private void button2_Click(object sender, EventArgs e)
{
    message = "Started";
    MyMethodAsync();           // Since this method is not awaited,
    MessageBox.Show(message);  // the execution of this line will NOT be delayed.
}

现在,你期望MessageBox显示什么?你可能会说“开始”。为什么?因为我们没有等待MyMethodAsync()方法;该方法内部的代码是异步运行的,但我们没有等待它完成,而是直接继续到下一行,此时message的值尚未改变。
如果你理解了上述行为,那么剩下的应该很容易。所以,让我们稍微改变一下上面的代码:
private async void button1_Click(object sender, EventArgs e)
{
    await Task.Delay(2000);
    message = "Finished";      // The execution of this line will be delayed by 2 seconds.
}

private void button2_Click(object sender, EventArgs e)
{
    message = "Started";
    button1_Click(null, null); // Since this "method" is not awaited,
    MessageBox.Show(message);  // the execution of this line will NOT be delayed.
}

现在,我所做的就是将原本在async方法MyMethodAsync()中的代码移动到async事件处理程序button1_Click中,并使用button1_Click(null, null)调用该事件处理程序。这与第一段代码有什么区别吗?没有,它们本质上是相同的;在两种情况下,我都调用了一个异步方法,但没有等待它完成.2

如果你同意我的看法,那么你可能已经理解为什么你的代码不能按预期工作了。上面的代码(第二种情况)与你的代码几乎完全相同。不同之处在于我使用了button1_Click(null, null)而不是button1.PerfomClick(),它们本质上是相同的。3

解决方案:

如果你想要等待button1_Click中的代码完成,你需要将button1_Click中的所有内容(只要是异步代码)移动到一个async方法中,然后在button1_Clickbutton2_Clickawait它。这正是你在"Edit" section中所做的,但请注意,button2_Click也需要有一个async签名。


1 如果你认为答案是其他内容,请查看这篇文章,它解释了这个警告。

2 唯一的区别在于,在第一种情况下,我们可以通过等待该方法来解决问题,然而,在第二种情况下,我们不能这样做,因为“方法”不可等待因为返回类型为void即使它具有async签名

3实际上,这两者之间存在一些差异(例如,PerformClick()中的验证逻辑),但这些差异并不影响我们当前情况下的最终结果。


我在编辑的 'Button2' 事件处理程序中添加了 'async',那是个打字错误。 - Poul Bak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接