3

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?

Dan Friedman
  • 4,941
  • 2
  • 41
  • 65
  • 1
    Have you tried to override the OnLoad method instead of subscribing the Load event? – Steeeve Oct 29 '21 at 18:52
  • I haven't. How would I solve it with `OnLoad`? – Dan Friedman Oct 29 '21 at 18:59
  • I'm pretty sure you can't. The only real option is to move that code out to creator of "SomeForm" and let them trigger/await the async part before calling `.Show`... (comment as I have not touched WinForms in quite a while and maybe there is now some async support) – Alexei Levenkov Oct 29 '21 at 19:04
  • Is this question useful? [Task sequencing and re-entracy](https://stackoverflow.com/questions/21424084/task-sequencing-and-re-entracy) – Theodor Zoulias Oct 29 '21 at 19:23
  • It is a threading race that you can't win, the ConfigureAwait() call makes it significantly worse. The code looks too fake to propose a workaround, but consider to not use an event at all. Create an async method that invokes the form constructor, does the async dance, then finishes by calling Show(). – Hans Passant Oct 29 '21 at 23:00

3 Answers3

1

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.

Steven Bone
  • 588
  • 5
  • 16
  • Steven the `_loadTask.ContinueWith` method returns a `Task`, which your code fires-and-forgets, which means that any exception thrown by this task will be swallowed. Which is not good. Also the [`async void`](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void) should generally be avoided, unless it is used for its intended purpose, which is to make async event handlers possible. In this case the OP is using it for its intended purpose, so there is no reason for warning them. Hence my downvote. – Theodor Zoulias Oct 30 '21 at 03:58
0

All solutions require something to block Shown from continuing until Load is completed. TaskCompletionSource seems to be the most elegant way of accomplishing that.

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";
}
Dan Friedman
  • 4,941
  • 2
  • 41
  • 65
-1

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 = OnLoadAsync();

    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";
}
Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
  • 1
    Paulo overriding the protected `OnLoad`, `OnShown` etc methods in application code is idiomatic, fragile and verbose IMHO. It's too easy to forget calling the `base.On...` method, and the next week spend hours debugging your app to find why the events of your form are not raised. Overriding these methods makes sense when writing library code (creating custom controls), not application code. – Theodor Zoulias Oct 30 '21 at 04:09
  • 2
    Overriding instead of Subscribing is totally separate from the actual problem. And you can do excatly the same with events. – H H Oct 30 '21 at 19:58