9

I have a WinForms app, and I have some code that needs to run on the UI thread. However, the code after the await runs on a different thread.

protected override async void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated(e);

    // This runs on the UI thread.
    mainContainer.Controls.Clear();

    var result = await DoSomethingAsync();

    // This also needs to run on the UI thread, but it does not.
    // Instead it throws an exception:
    // "Cross-thread operation not valid: Control 'mainContainer' accessed from a thread other than the thread it was created on"
    mainContainer.Controls.Add(new Control());
}

I also tried explicitly adding ConfigureAwait(true), but it makes no difference. My understanding was that if I omit ConfigureAwait(false), then the continuation should run on the original thread. Is this incorrect in some situations?

I've also noticed that if I add a control to the collection before the await, then the continuation magically runs on the correct thread.

protected override async void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated(e);

    // This runs on the UI thread.
    mainContainer.Controls.Add(new Control());
    mainContainer.Controls.Clear();

    var result = await DoSomethingAsync();

    // This also runs on the UI thread now. Why?
    mainContainer.Controls.Add(new Control());
}

My question is:

  1. Why is this happening?
  2. How do I convince the continuation to run on the UI thread (ideally without doing my hack of adding a control and removing it)?

For reference, here are the important parts of DoSomethingAsync. It submits an HTTP request using RestSharp.

protected async Task DoSomethingAsync()
{
    IRestRequest request = CreateRestRequest();

    // Here I await the response from RestSharp.
    // Client is an IRestClient instance.
    // I have tried removing the ConfigureAwait(false) part, but it makes no difference.
    var response = await Client.ExecuteTaskAsync(request).ConfigureAwait(false);

    if (response.ResponseStatus == ResponseStatus.Error)
        throw new Exception(response.ErrorMessage ?? "The request did not complete successfully.");

    if (response.StatusCode >= HttpStatusCode.BadRequest)
        throw new Exception("Server responded with an error: " + response.StatusCode);

    // I also do some processing of the response here; omitted for brevity.
    // There are no more awaits.
}
20
  • 1
    Why do you think this runs on a different thread? Where does this code runs? event handler? Commented Aug 25, 2015 at 18:40
  • I know it runs on a different thread because the code after the await throws an exception: "Cross-thread operation not valid: Control 'mainContainer' accessed from a thread other than the thread it was created on". Commented Aug 25, 2015 at 18:42
  • Yes, the code runs in an event handler. I will update my question. Commented Aug 25, 2015 at 18:42
  • Which event does OnHandleCreated is registered to? Because event handlers in winforms usually have 2 parameters. Commented Aug 25, 2015 at 18:47
  • 1
    There is definitely dark magic involved with the HandleCreated event. I switched to using the Load event, and everything works fine. I'm not sure why HandleCreated was being used, anyways. (I inherited this code from others, and a lot of it is very messy). Commented Aug 25, 2015 at 20:06

2 Answers 2

8

My understanding was that if I omit ConfigureAwait(false), then the continuation should run on the original thread. Is this incorrect in some situations?

What actually happens is that await will capture the current context by default, and use this context to resume the async method. This context is SynchronizationContext.Current, unless it is null, in which case it is TaskScheduler.Current (usually the thread pool context). Most of the time, the UI thread has a UI SynchronizationContext - in the case of WinForms, an instance of WinFormsSynchronizationContext.

I've also noticed that if I add a control to the collection before the await, then the continuation magically runs on the correct thread.

No thread starts with a SynchronizationContext automatically. The WinForms SynchronizationContext is installed on-demand when the first control is created. This is why you're seeing it resume on a UI thread after creating a control.

Since moving to OnLoad is a workable solution, I recommend you just go with that. The only other option (to resume on the UI thread before a control is created) is to manually create a control before your first await.

Sign up to request clarification or add additional context in comments.

1 Comment

Actually, WindowsFormsSynchronizationContext gets installed in Control's constructor here, so it should be there already when OnHandleCreated is called. This has to be something else. There's a possibly related bug, but in that case both OnHandleCreated and OnLoad would be affected.
2

It appears that something strange is happening with OnHandleCreated. My solution was to use OnLoad instead. I'm pretty happy with this solution because there is really no reason to use OnHandleCreated in my situation.

I'm still curious as to why this is happening, so if anyone knows, feel free to post another answer.

Edit:

I found the real problem: it turns out that I was calling Form.ShowDialog() after a ConfigureAwait(false). As such, the form was being constructed on the UI thread, but then I was calling ShowDialog on a non-UI thread. I'm surprised that this worked at all.

I've removed the ConfigureAwait(false) so now ShowDialog is getting called on the UI thread.

4 Comments

Could you place Debug.WriteLine(new { SynchronizationContext.Current }) right before await DoSomethingAsync() inside OnHandleCreated? Does it show System.Windows.Forms.WindowsFormsSynchronizationContext (expected) or System.Threading.SynchronizationContext ?
I get a System.Threading.SynchronizationContext before the await, and null after the await. I'm not sure why I was getting a non-null value yesterday. But I guess that explains it. I also noticed that if I run the same code in the same project on a different branch in our repo, I get a System.Windows.Forms.WindowsFormsSynchronizationContext, and everything works properly. I wonder if it has something to do with the way the form is getting initialized.
With your recent edit, you should mark your answer as accepted as it explains what actually happened. Otherwise it's safe to use await in OnHandleCreated, see my comment to Stephen's answer.
@Noseratio Yeah, I have to wait 2 days before I can accept my own answer. I will accept it once SO lets me :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.