使用 async await 乱序执行的 WinForms 事件

WinForms events executing out of order with async await

WinForms 事件 运行 乱序。我想我明白为什么,但我很好奇解决它的最佳方法。

在下面的(设计的)代码中,首先调用 Load,但是当达到 await 时它放弃控制,然后调用 ShownShown完成后,Load终于可以完成了。

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);
}

我需要确保 LoadShown 可以执行之前完成。

这是我目前的解决方案:


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";
}

这样做的明显缺点是我必须手动设置 boundaries/dependecies。确保事件按顺序执行的最佳方法是什么?

在这种情况下,覆盖 OnLoad 或订阅 Load 具有相同的效果。

但是,订阅 Load 会导致委托实例化和调用,而覆盖 OnLoad 不会,而且会给予对执行内容和执行顺序的更多控制。

试试这个:

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";
}

以正确的顺序获取事件。事件处理程序中的 async voidawait 混淆了这个问题。从 Form class 的角度来看,当它调用你的 Load 事件处理程序时,事件处理程序方法 returns 当 SomeMethod 中的 await 被命中时。当 SomeMethod 任务完成时,Load 事件处理程序中的其余代码将作为 continuation 在 UI 线程上执行 - 尽管它看起来像仍在 Load 事件处理程序中,因为那是您的代码所在的位置,而不是编译器发出的内容。当然,有点混乱。 Jon Skeet 的书 C# 深入:第四版 包含了我所见过的最好的解释之一。

此外,尽可能避免使用 async void 方法,因为抛出异常会使您的进程崩溃。为了解决这两个问题,并增加一些清晰度,我会使用 Load 事件来启动任务,然后在 Shown 中为它附加一个延续。这确保了当加载中的异步内容完成并显示时,您想要 运行 的代码。

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();
}

上面的示例将在 Visual Studio 中输出线程 ID 和经过的毫秒数到输出 window。调整延迟时间并取消注释抛出异常。请注意 DoStuffAfterShownAndAsyncLoadStuff 将始终在 UI 线程上执行。

所有解决方案都需要一些东西来阻止显示继续,直到加载完成。 TaskCompletionSource 似乎是最优雅的实现方式。

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";
}