views:

192

answers:

3

Hello. I'm getting an error that does not make sense.

Cross-thread operation not valid: Control 'buttonOpenFile' accessed from a thread other than the thread it was created on.

In my application, the UI thread fires off backgroundWorker1, which when almost complete fires off backgroundWorker2 and waits for it to complete. backgroundWorker1 waits for backgroundWorker2 to complete, before it completes. AutoResetEvent variables are used to flag when each of the workers complete. In backgroundWorker2_RunWorkerComplete a function is called that resets the form controls. It is in this ResetFormControls() function where the exception is thrown. I thought it was safe to modify form controls in the RunWorkerCompleted function. Both background workers are instantiated from the UI thread. Here is a greatly summarized version of what I am doing:

  AutoResetEvent evtProgrammingComplete_c = new AutoResetEvent(false);
  AutoResetEvent evtResetComplete_c = new AutoResetEvent(false);

  private void ResetFormControls()
  {
     toolStripProgressBar1.Enabled = false;
     toolStripProgressBar1.RightToLeftLayout = false;
     toolStripProgressBar1.Value = 0;

     buttonInit.Enabled = true;
     buttonOpenFile.Enabled = true; // Error occurs here.
     buttonProgram.Enabled = true;
     buttonAbort.Enabled = false;
     buttonReset.Enabled = true;
     checkBoxPeripheryModule.Enabled = true;
     checkBoxVerbose.Enabled = true;
     comboBoxComPort.Enabled = true;
     groupBoxToolSettings.Enabled = true;
     groupBoxNodeSettings.Enabled = true;
  }

  private void buttonProgram_Click(object sender, EventArgs e)
  {
     while (backgroundWorkerProgram.IsBusy)
        backgroundWorkerProgram.CancelAsync();

     backgroundWorkerProgram.RunWorkerAsync();
  }

  private void backgroundWorkerProgram_DoWork(object sender, DoWorkEventArgs e)
  {
     // Does a bunch of stuff...

     if (tProgramStat_c == eProgramStat_t.DONE)
     {
        tProgramStat_c = eProgramStat_t.RESETTING;

        while (backgroundWorkerReset.IsBusy)
           backgroundWorkerReset.CancelAsync();

        backgroundWorkerReset.RunWorkerAsync();
        evtResetComplete_c.WaitOne(LONG_ACK_WAIT * 2);

        if (tResetStat_c == eResetStat_t.COMPLETED)
           tProgramStat_c = eProgramStat_t.DONE;
     }
  }

  private void backgroundWorkerProgram_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  {
     // Updates form to report complete.  No problems here.

     evtProgrammingComplete_c.Set();
     backgroundWorkerProgram.Dispose();
  }

  private void backgroundWorkerReset_DoWork(object sender, DoWorkEventArgs e)
  {
     // Does a bunch of stuff...

     if (tResetStat_c == eResetStat_t.COMPLETED)
        if (tProgramStat_c == eProgramStat_t.RESETTING)
           evtProgrammingComplete_c.WaitOne();
  }

  private void backgroundWorkerReset_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  {
     CloseAllComms();
     ResetFormControls();
     evtResetComplete_c.Set();
     backgroundWorkerReset.Dispose();
  }

Any thoughts or suggestions you may have would be appreciated. I am using Microsoft Visual C# 2008 Express Edition. Thanks.

+1  A: 

BackgroundWorker objects cannot be nested. I recommend using .NET 4.0 Tasks if at all possible, since they do nest.

Nesting BGWs is only possible by using something like ActionDispatcher from the Nito.Async library.

Stephen Cleary
Thanks, Stephen, but my application performs well with the nested background workers (as far as the "background" tasks are concerned). The problem I am having is updating the form controls when `RunWorkerCompleted` is executed.
Jim Fell
+1 - Hadn't heard of the new Tasks namespace before this - it looks very useful.
Shane Fulmer
Tasks rock. Concurrent collections and everything. .Net 4 will make it much easier for people to use the work of the devil ... er, threading.
Ragoczy
A: 

Accessing controls within another thread will never be threadsafe.

Your question is: "How can I access a control created by the UI thread using another thread?"

The answer is by invoking your control. Here's a link where you can see a code sample allowing it.

Does this do what you need?

Will Marcouiller
+4  A: 

RunWorkerCompleted is going to execute on the thread that started the BackgroundWorker. Since you're chaining BackgroundWorkers (starting 2 from 1), 2's RunWorkerCompleted is executing on 1's thread, not the UI thread.

You'll want to marshall back to the UI thread with Invoke or move the UI update to 1's RunWorkerCompleted.

My suggestion would be to always check for InvokeRequired when updating the UI, that way you don't need to worry about what thread it's coming from.

Ragoczy
Technically, 2's RunWorkerCompleted will run on a totally different ThreadPool thread, not on 1's thread.
Stephen Cleary
Thanks, Ragoczy. So, if I understand correctly, `RunWorkerCompleted` runs on the thread that invoked `RunWorkerAsync` and not necessarily the thread that instantiated (created/declared) the BackgroundWorker. Is that correct?
Jim Fell
RunWorkerCompleted appears to run on a random threadpool thread (my quick test just happened to wind up reusing the same thread) UNLESS RunWorkerAsync is called from the main thread and a class that implements ISynchronizeInvoke -- it's interesting that both seem to be required in my limited test cases just now and there's probably more to it. Regardless, if your ResetFormControls method is changed to check InvokeRequired and Invoke if necessary, you should be okay-as that would let you implement other threading options (.net 4) in the future, while keeping your Form's interface threadsafe.
Ragoczy
Actually, the requirement for BGWs is that they take their "target" thread context from SynchronizationContext.Current. In WinForms apps, this is set to a WindowsFormsSynchronizationContext the first time a Win32 handle is created on that thread.My answer mentioned ActionDispatcher, which is a class I wrote that supplies a SynchronizationContext.Current, so it's possible to "own" BGWs from within ActionDispatcher's Run method.
Stephen Cleary