我们有一个使用异步初始化过程的 winforms 应用程序。简化后,您可以说应用程序将运行以下步骤:
- 初始化 - 异步运行
- 显示主窗体
- Application.Run()
当前存在的和有效的代码如下所示:
[STAThread]
private static void Main()
{
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
var task = StartUp();
HandleException(task);
Application.Run();
}
private static async Task StartUp()
{
await InitAsync();
var frm = new Form();
frm.Closed += (_, __) => Application.ExitThread();
frm.Show();
}
private static async Task InitAsync()
{
// the real content doesn't matter
await Task.Delay(1000);
}
private static async void HandleException(Task task)
{
try
{
await Task.Yield();
await task;
}
catch (Exception e)
{
Console.WriteLine(e);
Application.ExitThread();
}
}
Mark Sowul在这里非常详细地描述了它是如何工作的背景。
从 C# 7.1 开始,我们可以在 main 方法中使用异步任务。我们以直接的方式进行了尝试:
[STAThread]
private static async Task Main()
{
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
try
{
await StartUp();
Application.Run();
}
catch (Exception e)
{
Console.WriteLine(e);
Application.ExitThread();
}
}
private static async Task StartUp()
{
await InitAsync();
var frm = new Form();
frm.Closed += (_, __) => Application.ExitThread();
frm.Show();
}
private static async Task InitAsync()
{
// the real content doesn't matter
await Task.Delay(1000);
}
但这行不通。原因很清楚。第一个await
之后的所有代码都将转发到消息循环。但是消息循环还没有启动,因为启动它的代码(Application.Run()
)位于第一个await
之后。
删除同步上下文将解决问题,但会导致在其他线程中await
后运行代码。
在第一个await
之前重新排序代码以调用Application.Run()
将不起作用,因为它是阻塞调用。
我们尝试使用具有async Task Main()
的新功能,该功能使我们能够删除难以理解的HandleException
解决方案。但我们不知道怎么做。
你有什么建议吗?
你不需要async Main
.这是可能的:
[STAThread]
static void Main()
{
void threadExceptionHandler(object s, System.Threading.ThreadExceptionEventArgs e)
{
Console.WriteLine(e);
Application.ExitThread();
}
async void startupHandler(object s, EventArgs e)
{
// WindowsFormsSynchronizationContext is already set here
Application.Idle -= startupHandler;
try
{
await StartUp();
}
catch (Exception)
{
// handle if desired, otherwise threadExceptionHandler will handle it
throw;
}
};
Application.ThreadException += threadExceptionHandler;
Application.Idle += startupHandler;
try
{
Application.Run();
}
catch (Exception e)
{
Console.WriteLine(e);
}
finally
{
Application.Idle -= startupHandler;
Application.ThreadException -= threadExceptionHandler;
}
}
请注意,如果您不注册事件处理程序threadExceptionHandler
并且StartUp
抛出(或消息循环抛出的任何其他内容),它仍然可以工作。异常将被捕获在包装Application.Run
的try/catch
内。它只是一个TargetInvocationException
例外,原始异常可通过其InnerException
属性获得。
更新以解决评论:
但对我来说,将事件处理程序注册到 空闲事件,因此启动整个应用程序。完全清楚如何 这行得通,但仍然很奇怪。在这种情况下,我更喜欢 我已经拥有的处理异常解决方案。
我想这是一个品味问题。我不知道为什么WinForms API设计师没有提供类似WPFApplication.Startup
的东西。但是,由于WinForm的Application
类上缺少专用事件,因此在第一个Idle
事件上推迟特定的初始化代码是IMO的一个优雅的解决方案,并且在SO上广泛使用。
我特别不喜欢在Application.Run
开始之前显式手动配置WindowsFormsSynchronizationContext
,但是如果您想要替代解决方案,请在此处:
[STAThread]
static void Main()
{
async void startupHandler(object s)
{
try
{
await StartUp();
}
catch (Exception ex)
{
// handle here if desired,
// otherwise it be asynchronously propogated to
// the try/catch wrapping Application.Run
throw;
}
};
// don't dispatch exceptions to Application.ThreadException
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
using (var ctx = new WindowsFormsSynchronizationContext())
{
System.Threading.SynchronizationContext.SetSynchronizationContext(ctx);
try
{
ctx.Post(startupHandler, null);
Application.Run();
}
catch (Exception e)
{
Console.WriteLine(e);
}
finally
{
System.Threading.SynchronizationContext.SetSynchronizationContext(null);
}
}
}
IMO,这两种方法都比您问题中使用的方法更干净。附带说明一下,您应该使用ApplicationContext
来处理窗体闭包。您可以将ApplicationContext
实例传递给Application.Run
。
我唯一的一点 缺少的是已设置同步上下文的提示。 是的 - 但为什么呢?
它确实被设置为Application.Run
的一部分,如果当前线程上还没有出现的话。如果要了解更多详细信息,可以在 .NET 引用源中进行调查。