4

I'm very confused because I feel like I'm using async/await in a completely typical way here, no different than I've been using it for years, and yet I'm getting the dreaded Control accessed from a thread other than the thread it was created on message all over the place in my application (an old WinForms app I'm trying to breathe new life into).

The project is in .NET Framework 4.8 and I have just converted an old form from using a BackgroundWorker to async/await, and I'm losing the SynchronisationContext after await statements in various places, one example of which is shown below. I replaced the complex chain of async code that actually runs with Task.Delay and the problem still arises. Adding ConfigureAwait(true) or ConfigureAwait(false) to Task.Delay doesn't seem to make any difference, not that I feel I should even need it in this scenario.

Grateful if somebody can point out where I'm going wrong.

private async void ListViewSelectedIndexChanged(object sender, EventArgs e) {
    Debug.WriteLine(SynchronizationContext.Current != null); // true
    Debug.WriteLine(Thread.CurrentThread.ManagedThreadId); // 1
    await Task.Delay(100);
    Debug.WriteLine(SynchronizationContext.Current != null); // false - why?
    Debug.WriteLine(Thread.CurrentThread.ManagedThreadId); // 8
    // Touching controls here throws error
}

EDIT 1: Okay, I chased the creation of the form upwards, and I've hit the root of the problem though I still don't understand why it's a problem. The problem can be reduced to this:

// MainForm designer:
this.Load += new System.EventHandler(this.MainFormLoad);

// MainForm code-behind:
private async void MainFormLoad(object sender, EventArgs e) {
    var form = new BackupManagerForm();
    form.Show(); // BackupManagerForm will exhibit the problem
    // ... some async stuff including adding dynamic context menus to MainForm
}

The problem can be removed by removing async from the Load handler in MainForm. Now that I know that I can code around it, but I'm curious as to why that damages the SynchronizationContext so far downstream (despite the fact it seems perfectly able to add the context menus I mentioned above to MainForm for example).

EDIT 2: I just tried to reconstruct the problem in a blank WinForms problem and can't reproduce it. Making the Load handler for MainForm non-async definitely does solves the problem, but actually that's annoying because there's a load of async work I really need to do at that point.

As requested, this is how MainForm is instantiated (there's a lot of code removed for brevity here but hopefully nothing significant):

public static class App {
    [STAThread]
    public static void Main(string[] args) {
    var builder = new ContainerBuilder();
    IocConfig.RegisterDependencies(builder);
    var container = builder.Build();
    ServiceLocator.SetLocatorProvider(() => new AutofacServiceLocator(container));
    var bootstrapper = ServiceLocator.Current.GetInstance<IAppBootstrapper>();
    bootstrapper.Startup();
}
public class AppBootstrapper : IAppBootstrapper {
    private readonly ISettings _settings;
    private readonly MainForm _mainForm;
    public AppBootstrapper(MainForm mainForm, ISettings settings) {
        _mainForm = mainForm;
        _settings = settings;
    }
    public void Startup() {
        // Other stuff removed but perhaps these might be relevant?
        Application.DoEvents();
        _settings.SaveAsync().Wait();
        Application.Run(_mainForm);
    }
}

EDIT 3: That Application.DoEvents() turns out to be significant. So does instantiating the form with an IoC container. If I either remove the Application.DoEvents() statement or use Application.Run(new MainForm()) the problem disappears. The Application.DoEvents() exists because of a splash screen that is shown before MainForm with splashForm.Show() followed by Application.DoEvents() - when MainForm eventually loads, it hides the splash screen on load.

I've found a good solution to the problem now. This comprehensively solves the problem without any annoying side effects as far as I can tell:

public void Startup() {
    var context = SynchronizationContext.Current;
    _splashForm.Show();
    Application.DoEvents();
    ...
    SynchronizationContext.SetSynchronizationContext(context);
    Application.Run(_mainForm);
}

I still don't have a clear understanding of the problem, but it's obviously to do with the fact that the main form is not the first form to create a message loop. What's interesting though is how far down inside the application that manifests as a significant problem. Anyway, I'd be happy to accept an answer from anyone who can explain the phenomenon.

wwarby
  • 1,873
  • 1
  • 21
  • 37
  • 3
    I checked this in winforms application with `net4.7.2`, sync.context is restored for me. Is it a chance that there is some project level configuration which applies `ConfigureAwait(false)` globally? – mtkachenko Jul 30 '19 at 08:14
  • Also you can check final IL code using ILspy or dotPeek. – mtkachenko Jul 30 '19 at 08:16
  • 1
    This is of course not possible in normal conditions. How did you open the parent Form you're referring to? Do you have some other threading attempt somewhere else, related to this procedure? A left-over backgroundworker? Can you show some more code that gives a context to the snippet you posted. – Jimi Jul 30 '19 at 08:33
  • I found [this post](https://stackoverflow.com/questions/43220262/synchronization-context-not-preserved-in-winforms-event) would it help you to descibe the phenomenon? How to you open the form? – Mong Zhu Jul 30 '19 at 09:52
  • 3
    Is this in a modal dialog? Modal dialogs create their own message loops, which messes with the synchronization context. Try creating a working minimal example that we can run and see the problem. – Luaan Jul 30 '19 at 10:52
  • @Luaan to make the dialog modal does not suffice to evoke this problem. I am trying to recreate it. – Mong Zhu Jul 30 '19 at 11:12
  • Can you show us the code where you instantiate and run the `MainForm`? Are you calling `Show()` or `Application.Run()` and in what order? – John Wu Jul 30 '19 at 21:50
  • @MongZhu I think the post you linked was pretty close to describing the phenomenon I'm seeing here, although the detail is a little different. – wwarby Jul 31 '19 at 06:37
  • @wwarby I am glad to hear that. But I still am not able to reproduce your problem sucessfully. – Mong Zhu Jul 31 '19 at 06:56
  • I will just point you (as I am not sure if it is an answer) to [`Task.Yield()`](https://learn.microsoft.com/cs-cz/dotnet/api/system.threading.tasks.task.yield?view=netframework-4.8) method and [When would I use Task.Yield()?](https://stackoverflow.com/questions/22645024/when-would-i-use-task-yield). – dropoutcoder Aug 02 '19 at 22:27

0 Answers0