首页 文章

如何从其他线程更新GUI?

提问于
浏览
1212

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

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

我怎样才能做到这一点?

30 回答

  • 7

    我认为最简单的方法:

    void Update()
       {
           BeginInvoke((Action)delegate()
           {
               //do your update
           });
       }
    
  • 24

    当您在UI线程中时,您可以询问它的同步上下文任务调度程序 . 它会给你一个TaskScheduler来安排UI线程上的所有内容 .

    然后,您可以链接您的任务,以便在结果准备好后,另一个任务(在UI线程上安排)选择它并将其分配给标签 .

    public partial class MyForm : Form
    {
      private readonly TaskScheduler _uiTaskScheduler;
      public MyForm()
      {
        InitializeComponent();
        _uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
      }
    
      private void buttonRunAsyncOperation_Click(object sender, EventArgs e)
      {
        RunAsyncOperation();
      }
    
      private void RunAsyncOperation()
      {
        var task = new Task<string>(LengthyComputation);
        task.ContinueWith(antecedent =>
                             UpdateResultLabel(antecedent.Result), _uiTaskScheduler);
        task.Start();
      }
    
      private string LengthyComputation()
      {
        Thread.Sleep(3000);
        return "47";
      }
    
      private void UpdateResultLabel(string text)
      {
        labelResult.Text = text;
      }
    }
    

    这适用于preferred way of writing concurrent code now的任务(而不是线程) .

  • 350

    这是你应该这样做的经典方式:

    using System;
    using System.Windows.Forms;
    using System.Threading;
    
    namespace Test
    {
        public partial class UIThread : Form
        {
            Worker worker;
    
            Thread workerThread;
    
            public UIThread()
            {
                InitializeComponent();
    
                worker = new Worker();
                worker.ProgressChanged += new EventHandler<ProgressChangedArgs>(OnWorkerProgressChanged);
                workerThread = new Thread(new ThreadStart(worker.StartWork));
                workerThread.Start();
            }
    
            private void OnWorkerProgressChanged(object sender, ProgressChangedArgs e)
            {
                // Cross thread - so you don't get the cross-threading exception
                if (this.InvokeRequired)
                {
                    this.BeginInvoke((MethodInvoker)delegate
                    {
                        OnWorkerProgressChanged(sender, e);
                    });
                    return;
                }
    
                // Change control
                this.label1.Text = e.Progress;
            }
        }
    
        public class Worker
        {
            public event EventHandler<ProgressChangedArgs> ProgressChanged;
    
            protected void OnProgressChanged(ProgressChangedArgs e)
            {
                if(ProgressChanged!=null)
                {
                    ProgressChanged(this,e);
                }
            }
    
            public void StartWork()
            {
                Thread.Sleep(100);
                OnProgressChanged(new ProgressChangedArgs("Progress Changed"));
                Thread.Sleep(100);
            }
        }
    
    
        public class ProgressChangedArgs : EventArgs
        {
            public string Progress {get;private set;}
            public ProgressChangedArgs(string progress)
            {
                Progress = progress;
            }
        }
    }
    

    您的工作线程有一个事件 . 您的UI线程从另一个线程开始执行工作并挂接该工作事件,以便您可以显示工作线程的状态 .

    然后在UI中,您需要跨线程来更改实际控件...如标签或进度条 .

  • 204

    您可以使用已存在的委托 Action

    private void UpdateMethod()
    {
        if (InvokeRequired)
        {
            Invoke(new Action(UpdateMethod));
        }
    }
    
  • 7

    以前的答案中没有必要的Invoke内容 .

    您需要查看WindowsFormsSynchronizationContext:

    // In the main thread
    WindowsFormsSynchronizationContext mUiContext = new WindowsFormsSynchronizationContext();
    
    ...
    
    // In some non-UI Thread
    
    // Causes an update in the GUI thread.
    mUiContext.Post(UpdateGUI, userData);
    
    ...
    
    void UpdateGUI(object userData)
    {
        // Update your GUI controls here
    }
    
  • 32
    Label lblText; //initialized elsewhere
    
    void AssignLabel(string text)
    {
       if (InvokeRequired)
       {
          BeginInvoke((Action<string>)AssignLabel, text);
          return;
       }
    
       lblText.Text = text;           
    }
    

    请注意 BeginInvoke() 优先于 Invoke() ,因为它不太可能导致死锁(但是,仅在将文本分配给标签时这不是问题):

    使用 Invoke() 时,您正在等待返回的方法 . 现在,你可能需要在调用的代码中做一些需要等待线程的东西,如果它隐藏在你正在调用的某些函数中,这可能不会立即明显,这本身可能通过事件处理程序间接发生 . 所以你会等待线程,线程会等你,你就陷入僵局 .

    这实际上导致我们发布的一些软件挂起 . 通过用 BeginInvoke() 替换 Invoke() 很容易解决 . 除非您需要同步操作(如果需要返回值),否则请使用 BeginInvoke() .

  • 119

    线程代码经常出错并且总是难以测试 . 您无需编写线程代码来从后台任务更新用户界面 . 只需使用BackgroundWorker类来运行任务及其ReportProgress方法即可更新用户界面 . 通常,您只报告完成百分比,但是's another overload that includes a state object. Here'是一个仅报告字符串对象的示例:

    private void button1_Click(object sender, EventArgs e)
        {
            backgroundWorker1.WorkerReportsProgress = true;
            backgroundWorker1.RunWorkerAsync();
        }
    
        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            Thread.Sleep(5000);
            backgroundWorker1.ReportProgress(0, "A");
            Thread.Sleep(5000);
            backgroundWorker1.ReportProgress(0, "B");
            Thread.Sleep(5000);
            backgroundWorker1.ReportProgress(0, "C");
        }
    
        private void backgroundWorker1_ProgressChanged(
            object sender, 
            ProgressChangedEventArgs e)
        {
            label1.Text = e.UserState.ToString();
        }
    

    如果您总是想要更新相同的字段,那就没问题 . 如果要进行更复杂的更新,可以定义一个类来表示UI状态并将其传递给ReportProgress方法 .

    最后一件事,一定要设置 WorkerReportsProgress 标志,否则 ReportProgress 方法将被完全忽略 .

  • 53

    只需使用以下内容:

    this.Invoke((MethodInvoker)delegate
                {
                    progressBar1.Value = e.ProgressPercentage; // runs on UI thread
                });
    
  • 713

    .NET 3.5的Fire and forget扩展方法

    using System;
    using System.Windows.Forms;
    
    public static class ControlExtensions
    {
        /// <summary>
        /// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
        /// </summary>
        /// <param name="control"></param>
        /// <param name="code"></param>
        public static void UIThread(this Control @this, Action code)
        {
            if (@this.InvokeRequired)
            {
                @this.BeginInvoke(code);
            }
            else
            {
                code.Invoke();
            }
        }
    }
    

    这可以使用以下代码行调用:

    this.UIThread(() => this.myLabel.Text = "Text Goes Here");
    
  • 11

    由于场景的微不足道,我实际上会对状态进行UI线程轮询 . 我想你会发现它可以很优雅 .

    public class MyForm : Form
    {
      private volatile string m_Text = "";
      private System.Timers.Timer m_Timer;
    
      private MyForm()
      {
        m_Timer = new System.Timers.Timer();
        m_Timer.SynchronizingObject = this;
        m_Timer.Interval = 1000;
        m_Timer.Elapsed += (s, a) => { MyProgressLabel.Text = m_Text; };
        m_Timer.Start();
        var thread = new Thread(WorkerThread);
        thread.Start();
      }
    
      private void WorkerThread()
      {
        while (...)
        {
          // Periodically publish progress information.
          m_Text = "Still working...";
        }
      }
    }
    

    该方法避免了使用 ISynchronizeInvoke.InvokeISynchronizeInvoke.BeginInvoke 方法时所需的编组操作 . 使用编组技术没有任何问题,但您需要注意几个警告 .

    • 确保不要过于频繁地调用 BeginInvoke ,否则可能会超出消息泵 .

    • 在工作线程上调用 Invoke 是一个阻塞调用 . 它将暂时停止该线程中正在进行的工作 .

    我在这个答案中提出的策略颠倒了线程的通信角色 . UI线程轮询它而不是工作线程推送数据 . 这是许多场景中使用的常见模式 . 由于你想要做的只是显示工作线程的进度信息,我想你会发现这个解决方案是编组解决方案的一个很好的替代方案 . 它具有以下优点 .

    • UI和工作线程保持松散耦合,而不是紧密耦合它们的 Control.InvokeControl.BeginInvoke 方法 .

    • UI线程不会妨碍工作线程的进度 .

    • 工作线程无法控制UI线程花费更新的时间 .

    • UI和工作线程执行操作的时间间隔可以保持独立 .

    • 工作线程不能超出UI线程的消息泵 .

    • UI线程决定UI更新的时间和频率 .

  • 14

    您必须使用invoke和delegate

    private delegate void MyLabelDelegate();
    label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });
    
  • 5

    simplest 方式是传递给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 阻止执行直到它完成 - 这是同步代码 . 这个问题并不是关于异步代码的问题,但是当你想要了解它时,有很多关于编写异步代码的content on Stack Overflow .

  • 12

    出于许多目的,它就像这样简单:

    public delegate void serviceGUIDelegate();
    private void updateGUI()
    {
      this.Invoke(new serviceGUIDelegate(serviceGUI));
    }
    

    “serviceGUI()”是表单(this)中的GUI级方法,可以根据需要更改任意数量的控件 . 从另一个线程调用“updateGUI()” . 可以添加参数来传递值,或者(可能更快)使用带有锁定的类范围变量,如果有可能在访问它们的线程之间发生冲突而导致不稳定 . 使用如果非GUI线程是时间关键的,那么BeginInvoke而不是Invoke(记住Brian Gideon的警告) .

  • 6

    Salvete!搜索了这个问题后,我发现FrankG和Oregon Ghost的答案对我来说是最简单的 . 现在我Visual Basic中的代码并通过转换器运行此代码段;所以我不确定结果如何 .

    我有一个名为 form_Diagnostics, 的对话框,其中有一个名为 updateDiagWindow, 的richtext框,我将其用作一种日志记录显示 . 我需要能够从所有线程更新其文本 . 额外的线条允许窗口自动滚动到最新的线条 .

    所以,我现在可以用你认为无需任何线程的方式在整个程序的任何地方用一行更新显示:

    form_Diagnostics.updateDiagWindow(whatmessage);
    

    Main Code (put this inside of your form's class code):

    #region "---------Update Diag Window Text------------------------------------"
    // This sub allows the diag window to be updated by all threads
    public void updateDiagWindow(string whatmessage)
    {
        var _with1 = diagwindow;
        if (_with1.InvokeRequired) {
            _with1.Invoke(new UpdateDiagDelegate(UpdateDiag), whatmessage);
        } else {
            UpdateDiag(whatmessage);
        }
    }
    // This next line makes the private UpdateDiagWindow available to all threads
    private delegate void UpdateDiagDelegate(string whatmessage);
    private void UpdateDiag(string whatmessage)
    {
        var _with2 = diagwindow;
        _with2.appendtext(whatmessage);
        _with2.SelectionStart = _with2.Text.Length;
        _with2.ScrollToCaret();
    }
    #endregion
    
  • 19

    绝大多数答案使用 Control.Invoke ,这是一个race condition waiting to happen . 例如,考虑接受的答案:

    string newText = "abc"; // running on worker thread
    this.Invoke((MethodInvoker)delegate { 
        someLabel.Text = newText; // runs on UI thread
    });
    

    如果用户在调用 this.Invoke 之前关闭表单(记住, thisForm 对象),则可能会触发 ObjectDisposedException .

    解决方案是使用 SynchronizationContext ,特别是 SynchronizationContext.Current 作为hamilton.danielb建议(其他答案依赖于特定的 SynchronizationContext 实现,这是完全没有必要的) . 我会略微修改他的代码以使用 SynchronizationContext.Post 而不是 SynchronizationContext.Send (因为通常不需要工作线程等待):

    public partial class MyForm : Form
    {
        private readonly SynchronizationContext _context;
        public MyForm()
        {
            _context = SynchronizationContext.Current
            ...
        }
    
        private MethodOnOtherThread()
        {
             ...
             _context.Post(status => someLabel.Text = newText,null);
        }
    }
    

    请注意,在.NET 4.0及更高版本中,您应该使用异步操作的任务 . 有关基于任务的等效方法,请参阅n-san's answer(使用 TaskScheduler.FromCurrentSynchronizationContext ) .

    最后,在.NET 4.5及更高版本中,您还可以使用 Progress<T> (基本上在创建时捕获 SynchronizationContext.Current ),如Ryszard Dżegan's所示,用于长时间运行的操作需要在仍然工作的情况下运行UI代码的情况 .

  • 19

    这个类似于上面使用.NET Framework 3.0的解决方案,但它解决了 compile-time safety support 的问题 .

    public  static class ControlExtension
    {
        delegate void SetPropertyValueHandler<TResult>(Control souce, Expression<Func<Control, TResult>> selector, TResult value);
    
        public static void SetPropertyValue<TResult>(this Control source, Expression<Func<Control, TResult>> selector, TResult value)
        {
            if (source.InvokeRequired)
            {
                var del = new SetPropertyValueHandler<TResult>(SetPropertyValue);
                source.Invoke(del, new object[]{ source, selector, value});
            }
            else
            {
                var propInfo = ((MemberExpression)selector.Body).Member as PropertyInfo;
                propInfo.SetValue(source, value, null);
            }
        }
    }
    

    使用:

    this.lblTimeDisplay.SetPropertyValue(a => a.Text, "some string");
    this.lblTimeDisplay.SetPropertyValue(a => a.Visible, false);
    

    如果用户传递了错误的数据类型,编译器将失败 .

    this.lblTimeDisplay.SetPropertyValue(a => a.Visible, "sometext");
    
  • 42

    .NET 4的Marc Gravell's simplest solution的变化:

    control.Invoke((MethodInvoker) (() => control.Text = "new text"));
    

    或者使用Action委托代替:

    control.Invoke(new Action(() => control.Text = "new text"));
    

    请看这里比较两者:MethodInvoker vs Action for Control.BeginInvoke

  • 31

    尝试使用此刷新标签

    public static class ExtensionMethods
    {
        private static Action EmptyDelegate = delegate() { };
    
        public static void Refresh(this UIElement uiElement)
        {
            uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
        }
    }
    
  • 18

    我的版本是插入 one line 的递归"mantra":

    没有参数:

    void Aaaaaaa()
        {
            if (InvokeRequired) { Invoke(new Action(Aaaaaaa)); return; } //1 line of mantra
    
            // Your code!
        }
    

    对于具有参数的函数:

    void Bbb(int x, string text)
        {
            if (InvokeRequired) { Invoke(new Action<int, string>(Bbb), new[] { x, text }); return; }
            // Your code!
        }
    

    THAT is IT .


    Some argumentation :通常,在一行中的 if () 语句之后放置{}会导致代码可读性不佳 . 但在这种情况下,它是常规的"mantra" . 如果此方法在项目中保持一致,则不会破坏代码可读性 . 它可以避免您的代码乱扔垃圾(一行代码而不是五行) .

    如你所见 if(InvokeRequired) {something long} ,你只知道"this function is safe to call from another thread" .

  • 18

    创建一个类变量:

    SynchronizationContext _context;
    

    在创建UI的构造函数中设置它:

    var _context = SynchronizationContext.Current;
    

    如果要更新标签:

    _context.Send(status =>{
        // UPDATE LABEL
    }, null);
    
  • 26

    我只是阅读了答案,这似乎是一个非常热门的话题 . 我目前正在使用.NET 3.5 SP1和Windows Forms .

    在之前的答案中大量描述的众所周知的公式使用 InvokeRequired 属性涵盖了大多数情况,但不包括整个池 .

    如果尚未创建 Handle 怎么办?

    InvokeRequired 属性,如here (Control.InvokeRequired Property reference to MSDN)所述,如果调用是从不是GUI线程的线程调用,则返回true;如果调用是从GUI线程调用,则返回false,或者 Handle 尚未创建 .

    如果您希望另一个线程显示和更新模式表单,则可能会遇到异常 . 因为您希望以模态方式显示该表单,所以您可以执行以下操作:

    private MyForm _gui;
    
    public void StartToDoThings()
    {
        _gui = new MyForm();
        Thread thread = new Thread(SomeDelegate);
        thread.Start();
        _gui.ShowDialog();
    }
    

    委托可以更新GUI上的Label:

    private void SomeDelegate()
    {
        // Operations that can take a variable amount of time, even no time
        //... then you update the GUI
        if(_gui.InvokeRequired)
            _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
        else
            _gui.Label1.Text = "Done!";
    }
    

    如果标签更新"take less time"之前的操作(读取并将其解释为简化),则可能导致 InvalidOperationException ,而不是GUI线程创建 FormHandle 所花费的时间 . 这发生在 ShowDialog() 方法中 .

    您还应该像这样检查 Handle

    private void SomeDelegate()
    {
        // Operations that can take a variable amount of time, even no time
        //... then you update the GUI
        if(_gui.IsHandleCreated)  //  <---- ADDED
            if(_gui.InvokeRequired)
                _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
            else
                _gui.Label1.Text = "Done!";
    }
    

    如果尚未创建 Handle ,您可以处理要执行的操作:您可以忽略GUI更新(如上面的代码所示)或者您可以等待(风险更大) . 这应该回答这个问题 .

    可选的东西:我个人想出了以下代码:

    public class ThreadSafeGuiCommand
    {
      private const int SLEEPING_STEP = 100;
      private readonly int _totalTimeout;
      private int _timeout;
    
      public ThreadSafeGuiCommand(int totalTimeout)
      {
        _totalTimeout = totalTimeout;
      }
    
      public void Execute(Form form, Action guiCommand)
      {
        _timeout = _totalTimeout;
        while (!form.IsHandleCreated)
        {
          if (_timeout <= 0) return;
    
          Thread.Sleep(SLEEPING_STEP);
          _timeout -= SLEEPING_STEP;
        }
    
        if (form.InvokeRequired)
          form.Invoke(guiCommand);
        else
          guiCommand();
      }
    }
    

    我提供的表单由另一个具有此 ThreadSafeGuiCommand 实例的线程更新,我定义了更新GUI(在我的表单中)的方法,如下所示:

    public void SetLabeTextTo(string value)
    {
      _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
    }
    

    通过这种方式,我非常确定我会更新任何线程进行调用的GUI,可选择等待明确定义的时间(超时) .

  • 972

    您必须确保更新发生在正确的线程上; UI线程 .

    为此,您必须调用事件处理程序而不是直接调用它 .

    您可以通过以下方式举办活动来做到这一点:

    (代码在这里打印出来,所以我没有检查正确的语法等,但它应该让你去 . )

    if( MyEvent != null )
    {
       Delegate[] eventHandlers = MyEvent.GetInvocationList();
    
       foreach( Delegate d in eventHandlers )
       {
          // Check whether the target of the delegate implements 
          // ISynchronizeInvoke (Winforms controls do), and see
          // if a context-switch is required.
          ISynchronizeInvoke target = d.Target as ISynchronizeInvoke;
    
          if( target != null && target.InvokeRequired )
          {
             target.Invoke (d, ... );
          }
          else
          {
              d.DynamicInvoke ( ... );
          }
       }
    }
    

    请注意,上面的代码不适用于WPF项目,因为WPF控件不实现 ISynchronizeInvoke 接口 .

    为了确保上面的代码适用于Windows窗体和WPF以及所有其他平台,您可以查看 AsyncOperationAsyncOperationManagerSynchronizationContext 类 .

    为了以这种方式轻松地引发事件,我创建了一个扩展方法,它允许我通过调用简化来简化事件:

    MyEvent.Raise(this, EventArgs.Empty);
    

    当然,您也可以使用BackGroundWorker类,它将为您抽象出这个问题 .

  • 60

    您需要在GUI线程上调用该方法 . 您可以通过调用Control.Invoke来实现 .

    例如:

    delegate void UpdateLabelDelegate (string message);
    
    void UpdateLabel (string message)
    {
        if (InvokeRequired)
        {
             Invoke (new UpdateLabelDelegate (UpdateLabel), message);
             return;
        }
    
        MyLabelControl.Text = message;
    }
    
  • 24

    例如,访问当前线程以外的控件:

    Speed_Threshold = 30;
    textOutput.Invoke(new EventHandler(delegate
    {
        lblThreshold.Text = Speed_Threshold.ToString();
    }));
    

    那里 lblThreshold 是一个Label,而 Speed_Threshold 是一个全局变量 .

  • 20

    处理长期工作

    .NET 4.5 and C# 5.0开始,你应该使用Task-based Asynchronous Pattern (TAP)async - await关键字in all areas(包括GUI):

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

    而不是Asynchronous Programming Model (APM)Event-based Asynchronous Pattern (EAP)(后者包括BackgroundWorker Class) .

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

    • 事件处理程序的异步实现(是的,就是全部):
    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";
    }
    
    • 通知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());
            }
        }
    }
    

    请注意以下事项:

    有关更详细的示例,请参阅:The Future of C#: Good things come to those who 'await' by Joseph Albahari .

    另见UI Threading Model概念 .

    处理异常

    以下代码段是如何处理异常和切换按钮的 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...");
        }
    }
    
  • 20

    简单的解决方案是使用 Control.Invoke .

    void DoSomething()
    {
        if (InvokeRequired) {
            Invoke(new MethodInvoker(updateGUI));
        } else {
            // Do Something
            updateGUI();
        }
    }
    
    void updateGUI() {
        // update gui here
    }
    
  • 14

    这在Ian Kemp解决方案的C#3.0变体中:

    public static void SetPropertyInGuiThread<C,V>(this C control, Expression<Func<C, V>> property, V value) where C : Control
    {
        var memberExpression = property.Body as MemberExpression;
        if (memberExpression == null)
            throw new ArgumentException("The 'property' expression must specify a property on the control.");
    
        var propertyInfo = memberExpression.Member as PropertyInfo;
        if (propertyInfo == null)
            throw new ArgumentException("The 'property' expression must specify a property on the control.");
    
        if (control.InvokeRequired)
            control.Invoke(
                (Action<C, Expression<Func<C, V>>, V>)SetPropertyInGuiThread,
                new object[] { control, property, value }
            );
        else
            propertyInfo.SetValue(control, value, null);
    }
    

    你这样称呼它:

    myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
    
    • 它为"as MemberExpression"的结果添加了空值检查 .

    • 它改善了静态类型安全性 .

    否则,原始是一个非常好的解决方案 .

  • 13

    我想添加警告,因为我注意到一些简单的解决方案省略了 InvokeRequired 检查 .

    我注意到如果您的代码执行 before the window handle of the control has been created (例如在显示表单之前), Invoke 会抛出异常 . 所以我建议在调用 InvokeBeginInvoke 之前始终检查 InvokeRequired .

  • 12

    当我遇到同样的问题时,我向谷歌寻求帮助,但不是给我一个简单的解决方案,而是通过给出 MethodInvoker 和blah blah blah的例子让我更加困惑 . 所以我决定自己解决它 . 这是我的解决方案:

    像这样委托:

    Public delegate void LabelDelegate(string s);
    
    void Updatelabel(string text)
    {
       if (label.InvokeRequired)
       {
           LabelDelegate LDEL = new LabelDelegate(Updatelabel);
           label.Invoke(LDEL, text);
       }
       else
           label.Text = text
    }
    

    您可以在这样的新线程中调用此函数

    Thread th = new Thread(() => Updatelabel("Hello World"));
    th.start();
    

    不要与 Thread(() => .....) 混淆 . 我在线程上工作时使用匿名函数或lambda表达式 . 要减少代码行,你也可以使用 ThreadStart(..) 方法,我不应该在这里解释 .

  • 7

    对于.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);
    

    UPDATE 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 ||
          !@this.GetType().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版本好很多 .

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

相关问题