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.

Другие вопросы по тегам