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.