I've read lots of articles, completed a linked-in learning course and am still confused by async / await and Task.Run().
I have a WinForms app. I want to connect to two databases, and if the connections are successful, read a lot of data, and use that data to update the UI. I want it to run async because it takes quite a few seconds to connect to the database and load, and some of our users have low bandwidth, and will be sitting there for an even longer time.
The thing I don't understand is that I had it working, and then I started getting illegal cross-thread call exceptions. Now: I know that I can use a delegate to update the visibility, but that is not how I want to solve the problem. I want to understand how the control flow works in which thread / synchronization context, so that I can better design the UI updates later, without spamming BeginInvoke
or Invoke
everywhere.
So I worked out what I'd done, reversed it, and got it working again. But I don't understand why it stopped working. Let me explain:
To make my app async
, I made the Main Form loading event async, and used that to kick-off the work:
private async void Main_Load(object sender, EventArgs e)
{
this.actionProgress.Visible = true; \\<= this works fine.
Model = new Model(SiteCode, this);
bool connected = await Model.InitialiseConnectionsAsync().ConfigureAwait(true);
/* Snip StatusLight update etc*/
this.actionProgress.Visible = false; \\<= this started giving me an illegal cross-thread exception.
}
actionProgress
is a toolStripProgressBar
. There was some other code to update it based on the 'connected' result, but I've omitted that for clarity.
ModelInitialiseConnectionsAsync()
is as follows:
public async Task<bool> InitialiseConnectionsAsync()
{
bool piConnected = await OpenPIConnection().ConfigureAwait(true);
PIConnection.ConnectionStateChanged += PIConnection_ConnectionStateChanged;
bool afConnected = await OpenAFConnection().ConfigureAwait(true);
AFConnection.ConnectionStateChanged += AFConnection_ConnectionStateChanged;
return piConnected && afConnected;
}
And the two connection methods - synchronous code that I want to run asynchronously via Task.Run()
public async Task<bool> OpenPIConnection()
{
PIConnection = new PIConnection();
bool result = await Task.Run(() => PIConnection.ConnectToPI()).ConfigureAwait(true);
return result;
}
public async Task<bool> OpenAFConnection()
{
AFConnection = new AFConnection();
bool result = await Task.Run(() => AFConnection.ConnectToAF()).ConfigureAwait(true);
return result;
}
What I thought would happen is that InitialiseModelConnectionsAsync would happily run off, spawn some background threads to do the work, my UI would remain responsive, and then when the connections are finished, it would carry-on in the UI thread and update.
I read some more articles, played around with & without configureAwait(), tried to flatten the calls (I had nested Task.Run calls), but nothing worked. So I went back to look at what I'd changed.
What changed is the argument passed to the Model
constructor: SiteCode. In production, this will be an argument passed to the executable. The change I made was to check if a valid argument had been passed, and if not, generate a form for the user to select it. I put this code into the Form Constructor (just after the arguments were parsed):
if (SiteCode == string.Empty)
{
using (SiteSelector siteSelector = new SiteSelector())
{
siteSelector.ShowDialog();
SiteCode = siteSelector.SelectedSite;
}
}
I undid this, and my exception went away! So I figured I'd moved it from the Main constructor into the Main_Load() method... and my exception stayed away.
At that point, being thoroughly confused, I figured I'd come here for help. I have 'fixed' it, but I don't understand why, and as such I suspect that it might break again.
Any help gratefully received!
Edit: As per the comments below, I checked the hWnd and the SynchronizationContest in the MainForm constructor and in the MainForm_Load event, with the SiteSelector Form being called in the constructor (leading to the cross-thread exception) and the SiteSelector Form being called in the _Load event (which works OK).
SiteSelector shown in constructor:
Main Form Handle at constructor: 593070, Main Form Synchronisation Context at constructor: System.Windows.Forms.WindowsFormsSynchronizationContext
Main Form Synchronisation Context at constructor, after SiteSelector shown: System.Threading.SynchronizationContext
Main Form Handle at _load: 593070, Main Form Synchronisation Context at _Load: System.Threading.SynchronizationContext
Thread before await: 1, Context before await = System.Threading.SynchronizationContext
Thread after await: 5
System.Threading.SynchronizationContext.Current.**get** returned null.
With the Site Selector moved to the _Load Event
Main Form Handle at constructor: 199726, Main Form Synchronisation Context at constructor: System.Windows.Forms.WindowsFormsSynchronizationContext
Main Form Handle at _load: 199726, Main Form Synchronisation Context at _Load: System.Windows.Forms.WindowsFormsSynchronizationContext
Main Form Synchronisation Context at _Load, after SiteSelector shown: System.Windows.Forms.WindowsFormsSynchronizationContext
Thread before await: 1, Context before await = System.Windows.Forms.WindowsFormsSynchronizationContext
Thread after await: 1, Context after await = System.Windows.Forms.WindowsFormsSynchronizationContext
So... it is certainly having some effect!