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);
}
我需要确保Load
在Shown
执行之前完成。
这是我当前的解决方案:
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 void
和await
混淆了这个问题。从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";
}