Home > Enterprise >  WinForms events executing out of order with async await
WinForms events executing out of order with async await

Time:10-30

WinForms events are running out of order. I think I understand why, but I'm curious on the best way to solve it.

In the below (contrived) code, Load gets called first, but it gives up control when await is reached and then Shown is called. Once Shown is completed, Load can finally finish.

private async void SomeForm_Load(object sender, EventArgs e)
{
    someLabel.Text = "Start Load";
    await SomeMethod();
    someLabel.Text = "Finish Load";
}

private void SomeForm_Shown(object sender, EventArgs e)
{
    someLabel.Text = "Shown";
}


private async Task SomeMethod()
{
    await Task.Delay(1).ConfigureAwait(false);
}

I need to ensure that Load is completed before Shown can be executed.

This is my current solution:


Task _isLoadedTask;

private async void SomeForm_Load(object sender, EventArgs e)
{
    var tcs = new TaskCompletionSource();
    _isLoadedTask = tcs.Task;

    someLabel.Text = "Start Load";
    await SomeMethod();
    someLabel.Text = "Finish Load";

    tcs.TrySetResult();
}

private async void SomeForm_Shown(object sender, EventArgs e)
{
    await _isLoadedTask;

    someLabel.Text = "Shown";
}

The obvious downside to this is that I have to manually set up boundaries/dependecies. What's the best way to ensure events are executed in order?

CodePudding user response:

In this case, overriding OnLoad or subscribing to Load has the same effect.

However, subscribing to Load incurs in delegate instantiation and invocation and overriding OnLoad won't and will give more control over what will be executed and in what order.

Try this:

private Task isLoadedTask;

protected override void OnLoad(EventArgs e)
{
    this.isLoadedTask = onl oadAsync();

    base.OnLoad(e);
}

private async Task LoadAsync()
{
    someLabel.Text = "Start Load";
    await SomeMethod();
    someLabel.Text = "Finish Load";
}

protected override async void OnShown(EventArgs e)
{
    base.OnShown(e);

    await _isLoadedTask;

    someLabel.Text = "Shown";
}

CodePudding user response:

You are getting the events in the correct order. The async void with the await in the event handlers is confusing the issue. From the perspective of the Form class, when it calls your Load event handler, the event handler method returns when the await in SomeMethod is hit. When the SomeMethod task completes, the rest of the code in the Load event handler is executed as a continuation back on the UI thread - though it looks like it is still in the Load event handler because that is where YOUR code is located, that's not what the compiler emits. A bit confusing, for sure. Jon Skeet's book C# in Depth: Fourth Edition contains perhaps one of the best explanations of this I have seen.

Also, avoid the use of async void methods if at all possible, as throwing an exception can crash your process. To solve both of these problems, and add some clarity, I would use the Load event to start the task, then attach a continuation to it in Shown. This ensures that the code you want to run when your async stuff in Load completes and Shown happens.

private Task _loadTask = Task.CompletedTask;
private Stopwatch _sw = Stopwatch.StartNew();

private void Form1_Load(object sender, EventArgs e)
{
    Trace.TraceInformation("[{0}] {1} Form1_Load - Begin", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
    _loadTask = DoAsyncLoadStuff();
    Trace.TraceInformation("[{0}] {1} Form1_Load - End", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
}

private void Form1_Shown(object sender, EventArgs e)
{
    Trace.TraceInformation("[{0}] {1} Form1_Shown - Begin", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
    _loadTask.ContinueWith(DoStuffAfterShownAndAsyncLoadStuff, null, TaskScheduler.FromCurrentSynchronizationContext());
    Trace.TraceInformation("[{0}] {1} Form1_Shown - End", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
}

private void DoStuffAfterShownAndAsyncLoadStuff(Task asyncLoadTask, object context)
{
    Trace.TraceInformation("[{0}] {1} DoStuffAfterShownAndAsyncLoadStuff. Task was {2}", Thread.CurrentThread.ManagedThreadId, 
        _sw.ElapsedMilliseconds, asyncLoadTask.Status);
}

private async Task DoAsyncLoadStuff()
{
    await Task.Delay(5000).ConfigureAwait(false);
    Trace.TraceInformation("[{0}] {1} DoAsyncLoadStuff - Returning", Thread.CurrentThread.ManagedThreadId, _sw.ElapsedMilliseconds);
    //throw new NotImplementedException();
}

The sample above will output to the Output window in Visual Studio the thread id and elapsed milliseconds. Tweak the Delay time and uncomment throwing the exception. Note that DoStuffAfterShownAndAsyncLoadStuff will always execute on the UI thread.

  • Related