我们有一个使用异步初始化过程的winforms应用程序。简化后您可以说应用程序将运行以下步骤:
现有的和正在运行的代码如下所示:
[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 here详细描述了这种工作的背景。
从C#7.1开始,我们可以在main方法中使用async Task。我们直接尝试了它:
[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
之后的其他线程中运行代码。
重新排序代码以便在第一个Application.Run()
之前调用await
将无效,因为它是阻止调用。
我们尝试使用async Task Main()
的新功能,允许我们删除难以理解的HandleException
解决方案。但我们不知道如何。
你有什么建议吗?
答案 0 :(得分:1)
您不需要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
抛出(或者消息循环中的任何其他内容抛出,那么)它仍然可以工作。异常将被包含在try/catch
的{{1}}内部捕获。它只是一个Application.Run
异常,原始异常通过其TargetInvocationException
属性提供。
更新了以解决评论:
但是对我来说,将一个EventHandler注册到 idle事件因此启动整个应用程序。如何完全清楚 虽然有效,但仍然很奇怪。在那种情况下,我更喜欢 我已经拥有的HandleException解决方案。
我想这是一个品味问题。我不知道为什么WinForms API设计者没有提供像WPF Application.Startup
这样的东西。但是,由于在WinForm的InnerException
类中没有专门的事件,在第一个Application
事件上推迟特定的初始化代码是IMO的优雅解决方案,并且它在SO上被广泛使用。
我特别不喜欢在 Idle
开始之前明确手动配置WindowsFormsSynchronizationContext
,但如果您想要一个替代解决方案,请转到:
Application.Run
IMO,这两种方法都比你问题中使用的方法更干净。另外,您应该使用ApplicationContext
来处理表单关闭。您可以将[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);
}
}
}
的实例传递给ApplicationContext
。
我唯一的观点 缺少的是您已经设置了同步上下文的提示。 是的 - 但为什么呢?
它确实设置为Application.Run
的一部分,如果当前线程上尚未存在的话。如果您想了解更多详情,可以在.NET Reference Source进行调查。