解决 WinForms 中的跨线程异常

8

目前我正在使用C#中的WinForms,并且我需要在后台运行应用程序。为此,我使用异步。当我运行应用程序时,它会显示一个异常,如下所示:

"跨线程操作无效: 从创建它的线程以外的线程访问控件''。"

我该如何解决这个错误?


8个回答

12

在调用控件的方法时,如果调用者所在的线程与创建该控件的线程不同,则需要使用Control.Invoke方法进行调用。以下是一个代码示例:

// you can define a delegate with the signature you want
public delegate void UpdateControlsDelegate();

public void SomeMethod()
{
    //this method is executed by the background worker
    InvokeUpdateControls();
}

public void InvokeUpdateControls()
{
    if (this.InvokeRequired)
    {
        this.Invoke(new UpdateControlsDelegate(UpdateControls));
    }
    else
    {
        UpdateControls();
    }
}

private void UpdateControls()
{
    // update your controls here
}

希望能对你有所帮助。


你好,感谢您的回复。我正在尝试使用您提供的代码,实际上我在button_click事件中调用了线程,但它总是进入else部分,而不是进入这个if (this.InvokeRequired)语句块。{ this.Invoke(new PerformSomeActionDelegate(MyAction)); } - Victor
@Victor:这意味着此时不需要使用Invoke。确保在线程中执行的所有代码都执行“安全”(Invoke)调用以更新窗体控件。 - Daniel Peñalba
@Victor: 对不起,这是不可能的 - Daniel Peñalba

4
通常,在WinForms中完成此类任务的最佳方法是使用BackgroundWorker,它将在后台线程上运行您的工作,并为您提供一种清晰易懂的方式来向UI报告状态。
在许多日常.NET编程中,显式创建线程或调用.Invoke通常意味着您没有充分利用框架的优势(当然,也有很多合法的低级操作的原因,只是它们比人们意识到的要少)。

谢谢您的回复,我正在将您提供的背景代码用于我的应用程序。但是它显示错误... - Victor
2
@Victor - 你必须使用.Invoke(参见其他人的示例),或者使用BackgroundWorker.ReportProgress方法(以及它引发的ProgressChanged事件)来访问表单。如果我正在使用BackgroundWorker,我总是更喜欢ReportProgress而不是直接调用Invoke。 - Will Dean

2

您需要检查您尝试更新的控件是否需要调用。可以这样做:

Action<Control, string> setterCallback = (toSet, text) => toSet.Text = text;

void SetControlText(Control toSet, string text) {
  if (this.InvokeRequired) {
    this.Invoke(setterCallback, toSet, text);
  }
  else {
    setterCallback(toSet, text);
  }
}

1
一个你可能会觉得有用的模式是,在与 GUI 交互的函数顶部进行检查,以查看是否正在运行在正确的线程上,并在需要时使函数调用自身。就像这样:
    public delegate void InvocationDelegate();

    public void DoGuiStuff(){
      if (someControl.InvokeRequired){
        someControl.Invoke(InvocationDelegate(DoGuiStuff));
        return;  
      }

      //GUI manipulation here
    }

使用这种模式 - 如果在调用方法时您在正确的线程上,它不会调用自身,但如果您在不同的线程上,则会调用自身,然后返回(因此GUI操作逻辑始终只被调用一次)。

1

从Invoke更新为begin Invoke

// you can define a delegate with the signature you want
public delegate void UpdateControlsDelegate();

public void SomeMethod()
{
    //this method is executed by the background worker
    InvokeUpdateControls();
}

public void InvokeUpdateControls()
{
    if (this.InvokeRequired)
    {
        this.BeginInvoke(new UpdateControlsDelegate(UpdateControls));
    }
    else
    {
        UpdateControls();
    }
}

private void UpdateControls()
{
    // update your controls here
}

嗨,Pankaj,我已经尝试过这段代码了,但是当invokerequired为true时,它总是返回false...我在我的按钮单击事件中调用了invokerequired。 - Victor
替换此处的按钮ID并重新检查。 - Pankaj
我写了这样的代码:private void btnsiteranks_Click_1(object sender, EventArgs e) { InvokeUpdateControls(); }public void InvokeUpdateControls() { if (this.InvokeRequired) { this.BeginInvoke(new PerformSomeActionDelegate(Geturls)); } else { Geturls(); } } - Victor
现在它正常工作了,我只是改变了代码,像这样if(!this.InvokeRequired)...但是当数据处理时,我想显示标签文本,怎么办...请帮帮我。 - Victor
@Pankaj,你的InvokeUpdateControls有点危险。如果需要调用Invoke,那么你正在异步执行委托,即将其安排到以后的某个时间执行,但是如果你正在同一线程上(或窗口句柄尚未创建),则立即在那里执行它。这可能是预期的,也可能不是,但应该记住这种区别。 - nawfal

0

UI的变化可以通过Control.Invoke()方法来完成,这个跨线程异常可以使用下面的代码片段来解决。

void UpdateWorker()
{
   //Here ddUser is the user control
   //Action to be performed should be called within { } as like below code
   if (this.ddUser.InvokeRequired)
      ddUser.Invoke(new MethodInvoker(() => { ddUser.Size = new Size(100, 100); }));
}

0

BeginInvoke

这是一种很好的方法,可以防止跨线程异常。我在书籍《C#程序员学习指南(MCSD)》中读到过。

您可以使用BeginInvoke

BeginInvoke方法用于从其他线程更改UI控件的值。它以线程安全的方式执行此操作。它需要一个委托,告诉它哪个UI控件需要更改其值。

  private async void button1_Click(object sender, EventArgs e)
   {
     Task task = Task.Run(() =>
        {
           this.BeginInvoke(new Action(() =>
               {
                  label1.Text = "Hello";
                  }));
         });
      await task;
  }
的值将被更改为“Hello”,由于它是线程安全的操作,因此不会出现异常。

0

我知道这个话题已经有10年了,但是我想通过lambda选择器来改进通用解决方案,而不是定义每种类型的setter。

    private void SetControlSafety<C, V>(C control, Expression<Func<C, V>> selector, V value)
    {
        if (this.InvokeRequired)
            this.Invoke(MyUtils.GetSetter(selector), control, value);
        else
            DataCrawlerUtils.GetSetter(selector)(control, value);
    }  

或者是静态的

    public static void SetControlSafety<C, V>(C control, Expression<Func<C, V>> selector, V value) where C : Control
    {
        if (control.InvokeRequired)
            control.Invoke(DataCrawlerUtils.GetSetter(selector), control, value);
        else
            DataCrawlerUtils.GetSetter(selector)(control, value);
    }

通过lambda选择了从这里获取值并赋给属性的GetSetter方法

    public static Action<T, TProperty> GetSetter<T, TProperty>(
       Expression<Func<T, TProperty>> pExpression
    )
    {
        var parameter1 = Expression.Parameter(typeof(T));
        var parameter2 = Expression.Parameter(typeof(TProperty));

        // turning an expression body into a PropertyInfo is common enough
        // that it's a good idea to extract this to a reusable method
        var member = (MemberExpression)pExpression.Body;
        var propertyInfo = (PropertyInfo)member.Member;

        // use the PropertyInfo to make a property expression
        // for the first parameter (the object)
        var property = Expression.Property(parameter1, propertyInfo);

        // assignment expression that assigns the second parameter (value) to the property
        var assignment = Expression.Assign(property, parameter2);

        // then just build the lambda, which takes 2 parameters, and has the assignment
        // expression for its body
        var setter = Expression.Lambda<Action<T, TProperty>>(
           assignment,
           parameter1,
           parameter2
        );

        return setter.Compile();
    }

然后使用起来非常简单

        SetControlSafety(txtStatus, x => x.Text, "Loading resources...");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接