0

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!

ainwood
  • 992
  • 7
  • 17
  • 1
    Are you running on a very old framework? Windows Forms used to have a weird situation where `SynchronizationContext.Current` wasn't set until the first Windows `HANDLE` was created; I believe this is no longer the case on newer framework versions. You may want to check `SynchronizationContext.Current` before the first `await` just to make sure. If that's not it, then it's likely something with WinForms treating the first created/shown form as "the main form"; sometimes you have to do weird things like show your main form hidden first and *then* show a splash or dialog. – Stephen Cleary May 27 '22 at 17:23
  • @StephenCleary - thanks for the suggestion. I've edited-in the debugger output for the scenarios above. It seems that showing the SiteSelector form in the MainForm constructor clears / sets the SynchonizationContext to `System.Threading.SynchronizationContext` - i.e. not the WindowsForms one. By moving that form to the _Load event, it seems to behave. Can I assume that, aside from this behavior, there is nothing fundamentally wrong with my async/await, Task.Run() code? (except ConfiureAwait probably not needed, and I need to restore my error handling!) – ainwood May 29 '22 at 21:26
  • Oh - apologies on not including the framework details. 4.7.2, prefer 32-bit. – ainwood May 30 '22 at 00:56
  • For anyone who stumbles across this, as noted above I've moved the logic that shows the form to set the Site to the Main_Load() event, and everything seems to be working OK. – ainwood Jun 12 '22 at 23:41

0 Answers0