简体   繁体   中英

C# Cross-thread operation not valid in BackgroundWorker

I'm adding items to a listbox when the main form is loaded:

private void MainForm_Load(object sender, EventArgs e)
{
    Dictionary<string, string> item = new Dictionary<string, string>();
    item.Add("Test 1", "test1");
    item.Add("Test 2", "test 2");

    cmbTest.DataSource = new BindingSource(item, null);
    cmbTest.DisplayMember = "Key";
    cmbTest.ValueMember = "Value";
}

Then I trying to get the selected item value in a BackgroundWorker but it's fails.

private void TestWorker_DoWork(object sender, DoWorkEventArgs e)
{
    string test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
    MessageBox.Show(test);
}

The backgroundworker should not try to do anything with the UI. The only thing that the thread can do is notify those who are interested that the background worker calculated something noteworthy.

This notification is done using the Event ProgressChanged.

  • Use Visual Studio Designer to create a BackGroundWorker.
  • Let designer add event handlers for DoWork, ProgressChanged, and if needed RunWorkerCompleted
  • If the BackGroundWorker wants to notify the form that something should be displayed, use ProgressChanged

You probably simplified your problem, but the backgroundworker should not read the selected value of the combo box. If the combobox changes, the Form should start the backgroundworker while passing the value of the selected combobox item.

So let's make the problem a bit more interesting: if the user selects an item in comboBox1, the backgroundworker is ordered to calculate something with the selected combobox value.

During the calculation the BackGroundWorker regularly notifies the form about progress and intermediate calculated values. When it finishes, the end result is returned.

The code will be like this:

private void InitializeComponent()
{
    this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
    this.SuspendLayout();
    this.backgroundWorker1.DoWork += new DoWorkEventHandler(this.DoBackgroundWork);
    this.backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(this.NotifyProgress);
    this.backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(
                                                 this.OnBackgounrWorkCompleted);
    ...
}

When an item is selected comboBox1 the backgroundworker is started using the selected value. While the backgroundworker is started, users can't change combobox1 again, because we can't start the same backgroundworker while it is still busy.

Therefore the combobox is disabled, and a progressbar is shown. During the calculations the progressbar is updated and intermediate results are shown in Label1. When the backgroundworker finishes, the progressbar is removed, the final result is displayed in Lable1 and the combobox is enabled again.

Note that the rest of the form is still working while the backgroundworker is calculating.

private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
    ComboBox comboBox = (ComboBox)sender;

    // disable ComboBox, show ProgressBar:
    comboBox.Enabled = false;
    this.progressBar1.Minimum = 0;
    this.progressBar1.Maximum = 100;
    this.progressBar1.Value = 0;
    this.progressBar1.Visible = true;

    // start the backgroundworker using the selected value:
    this.backgroundWorker1.RunWorkerAsync(comboBox.SelectedValue);
}

The Background work:

private void DoBackgroundWork(object sender, DoWorkEventArgs e)
{
    // e.Argument contains the selected value of the combobox
    string test = ((KeyValuePair<string, string>)e.Argument;

    // let's do some lengthy processing:
    for (int i=0; i<10; ++i)
    {
        string intermediateText = Calculate(test, i);

        // notify about progress: use a percentage and intermediateText
        this.backgroundWorker1.ReportProgress(10*i, intermediateText);
    }

    string finalText = Calculate(test, 10);

    // the result of the background work is finalText
    e.Result = finalText;
}

Regularly your Form gets notified about the progress: let it update the ProgressBar and show the intermediate text in Label1

private void NotifyProgress(object sender, ProgressChangedEventArgs e)
{
    this.progressBar1.Value = e.ProgressPercentage;
    this.label1.Text = e.UserState.ToString();
}

When the BackgroundWorker completes, the final text is displayed in label1, the progressbar disappears and the Combobox is enabled again:

private void OnBackgoundWorkCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    this.label1.Text = e.Result.ToString();
    this.progressBar1.Visible = false;
    this.comboBox1.Enabled = true;
}

The reason is the background worker runs on separate threads. You have to use UI thread to read values from UI thread.in this case, cmbTest is on the UI

 this.Invoke(new Action(() =>
 {
   string test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
 }));

If you need values to do async proccess

private void TestWorker_DoWork(object sender, DoWorkEventArgs e)
{
 string test;
 this.Invoke(new Action(() =>
 {
     //Any other things you need from UI thread
     test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
 }));
 //Here you have access to UI thread values
}

The UI elements can only be accessed by the UI thread.

If you are working with WinForms so you can use Control.InvokeRequired flag and Control.Invoke metod:

private void TestWorker_DoWork(object sender, DoWorkEventArgs e)
{
    if(!cmbTest.InvokeRequired)
    {
        string test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
        MessageBox.Show(test);
    }
    else 
    {
        string test;
        Invoke(() => test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value);
        Invoke(() => MessageBox.Show(test));
    }
}

Control.InvokeRequired gets a value indicating whether the caller must call an invoke method when making method calls to the control because the caller is on a different thread than the one the control was created on.

Control.Invoke executes the specified delegate on the thread that owns the control's underlying window handle.

If you are working with WPF than there is a solution for it too. Dispatcher.Invoke executes the specified delegate synchronously on the thread the Dispatcher is associated with:

private void TestWorker_DoWork(object sender, DoWorkEventArgs e)
{
    this.Dispatcher.Invoke(() => {
        string test = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
        MessageBox.Show(test);
    });
}

You can read more about thread-safe calls to windows forms controls of about WPF Threading Model on Microsoft docs.

Other solution.

Declare form field:

private string _value;

Enter the value in this field from the UI control in the place where you run BackgroundWorker

private void RunWorkButton_Click(object sender, EventArgs e)
{
    _value = ((KeyValuePair<string, string>)cmbTest.SelectedItem).Value;
    testWorker.RunWorkerAsync();
}

Next, use this field in the DoWork method:

private void TestWorker_DoWork(object sender, DoWorkEventArgs e)
{
    // use _value somehow
    string test = _value;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM