协慌网

登录 贡献 社区

如何从其他线程更新 GUI?

从另一个线程更新Label的最简单方法是什么?

我在thread1上有一个Form ,从那里我开始另一个线程( thread2 )。当thread2正在处理一些文件时,我想更新Form上的Label ,其中包含thread2工作的当前状态。

我怎样才能做到这一点?

答案

最简单的方法是传递给Label.Invoke的匿名方法:

// Running on the worker thread
string newText = "abc";
form.Label.Invoke((MethodInvoker)delegate {
    // Running on the UI thread
    form.Label.Text = newText;
});
// Back on the worker thread

请注意, Invoke阻止执行直到完成 - 这是同步代码。这个问题并不是关于异步代码的问题,但是当你想要了解异步代码时,Stack Overflow 上有很多关于编写异步代码的内容。

对于. NET 2.0,这里有一些我编写的代码,它完全符合您的要求,适用于Control上的任何属性:

private delegate void SetControlPropertyThreadSafeDelegate(
    Control control, 
    string propertyName, 
    object propertyValue);

public static void SetControlPropertyThreadSafe(
    Control control, 
    string propertyName, 
    object propertyValue)
{
  if (control.InvokeRequired)
  {
    control.Invoke(new SetControlPropertyThreadSafeDelegate               
    (SetControlPropertyThreadSafe), 
    new object[] { control, propertyName, propertyValue });
  }
  else
  {
    control.GetType().InvokeMember(
        propertyName, 
        BindingFlags.SetProperty, 
        null, 
        control, 
        new object[] { propertyValue });
  }
}

像这样称呼它:

// thread-safe equivalent of
// myLabel.Text = status;
SetControlPropertyThreadSafe(myLabel, "Text", status);

如果您使用的是. NET 3.0 或更高版本,则可以将上述方法重写为Control类的扩展方法,这样可以简化对以下内容的调用:

myLabel.SetPropertyThreadSafe("Text", status);

更新 05/10/2010:

对于. NET 3.0,您应该使用以下代码:

private delegate void SetPropertyThreadSafeDelegate<TResult>(
    Control @this, 
    Expression<Func<TResult>> property, 
    TResult value);

public static void SetPropertyThreadSafe<TResult>(
    this Control @this, 
    Expression<Func<TResult>> property, 
    TResult value)
{
  var propertyInfo = (property.Body as MemberExpression).Member 
      as PropertyInfo;

  if (propertyInfo == null ||
      [email protected]().IsSubclassOf(propertyInfo.ReflectedType) ||
      @this.GetType().GetProperty(
          propertyInfo.Name, 
          propertyInfo.PropertyType) == null)
  {
    throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
  }

  if (@this.InvokeRequired)
  {
      @this.Invoke(new SetPropertyThreadSafeDelegate<TResult> 
      (SetPropertyThreadSafe), 
      new object[] { @this, property, value });
  }
  else
  {
      @this.GetType().InvokeMember(
          propertyInfo.Name, 
          BindingFlags.SetProperty, 
          null, 
          @this, 
          new object[] { value });
  }
}

它使用 LINQ 和 lambda 表达式来允许更清晰,更简单和更安全的语法:

myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile

现在不仅在编译时检查属性名称,属性的类型也是如此,因此不可能(例如)将字符串值赋给布尔属性,从而导致运行时异常。

不幸的是,这并没有阻止任何人做愚蠢的事情,比如传入另一个Control的属性和值,所以以下内容将很乐意编译:

myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);

因此,我添加了运行时检查,以确保传入的属性确实属于调用该方法的Control 。不完美,但仍然比. NET 2.0 版本好很多。

如果有人对如何为编译时安全性改进此代码有任何进一步的建议,请评论!

处理长期工作

.NET 4.5 和 C#5.0 开始,您应该使用基于任务的异步模式(TAP)异步 - 等待 所有区域 (包括 GUI)中的关键字:

TAP 是新开发的推荐异步设计模式

而不是异步编程模型(APM)基于事件的异步模式(EAP) (后者包括BackgroundWorker 类 )。

然后,推荐的新开发解决方案是:

  1. 事件处理程序的异步实现(是的,就是全部):

    private async void Button_Clicked(object sender, EventArgs e)
    {
        var progress = new Progress<string>(s => label.Text = s);
        await Task.Factory.StartNew(() => SecondThreadConcern.LongWork(progress),
                                    TaskCreationOptions.LongRunning);
        label.Text = "completed";
    }
  2. 通知 UI 线程的第二个线程的实现:

    class SecondThreadConcern
    {
        public static void LongWork(IProgress<string> progress)
        {
            // Perform a long running work...
            for (var i = 0; i < 10; i++)
            {
                Task.Delay(500).Wait();
                progress.Report(i.ToString());
            }
        }
    }

请注意以下事项:

  1. 以顺序方式编写的简短而干净的代码,没有回调和显式线程。
  2. 任务而不是线程
  3. async关键字,允许使用await反过来阻止事件处理程序达到完成状态,直到任务完成,同时不阻止 UI 线程。
  4. 进度类(参见IProgress 接口 ),支持Separation of Concerns(SoC)设计原则,不需要显式调度和调用。它使用来自其创建位置的当前SynchronizationContext (此处为 UI 线程)。
  5. TaskCreationOptions.LongRunning ,提示不将任务排入ThreadPool

有关更详细的例子,请参阅: C#的未来: 约瑟夫 · 阿尔巴哈里 “等待”的人会遇到好事

另请参阅UI 线程模型概念。

处理异常

下面的代码段是如何处理异常和切换按钮的Enabled属性以防止在后台执行期间多次单击的示例。

private async void Button_Click(object sender, EventArgs e)
{
    button.Enabled = false;

    try
    {
        var progress = new Progress<string>(s => button.Text = s);
        await Task.Run(() => SecondThreadConcern.FailingWork(progress));
        button.Text = "Completed";
    }
    catch(Exception exception)
    {
        button.Text = "Failed: " + exception.Message;
    }

    button.Enabled = true;
}

class SecondThreadConcern
{
    public static void FailingWork(IProgress<string> progress)
    {
        progress.Report("I will fail in...");
        Task.Delay(500).Wait();

        for (var i = 0; i < 3; i++)
        {
            progress.Report((3 - i).ToString());
            Task.Delay(500).Wait();
        }

        throw new Exception("Oops...");
    }
}