繁体   English   中英

C# BackgroundWorker 中的跨线程操作无效

[英]C# Cross-thread operation not valid in BackgroundWorker

加载主窗体时,我将项目添加到列表框:

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";
}

然后我试图在 BackgroundWorker 中获取选定的项目值,但它失败了。

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

后台工作者不应尝试对 UI 做任何事情。 线程唯一可以做的就是通知那些感兴趣的人后台工作人员计算了一些值得注意的事情。

此通知是使用 Event ProgressChanged 完成的。

  • 使用 Visual Studio 设计器创建一个 BackGroundWorker。
  • 让设计师为 DoWork、ProgressChanged 和 RunWorkerCompleted 添加事件处理程序(如果需要)
  • 如果 BackGroundWorker 想要通知表单应该显示某些内容,请使用 ProgressChanged

您可能简化了您的问题,但后台工作人员不应读取组合框的选定值。 如果 combobox 发生变化,表单应在传递所选 combobox 项的值时启动后台工作程序。

因此,让我们把问题变得更有趣一点:如果用户在 comboBox1 中选择了一个项目,后台工作人员将被命令使用所选的 combobox 值计算一些东西。

在计算期间,BackGroundWorker 会定期通知表单有关进度和中间计算值的信息。 完成后,返回最终结果。

代码将是这样的:

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. 当 backgroundworker 启动时,用户无法再次更改 combobox1,因为我们无法在它仍然忙碌时启动同一个 backgroundworker。

因此 combobox 被禁用,并显示一个进度条。 在计算过程中,进度条会更新,中间结果显示在 Label1 中。 当 backgroundworker 完成后,进度条被移除,最终结果显示在 Lable1 中并再次启用 combobox。

请注意,后台工作人员正在计算时,表单的 rest 仍在工作。

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);
}

背景工作:

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;
}

您的表单会定期收到有关进度的通知:让它更新 ProgressBar 并在 Label1 中显示中间文本

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

当 BackgroundWorker 完成时,最终文本显示在 label1 中,进度条消失,Combobox 再次启用:

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

原因是后台工作程序在单独的线程上运行。 您必须使用 UI 线程从 UI 线程读取值。在这种情况下,cmbTest 在 UI 上

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

如果您需要值来执行异步处理

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
}

UI 元素只能由 UI 线程访问。

如果您使用的是 WinForms,那么您可以使用Control.InvokeRequired标志和Control.Invoke方法:

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获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用 invoke 方法,因为调用方所在的线程与创建控件的线程不同。

Control.Invoke在拥有控件的基础 window 句柄的线程上执行指定的委托。

如果您使用的是 WPF,那么也有解决方案。 Dispatcher.Invoke在 Dispatcher 关联的线程上同步执行指定的委托:

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

您可以在 Microsoft 文档上阅读更多关于windows forms 控件的线程安全调用WPF Threading Model的信息。

其他解决方案。

声明表单域:

private string _value;

从运行BackgroundWorker的地方的 UI 控件中在此字段中输入值

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

接下来,在DoWork方法中使用这个字段:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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