6

I was playing with Task.ConfigureAwait in order to better understand what is going beyond the hood. So i got this strange behavior while combining some UI access stuff with the ConfigureAwait.

Below is the sample app using a simple windows form, with 1 Button followed by the test results:

private async void btnDoWork_Click(object sender, EventArgs e)
{
    List<int> Results = await SomeLongRunningMethodAsync().ConfigureAwait(false);

    int retry = 0;
    while(retry < RETRY_COUNT)
    {
        try
        {
            // commented on test #1 & #3 and not in test #2
            //if(retry == 0)
                //throw new InvalidOperationException("Manually thrown Exception");

            btnDoWork.Text = "Async Work Done";
            Logger.Log("Control Text Changed", logDestination);
            return;
        }
        catch(InvalidOperationException ex)
        {
            Logger.Log(ex.Message, logDestination);
        }

        retry++;
    }
}

Now after button Click:

Test 1 Log results : (Exactly as the above code)

1. Cross-thread operation not valid: Control 'btnDoWork' accessed from a thread other than the thread it was created on.
2. Control Text Changed

Test 2 Log results : (Manual exception throw uncommented)

1.  Manually thrown Exception
2.  Cross-thread operation not valid: Control 'btnDoWork' accessed from a thread other than the thread it was created on.
3.  Control Text Changed

Test 3 Log results : (Same as 1 but without a debugger)

1. Control Text Changed

So the questions are:

  1. Why does the first UI Access (Cross-Thread Operation) have the next iteration of the loop execute on the Main Thread ?

  2. Why doesn't the manual exception lead to the same behavior ?

  3. Why does executing the above sample without a debugger attached (directly from exe) doesn't show the same behavior ?

Zein Makki
  • 29,485
  • 6
  • 52
  • 63
  • 1
    Please be explicit of what behavior you see in #3. It is a lot easier to explain why something is different if you first tell us how it was different. – Scott Chamberlain Jun 18 '16 at 15:21
  • @ScottChamberlain Question updated, thank you. – Zein Makki Jun 18 '16 at 15:25
  • Interestingly I can't reproduce on WPF, only on WinForm. The first time a cross-thread exception is thrown, as expected. The second time no exception is thrown, even though we're in the same thread. Weird Oo – Kevin Gosse Jun 18 '16 at 15:32
  • For your point 2/, it's explained by the fact that the cross-thread exception is thrown only the first time you try to modify the control. As to know why it's thrown only the first time, I'm still scratching my head – Kevin Gosse Jun 18 '16 at 15:33
  • @KooKiz The point is: If an exception is causing the context to go back to the main thread. Why doesn't a manual exception of the same type lead to the same result ? – Zein Makki Jun 18 '16 at 15:34
  • 1
    @user3185569 That's the thing, the exception **isn't** causing the context to go back to the main thread (as far as I can tell). It's still the same thread, but the exception is thrown only the first time – Kevin Gosse Jun 18 '16 at 15:36
  • 1
    @KooKiz That makes it even more interesting :) – Zein Makki Jun 18 '16 at 15:37
  • @user3185569 There is nothing interesting here. By default WinForms check for cross thread calls only in debug mode. – Ivan Stoev Jun 18 '16 at 15:42
  • 1
    @IvanStoev That doesn't explain why it throws the exception only the first time – Kevin Gosse Jun 18 '16 at 15:43
  • 2
    @KooKiz Sounds like `Text` property setter bug. Try setting different text like `btnDoWork.Text = "Async Work Done" + retry;` and I'm pretty sure you'll get exception every time. – Ivan Stoev Jun 18 '16 at 15:50
  • 1
    See the [reference source](http://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs,952c66876790b53a). It first stores the value in the field, then generates exception, and the next time since the `value == Text` it does just `return` :) – Ivan Stoev Jun 18 '16 at 15:56

1 Answers1

4

This one got me scratching my head a bit, but finally found the trick.

The code of the setter of the Button.Text property is:

  set
  {
    if (value == null)
      value = "";
    if (value == this.Text)
      return;
    if (this.CacheTextInternal)
      this.text = value;
    this.WindowText = value;
    this.OnTextChanged(EventArgs.Empty);
    if (!this.IsMnemonicsListenerAxSourced)
      return;
    for (Control control = this; control != null; control = control.ParentInternal)
    {
      Control.ActiveXImpl activeXimpl = (Control.ActiveXImpl) control.Properties.GetObject(Control.PropActiveXImpl);
      if (activeXimpl != null)
      {
        activeXimpl.UpdateAccelTable();
        break;
      }
    }
  }

The line throwing the exception is this.WindowText = value; (because it internally tries to access the Handle property of the button). The trick is that, right before, it sets the text property in some kind of cache:

if (this.CacheTextInternal)
   this.text = value;

I'll be honest, I have no clue how this cache works, or when it is activated or not (turns out, it seems to be activated in this precise case). But because of this, the text is set even though the exception was thrown.

On further iterations of the loop, nothing happens because the property has a special check to make sure you don't set the same text twice:

if (value == this.Text)
  return;

If you change your loop to set a different text every time, then you'll see that the exception is thrown consistently at each iteration.

Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94
  • That explains the delay in UI change. I see the log written `"Control Text Changed"` and then after couple of seconds the UI actually gets updated. And if the mouse was still over the button, the text doesn't change until the mouse leaves the control. That's a weird implementation. Thank you @KooKiz. – Zein Makki Jun 18 '16 at 15:57