6

We're having a winforms application that uses an async initialization process. Simplified you can say that the application will run the following steps:

  • Init - this runs async
  • Show MainForm
  • Application.Run()

The currently existing and working code looks like this:

[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();
    }
}

The background how this is working is described very detailed by Mark Sowul here.

Since C# 7.1 we're able to use async Task in main method. We tried it in a straight forward way:

[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);
}

But that doesn't work. The reason is clear. All the code after the first await will be forwarded to the message loop. But the message loop hasn't startet yet because the code that starts it (Application.Run()) is located after the first await.

Removing the synchronization context will fix the problem but causes to run the code after await in a different thread.

Reordering the code to call Application.Run() before the first await will not work because it is a blocking call.

We try to use the new feature of having an async Task Main() that allows us to remove the HandleException-solution that is hard to understand. But we don't know how.

Do you have any suggestions?

Sebastian Schumann
  • 3,204
  • 19
  • 37

1 Answers1

5

You don't need async Main. Here is how it can possibly be done:

[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;
    }
}

Note, if you don't register threadExceptionHandler event handler and StartUp throws (or anything else on the message loop throws, for the matter), it will still work. The exception will be caught inside the try/catch which wraps Application.Run. It will just be a TargetInvocationException exception with the original exception available via its InnerException property.

Updated to address the comments:

But for me it looks very strange to register an EventHandler to the idle event so startup the whole application. It's totally clear how that works but still strange. In that case I prefer the HandleException solution that I already have.

I guess it's a matter of taste. I don't know why WinForms API designers didn't provide something like WPF's Application.Startup. However, in the lack of a dedicated event for this on WinForm's Application class, deferring specific initialization code upon the first Idle event is IMO an elegant solution, and it's widely used here on SO.

I particularly don't like the explicit manual provisioning of WindowsFormsSynchronizationContext before Application.Run has started, but if you want an alternative solution, here you go:

[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, either approach is more clean than the one used in your question. On a side note, you should be using ApplicationContext to handle the form closure. You can pass an instance of ApplicationContext to Application.Run.

The only point that I'm missing is your hint that the synchronization context is already set. Yes it is - but why?

It is indeed set as a part of Application.Run, if not already present on the current thread. If you like to learn more details, you could investigate it in .NET Reference Source.

noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Thx for your answer. I tested it and - of course - it works. But for me it looks very strange to register an EventHandler to the idle event so startup the whole application. It's totally clear how that works but still strange. In that case I prefer the `HandleException` solution that I already have. The only point that I'm missing is your hint that the synchronization context is already set. Yes it is - but why? I know that it is initialized on creating the first control. Maybe `Application.Run()` will also initialize it. – Sebastian Schumann Apr 19 '18 at 05:35
  • 1
    As an aside, we had a similar problem but we didn't want to call Application.Run() at all (for a mixed CLI/GUI application with some weird constraints...). We resolved it thanks to your answer and this one https://stackoverflow.com/a/57596559/755986 by wrapping our async code in AsyncContext.Run(...) (AsyncContext by Stephen Cleary) – Melvyn Nov 19 '19 at 16:37