Search code examples
c#winformsasync-awaiteventhandler

Button.PerformClick() doesn't wait an async event hanlder


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!"

Solution

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