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.