async Task Main() в приложении WinForms с асинхронной инициализацией
У нас есть приложение winforms, которое использует процесс асинхронной инициализации. Упрощенно можно сказать, что приложение будет запускать следующие шаги:
- Init - работает асинхронно
- Показать MainForm
- 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();
}
}
Фон, как это работает, очень подробно описан Марком Совулом здесь.
Начиная с 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
в другой теме.
Изменение порядка кода для вызова Application.Run()
до первого await
не будет работать, потому что это блокирующий вызов.
Мы стараемся использовать новую функцию async Task Main()
что позволяет нам удалить HandleException
решение, которое трудно понять. Но мы не знаем как.
У вас есть какие-нибудь предложения?
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
throws (или что-либо еще в throws цикла сообщений, между прочим), это все еще будет работать. Исключение будет поймано внутри try/catch
который оборачивает Application.Run
, Это будет просто TargetInvocationException
исключение с исходным исключением, доступным через его InnerException
имущество.
Обновлено с учетом комментариев:
Но для меня это выглядит очень странно - регистрировать EventHandler на событие бездействия, чтобы запустить все приложение. Совершенно ясно, как это работает, но все же странно. В этом случае я предпочитаю решение HandleException, которое у меня уже есть.
Я думаю, это вопрос вкуса. Я не знаю, почему разработчики WinForms API не предоставили что-то вроде WPF Application.Startup
, Тем не менее, в отсутствие специального события для этого на WinForm Application
класс, откладывая конкретный код инициализации на первый Idle
IMO - это элегантное решение, и оно широко используется здесь, на SO.
Мне особенно не нравится явное ручное обеспечение WindowsFormsSynchronizationContext
до Application.Run
запущен, но если вы хотите альтернативное решение, то вы идете:
[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
обрабатывать закрытие формы. Вы можете передать экземпляр ApplicationContext
в Application.Run
,
Единственный момент, который я упускаю, это ваш намек на то, что контекст синхронизации уже установлен. Да, но почему?
Это действительно установлено как часть Application.Run
, если еще не присутствует в текущем потоке. Если вы хотите узнать больше подробностей, вы можете исследовать это в .NET Reference Source.