2

The awaitable below does not complete at the await point and it does not capture the UI context. It means the UI modifying code that follows will be invoked in another thread (thread pool thread in this case).

private async void Button1_Click(object sender, EventArgs e)
{
    StringBuilder sb = new StringBuilder();
    sb.Append($"{Thread.CurrentThread.ManagedThreadId}, ");

    Task t = Task.Delay(1000);
    await t.ConfigureAwait(false);

    sb.Append($"{Thread.CurrentThread.ManagedThreadId}");

    Text = sb.ToString();
}

The code above runs without any problem. No error at runtime.

Question

Why is it allowed to modify UI components in non-UI thread? Is there something wrong with my understanding?

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
Second Person Shooter
  • 14,188
  • 21
  • 90
  • 165
  • We aren't. That `Text = DateTime.Now.ToString();` seems to be setting a field or property, not affecting any UI element – Panagiotis Kanavos Apr 19 '19 at 09:42
  • What is `Text`? How is it tied to the UI? Databinding? – Kevin Gosse Apr 19 '19 at 09:43
  • What is that `Text`? What UI are we talking about? WinForms? WPF? Is `Text` a property to which a WPF element binds? In that case modifying `Text` won't modify the UI, it will notify it to go and read the new values. It's the UI thread that goes and reads `Text` and then updates the bound elements – Panagiotis Kanavos Apr 19 '19 at 09:44
  • @PanagiotisKanavos: WinForms. – Second Person Shooter Apr 19 '19 at 09:47
  • What is Text then? – Panagiotis Kanavos Apr 19 '19 at 09:53
  • @PanagiotisKanavos `Text` is a property of Form. I don't understand your question. – Second Person Shooter Apr 19 '19 at 09:54
  • The question is "what is this code?" Until now you haven't explained what this code was or where it run, ie at the top level of a form. You were forcing people to guess about the stack and the code – Panagiotis Kanavos Apr 19 '19 at 10:01
  • This is an interesting question. I would prefer if trying to update a UI control from a non-UI thread failed fast and consistently. But instead it seems that sometimes fails and sometimes not. – Theodor Zoulias Apr 19 '19 at 10:01
  • @TheodorZoulias it would be interesting if it was reproducible. No repro – Panagiotis Kanavos Apr 19 '19 at 10:03
  • @PanagiotisKanavos: Not only title but also textbox, label. – Second Person Shooter Apr 19 '19 at 10:03
  • @ArtificialHairlessArmpit no repro. In a new Winforms project, using this exact code, I get an InvalidOperationException as expected. I though perhaps there was a difference about the title, but there isn't. Did you modify the app.config settings perhaps, to disable this exception? – Panagiotis Kanavos Apr 19 '19 at 10:04
  • @Panagiotis Kanavos I can reproduce it: [screenshot](https://prnt.sc/ne3b7t). Windows 10, .NET Framework 4.7.2, C# 7.3, Visual Studio 2017 15.9.11. Plain vanilla new Windows Forms App. – Theodor Zoulias Apr 19 '19 at 10:09
  • Consistently crashes for me. .net 4.7.2, Windows 10, VS2019. New windows forms app – Kevin Gosse Apr 19 '19 at 10:11
  • 2
    Ok, got it. It crashes with the debugger attached, works without it. Looks like the cross-thread check is activated by default on winform only if the debugger is attached: https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs,309 – Kevin Gosse Apr 19 '19 at 10:12
  • 1
    Adding `Control.CheckForIllegalCrossThreadCalls = true;` makes it crash even without the debugger, as expected – Kevin Gosse Apr 19 '19 at 10:13
  • In .net 1.1 there was no cross-thread checks, this was added in .net 2.0. I suppose they made it opt-in to preserve retro-compatibility. WPF does not have this issue since they introduced cross-thread checks right away. Learned something today. – Kevin Gosse Apr 19 '19 at 10:15
  • @Kevin Gosse Yeap, starting with debugging [crashes for me too](https://prnt.sc/ne3h1w). Problem is, I prefer starting without debugging most of the time. – Theodor Zoulias Apr 19 '19 at 10:21
  • 1
    @TheodorZoulias Well, you can (and probably should) add `Control.CheckForIllegalCrossThreadCalls = true;` to your projects. I understand why it's not enabled by default, but I wonder why they didn't add it to the new-project template. – Kevin Gosse Apr 19 '19 at 10:22
  • 1
    @KevinGosse: You should make it as your answer. I will accept it. Thank you! – Second Person Shooter Apr 19 '19 at 10:28
  • @Kevin Gosse very helpful, thanks! From now on `Control.CheckForIllegalCrossThreadCalls = true;` will become a must-have line of code in my projects! Btw the `InvalidOperationException` can be catched and handled in the `Application.ThreadException` event. – Theodor Zoulias Apr 19 '19 at 10:36
  • 1
    While we're there, I opened an issue to see if this could be fixed in .net core: https://github.com/dotnet/winforms/issues/832 – Kevin Gosse Apr 19 '19 at 11:10

1 Answers1

3

Cross-thread checks are disabled by default in winforms unless the debugger is attached. This can be seen in the initialization of the checkForIllegalCrossThreadCalls field:

private static bool checkForIllegalCrossThreadCalls = Debugger.IsAttached;

If I had to guess, I'd say this is an attempt to preserve retro-compatibility. .NET 1.1 did not have cross-thread checks (and back in the days I was left wondering why my apps would mysteriously crash after a few hours), they were added with .NET 2.0. But this is a huge breaking change, and I suppose that's why they made it opt-in. With WPF, cross-thread checks were introduced right-away, so they could activate them for everybody.

In light of this, I strongly recommend to enable cross-thread checks manually in any winform project by adding this line to the entry-point:

[STAThread]
static void Main()
{
    Control.CheckForIllegalCrossThreadCalls = true;

    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());
}
Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94
  • 1
    It has a (minuscule) performance cost though. Updating the `Text` property of a `Form` becomes 4% slower with `CheckForIllegalCrossThreadCalls` enabled. :-) – Theodor Zoulias Apr 19 '19 at 11:15
  • 1
    I think a good compromise would be to use a preprocessor directive: #if DEBUG Control.CheckForIllegalCrossThreadCalls = true; #endif. I should be able to discover and fix these kind of bugs during development, before releasing the application. – Theodor Zoulias Apr 19 '19 at 11:29