0

As far as I can see when an "async void" method (such as an event handler) is called, the caller can never know when it has completed (as it can't wait for the Task to complete). So effectively its a fire and forget call.

This is demonstrated with this code (i've put a Button and TabControl onto a form and hooked up the 2 events). When the button is clicked it changes the tab, this causes the SelectedIndexChanged event to be raised which is async.

    private void button1_Click(object sender, EventArgs e)
    {
        Debug.WriteLine("Started button1_Click");
        tabControl1.SelectedTab = tabControl1.SelectedIndex == 0 ? tabPage2 : tabPage1;
        Debug.WriteLine("Ended button1_Click");
    }

    private async void tabControl1_SelectedIndexChanged(object sender, EventArgs e)
    {
        Debug.WriteLine("Started tabControl1_SelectedIndexChanged");
        await Task.Delay(1000);
        Debug.WriteLine("Ended tabControl1_SelectedIndexChanged");
    }

The resulting output is

Started button1_Click
Started tabControl1_SelectedIndexChanged
Ended button1_Click
Ended tabControl1_SelectedIndexChanged

As you can see the SelectedIndexChanged event handler was fired, but the caller did not wait not wait for it to complete (it couldn't as it had no Task to await).

My Proposed solution

Instead of using async the event handler waits on any Async methods it uses, then everything seems to work... It waits by polling the Task.IsCompleted property while calling DoEvents in order to keep the async task alive and processing (in this case Task.Delay).

    private void button1_Click(object sender, EventArgs e)
    {
        Debug.WriteLine("Started button1_Click");
        tabControl1.SelectedTab = tabControl1.SelectedIndex == 0 ? tabPage2 : tabPage1;
        Debug.WriteLine("Ended button1_Click");
    }

    private void tabControl1_SelectedIndexChanged(object sender, EventArgs e)
    {
        Debug.WriteLine("Started tabControl1_SelectedIndexChanged");
        Await(Task.Delay(1000));
        Debug.WriteLine("Ended tabControl1_SelectedIndexChanged");
    }

    public static void Await(Task task)
    {
        while (task.IsCompleted == false)
        {
            System.Windows.Forms.Application.DoEvents();
        }

        if (task.IsFaulted && task.Exception != null)
            throw task.Exception;
        else
            return;
    }

This now gives the expected result

Started button1_Click
Started tabControl1_SelectedIndexChanged
Ended tabControl1_SelectedIndexChanged
Ended button1_Click

Can anyone see any issues with taking this approach???

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Sprotty
  • 5,676
  • 3
  • 33
  • 52
  • 4
    `Application.DoEvents();` is considered a bug by our team and nobody is allowed to check in such code. It allows re-entrant calls which can cause a number of difficult-to-diagnose bugs. And your solution also blocks, and is likely to introduce deadlock in some case. So no, don't do that. – Matthew Watson Sep 22 '22 at 08:26
  • Also by forcing the code to wait, you've blocked the UI. The whole point about `async/await` is that you DON'T block the UI! – Matthew Watson Sep 22 '22 at 08:28
  • The DoEvents keeps the UI alive and processing, the code doesn't really block as its still processing the async Task list, in the same way as if you had used async await – Sprotty Sep 22 '22 at 08:30
  • 3
    [The `DoEvents()` is very likely to introduce bugs.](https://stackoverflow.com/a/5183623/106159) The big question here is WHY do you want to do it like this? What issue are you actually trying to solve? – Matthew Watson Sep 22 '22 at 08:34
  • The order of processing is not preserved. For example I programmatically change the Tab, and expect the event handler to load the tab data, I then want to do something to the loaded data, Currently I set the tab, the event fires - it starts loading tab data and returns immediately, the event handler call returns and I then try to interact with a partially loaded tab. This kind of issue happens anywhere you inadvertently trigger an async event handler – Sprotty Sep 22 '22 at 08:51
  • [TaskCompletionSource](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1) (make `button1_Click` async and await for the completion) – Jimi Sep 22 '22 at 09:13
  • I mean, in case you really need to rely on cascading events to perform these actions (you probably don't) – Jimi Sep 22 '22 at 09:20
  • 1
    If you really must block your UI while you wait for data to populate it, you should look at [this thread](https://stackoverflow.com/questions/9343594/how-to-call-asynchronous-method-from-synchronous-method-in-c) to see how to call an async method synchronously. – Matthew Watson Sep 22 '22 at 09:31
  • The Nito.AsyncEx.AsyncContext.Run blocks until the async task completes - which is not ideal - but prevents the other issues associated with DoEvents. Looks like calling DoEvents is only really safe if you disable the UI - which is better than blocking, but still means the user can't interact with the app. – Sprotty Sep 22 '22 at 10:11
  • From what I can see whenever an await is being actually waited on, the windows message pump will be processed (so user interaction will raise events ect) , which is exactly what calling DoEvents does. So why aren't all the issues linked with DoEvents also linked with await ? – Sprotty Sep 22 '22 at 10:15

1 Answers1

2

It waits by polling the Task.IsCompleted property while calling DoEvents

This is one form of blocking on asynchronous code which I call the nested message loop hack.

Can anyone see any issues with taking this approach?

Yes. This kind of solution suffers from what I call "unexpected reentrancy".

Unexpected reentrancy historically has been the cause of many, many bugs, because some code inevitably ends up being called from within other code (on the same stack). In your example, tabControl1_SelectedIndexChanged (or more specifically, Await) may execute any other UI code directly. Including another invocation of tabControl1_SelectedIndexChanged. This causes problems as soon as your code becomes non-trivial, and even worse, it's timing-dependent so you get "heisenbugs".

There's a long-standing phrase: "DoEvents is evil". That bears careful consideration.

why aren't all the issues linked with DoEvents also linked with await?

That's a good question; a lot of developers were initially skeptical of await because they had been so badly burned by DoEvents. The reason await doesn't fall into this trap is because there's only one message loop. await returns to that singular message loop. There's no nested message loop, and thus no unexpected re-entrancy.

I would never recommend DoEvents as a solution. There are always better solutions.

The order of processing is not preserved. For example I programmatically change the Tab, and expect the event handler to load the tab data, I then want to do something to the loaded data, Currently I set the tab, the event fires - it starts loading tab data and returns immediately, the event handler call returns and I then try to interact with a partially loaded tab. This kind of issue happens anywhere you inadvertently trigger an async event handler

So, here's the actual problem: C# events (at least the void-returning events which are the vast majority of events) are insufficiently powerful to act as anything other than notifications. In design terms, C# events allow you to implement the Observer pattern, but are the wrong choice to implement the Strategy pattern.

To apply to your example, the tabControl1_SelectedIndexChanged is just a notification (Observer pattern) to inform your code that the tab index changed. It's not a hook to provide data loading (Strategy pattern), and attempting to use it that way is what is causing the actual problem here.

The solution then is not to depend on event handlers to drive your logic. Take a look at some of the Model-View-ViewModel (MVVM) concepts out there for some inspiration. Using a ViewModel kind of approach would probably help in this situation. That way your code would not update the tab control at all; instead, it would update the ViewModel, and your VM can do the work asynchronously (with no async void at all) and then your code can update the UI when the work is complete.

A simple approach (with no data binding and no explicit VM) may look something like:

private async void button1_Click(object sender, EventArgs e)
{
  Debug.WriteLine("Started button1_Click");
  var newTab = tabControl1.SelectedIndex == 0 ? tabPage2 : tabPage1;
  ShowLoadingState(newTab);
  tabControl1.SelectedTab = newTab;
  await LoadDataForTab(newTab); 
  Debug.WriteLine("Ended button1_Click");
}

private async Task LoadDataForTab(TabPage tab)
{
  await Task.Delay(1000);
  // load data into tab
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks, I agree with most of your post, but you still get re-entrancy with the async code. In your example just click the button a few times, you'll see 'start, start end end' which is exactly the issue you see with DoEvents. In both scenarios your picking up and processing the message queue while inside a function. So I struggle to see why async event handlers aren't tared with the same brush as DoEvents. That aside changing the pattern used as you suggest is probably the way to go. – Sprotty Oct 07 '22 at 05:19