Cross-thread operation not valid, even if using In

2019-06-20 05:25发布

问题:

This question already has an answer here:

  • Invoke or BeginInvoke cannot be called on a control until the window handle has been created 10 answers

I have a form with my custom controls on it.

I have a method in my Form:

private void SetEnabledOnControls(bool val)
{
  if (InvokeRequired)
  {
      Invoke((Action<bool>)SetEnabledOnControls, val);
  }
  else
  {
       //do the work - iterate over child controls, 
       //and they iterate over their children, etc...
  }
}

And inside the methods that are on the else branch I get the mentioned exception:
Cross-thread operation not valid: Control 'txtNumber' accessed from a thread other than the thread it was created on.

My scenario is actually a bit more complicated - I just extrapolated this as an example. What's actually going on is that I'm using WorkflowFoundation - I have StateMachineActivity (CTP1) running in WorkflowApplication (which runs in it's own thread), I subscribed to it's event, and from there I'm calling SetEnabledOnControls. Also, I'm using bookmarks to resume my workflow (and also, there's MEF on the side, not involved in the scenario).

All of that is irrelevant to my obvious misunderstanding of the InvokeRequired - how is it possible that if the InvokeRequired is false, I have cross threaded exception? I don't create any of the controls 'manually' - it's all there in the Initialize() placed by designer.

Can anyone shed some light on this?

Thanks!

EDIT Using GWLlosa suggestion, I've tracked the ThreadId using System.Threading.Thread.CurrentThread.ManagedThreadId. Now comes the weird part... the thread id in Initialize() is 10. Between passing the first 2 states, it comes in with Id 13 - InvokeRequired was true, and it invoked correctly. BUT, after the second state, when it enters SetEnabledOnControls it's again 13, but this time InvokeRequired is false! How come!? Later on, of course, it fails to change child controls (not surprising). Could it be that the Form somehow changed the thread it's living in??

EDIT 2 Now I'm calling with:

 if (IsHandleCreated)
 {
     Invoke((Action<bool>)SetEnabledOnControls, val);
 }

and it has IsHandleCreated to true, but still fails with what devSpeed pointed at.

EDIT 3 FACEPALM :) One of the buttons that were resuming state was at first CancelButton for the Form. When it was removed from the property as such, the codebihind still had the DialogResult=Cancel for it - so my form was indeed closing, and of course it was missing the handle so the InvokeRequired didn't return correct info, and hence the errors.

Thanks everyone! I learned a new thing today :)

回答1:

It might make your debugging easier if you log the thread ID when the controls are created (in the Initialize() function) and the thread ID just before you try to touch it. Generally, I've seen this happen when you've somehow created the controls on a thread other than the one you expect in the first place.



回答2:

I just ran into a similar problem myself, where I have a function tearing down, and then dynamically creating controls within a FlowLayoutPanel:

        public static void RenderEditorInstance(DataContext dataContext, object selectedItem, Form targetForm, Control targetControl, List<DynamicUserInterface.EditorControl> editorControls, EventHandler ComboBox_SelectedIndexChanged, EventHandler TextBoxControl_TextChanged, EventHandler CheckBox_CheckChanged, EventHandler NumericUpDown_ValueChanged, CheckedListControl.ItemChecked OnItemChecked, EventHandler dateTimePicker_ValueChanged, DynamicUserInterface.DuplicationValidationFailed liveLookupValidationFailed, DynamicUserInterface.PopulateComboBoxCallback populateComboBoxCallback)
        {           if (targetForm.InvokeRequired)
            {
                InstanceRenderer renderer = new InstanceRenderer(RenderEditorInstance);
                targetForm.Invoke(renderer, dataContext, selectedItem, targetForm, targetControl, editorControls, ComboBox_SelectedIndexChanged, TextBoxControl_TextChanged, CheckBox_CheckChanged, NumericUpDown_ValueChanged, OnItemChecked, dateTimePicker_ValueChanged, liveLookupValidationFailed, populateComboBoxCallback);
            }
            else
            {
                targetControl.Padding = new Padding(2);
                targetControl.Controls.Clear();

                ...{other code doing stuff here }
            }
         }

And in one instance of about 12 where this code was being used, a cross-thread exception was being raised. All instances of where this code was being used, was written in such a way, that the interface building is achieved asynchronously using the 'await' keyword.

Using the suggestion made by GWLlosa, I wrote an extension method to controls to get the OwningThread a control belongs to:

    public static Thread OwnerThread(this Control ctrl)
    {
        Thread activeThread = null;

        if (ctrl.InvokeRequired)
        {
            activeThread = (Thread)ctrl.Invoke(new Func<Control, Thread>(OwnerThread), new object[] { ctrl });
        }
        else
        {
            activeThread = Thread.CurrentThread;
        }

        return activeThread;
    }

..which highlighted that after a few iterations the Thread Id was indeed changing.

Buried deep inside some of this code were routines used to fetch data populating the relevant controls, through the use of Task.Run() which from MSDN (https://msdn.microsoft.com/en-us/library/hh195051(v=vs.110).aspx) clearly states:

The example shows that the asynchronous task executes on a different thread than the main application thread

Once the Task.Run() was taken out of the equation, the control's thread never changed. So you need be careful about how and when you use it!