WinForms事件执行异步等待无序



WinForms事件运行无序。我想我知道为什么,但我很好奇解决这个问题的最佳方法。

在下面的(人为的)代码中,首先调用Load,但是当到达await时,它放弃控制,然后调用Shown。一旦Shown完成,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";
}

明显的缺点是我必须手动设置边界/依赖项。确保事件按顺序执行的最佳方法是什么?

正在以正确的顺序获取事件。事件处理程序中的async voidawait混淆了这个问题。从Form类的角度来看,当它调用Load事件处理程序时,当命中SomeMethod中的await时,事件处理程序方法返回。当SomeMethod任务完成时,Load事件处理程序中的其余代码作为延续在UI线程上执行-尽管它看起来仍然在Load事件处理程序中,因为那是你的代码所在的位置,这不是编译器发出的。当然,这有点令人困惑。Jon Skeet的书c#深度:第四版可能是我所见过的最好的解释之一。

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

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的output窗口输出线程id和经过的毫秒数。调整延迟时间并取消引发异常的注释。注意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";
}

在这种情况下,覆盖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";
}

相关内容

  • 没有找到相关文章

最新更新