3

I have a problem in a Windows Forms application where I use PerformClick to call an async event handler. It seems that the event handler doesn't await but just returns immediately. I have created this simple application to show the problem (it's just a form with 3 buttons, easy to test yourself):

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

The interesting problem here is, what do you think message shows, when I click Button2? Surprisingly, it shows "Started", not "Finished", as you would expect. In other Words, it doesn't await MyMethod(), it just starts the Task, then continues.

Edit:

In this simple code I can make it Work by calling await Method() directly from Button2 event handler, like this:

private async void button2_Click(object sender, EventArgs e)
{
    await MyMethodAsync();
    MessageBox.Show(message);
}

Now, it waits 2 seconds and displays 'Finished'.

What is going on here? Why doesn't it work when using PerformClick?

Conclusion:

Ok, now I get it, the conclusion is:

  1. Never call PerformClick if the eventhandler is async. It will not await!

  2. Never call an async eventhandler directly. It will not await!

What's left is the lack of documentation on this:

  1. Button.PerformClick should have a Warning on the doc page:

Button.PerformClick "Calling PerformClick will not await async eventhandlers."

  1. Calling an async void method (or eventhandler) should give a compiler Warning: "You're calling an async void method, it will not be awaited!"
Poul Bak
  • 10,450
  • 5
  • 32
  • 57
  • 3
  • Are you _sure_ it still says "Started" after 2 seconds? if you're keeping on clicking your `button2`, that's changing the message to "Started" for 2 seconds before it's changed to "Finished" again. – Saeb Amini Sep 03 '18 at 22:27
  • 1
    That's the point, it's not awating. – Poul Bak Sep 03 '18 at 22:40
  • 2
    No, it's _awaiting_, it's just not _blocking_ the thread/UI so you can keep clicking the buttons and make it await as many times as you want. – Saeb Amini Sep 03 '18 at 22:49
  • 1
    It is working as intended. When calling `await MyMethodAsync();` the system checks if `MyMethodAsync` has returned. It hasn't due to `Task.Delay(2000);` and so it resumes back to `button2_Click` and prints *"Started"*. Of cource `message = "Started";` has already been executed. – γηράσκω δ' αεί πολλά διδασκόμε Sep 03 '18 at 23:00
  • You're wrong, the point of 'awaiting' a method, is that it shouldn't move on, untill the awaited method returns. – Poul Bak Sep 03 '18 at 23:12
  • You can send it to microsoft as a bug to fix it. – γηράσκω δ' αεί πολλά διδασκόμε Sep 03 '18 at 23:25
  • @PoulBak And `button1_Click` doesn't move on until `MyMethodAsync` returns. But `button2_Click` doesn't (and cannot) await `button1_Click`, so does move on. –  Sep 04 '18 at 07:53
  • 2
    Nothing unusual is happening here. The proper mental model is that the code that is *after* an await expression executes later, as though it were activated by a timer. There is no mechanism to block a method on the UI thread, other than ShowDialog() and the dreaded Application.DoEvents(). Dreaded because of the high odds for causing nasty re-entrancy bugs, async/await does not solve that fundamental problem. – Hans Passant Sep 04 '18 at 08:35
  • 1
    @PoulBak what I would expect is that the code would show `Started` and after two seconds, it would show `Finished`. The code you posted though doesn't allow you to see that transition. It shows the *field's* contents when you click on a button. If you click fast enough, you'd see `Started`. If you want eg for 5 seconds you'll see `Finished`. Instead of storing the value in a field try changing the form's title. There's no bug or anything unexpected here – Panagiotis Kanavos Sep 04 '18 at 08:58

1 Answers1

6

You seem to have some misconceptions about how async/await and/or PerformClick() work. To illustrate the problem, consider the following code:
Note: the compiler will give us a warning but let's ignore that for the sake of testing.

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

Now, what do you expect the MessageBox to show? You'd probably say "started".1 Why? Because we didn't await the MyMethodAsync() method; the code inside that method runs asynchronously but we didn't wait for it to complete, we just continued to the next line where the value of message isn't yet changed.

If you understand that behavior so far, the rest should be easy. So, let's change the above code a little bit:

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

Now, all I did was that I moved the code that was inside the async method MyMethodAsync() into the async event handler button1_Click, and then I called that event handler using button1_Click(null, null). Is there a difference between this and the first code? No, it's essentially the same thing; in both cases, I called an async method without awaiting it.2

If you agree with me so far, you probably already understand why your code doesn't work as expected. The code above (in the second case) is nearly identical to yours. The difference is that I used button1_Click(null, null) instead of button1.PerfomClick(), which essentially does the same thing.3

The solution:

If you want to wait for the code in button1_Click to be finished, you need to move everything inside button1_Click (as long as it's asynchronous code) into an async method and then await it in both button1_Click and button2_Click. This is exactly what you did in your "Edit" section but be aware that button2_Click will need to have an async signature as well.


1 If you thought the answer was something else, then you might want to check this article which explains the warning.

2 The only difference is that in the first case, we could solve the problem by awaiting the method, however, in the second case, we can't do that because the "method" is not awaitable because the return type is void even though it has an async signature.

3Actually, there are some differences between the two (e.g., the validation logic in PerformClick()), but those differences don't affect the end result in our current situation.