具有异步初始化的WinForms应用程序中的异步任务Main()



我们有一个使用异步初始化过程的 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.Runtry/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 引用源中进行调查。

最新更新