WPF:如何在工作线程中更新窗体的UI元素(Dispatcher机制)

这是一个普遍的问题:如果我们再程序中使用了多线程技术,而工作线程(后台线程)如果需要更新界面上的元素(例如进度条等),就会有一个线程安全性问题,因为进度条是由主线程创建出来的。

关于这一点,大致上看,WPF的机制与Windows Forms是没有差别的。我们在Windows Forms中需要按照下面的方式更新窗体元素。

image

using System;
using System.Windows.Forms;
using System.Threading;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //错误的写法:直接更新
            new Thread(() =>
            {
                progressBar1.Value = 10;
            }).Start();


        }

        private void button2_Click(object sender, EventArgs e)
        {
            //正确的写法,通知主线程更新
            new Thread(()=>{
                this.Invoke(new Action(() =>
                {
                    progressBar1.Value = 10;
                }));
            }).Start();
        }
    }
}

第一种是错误的写法,它将导致一个运行时错误

image

了解了这些,我们来看看WPF是怎么做的?

image

点击第一个按钮的话,我们同样会收到一个错误

image

点击第二个按钮,则工作正常

image

我们看看代码有什么区别

using System;
using System.Windows;
using System.Threading;

namespace WpfApplication1
{
    /// <summary>
    /// Window1.xaml 的交互逻辑
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            //错误的写法:直接更新
            new Thread(() =>
            {
                progressBar1.Value = 20;
            }).Start();

        }

        private void button2_Click(object sender, RoutedEventArgs e)
        {
            //正确的写法:通知主线程去完成更新
            new Thread(()=>{
                this.Dispatcher.Invoke(new Action(()=>{
                    progressBar1.Value=20;
                }));
            }).Start();
        }
    }
}

请注意,Window类并没有Invoke方法,这是与Form不一样的。取而代之的是,我们需要通过访问Window.Dispatcher属性,然后调用Invoke方法 。仅此而已

好吧,那么到底什么是Dispatcher呢?从字面上来说,它是所谓的接线员,或者调度员的意思。这说明什么呢?每个线程都有一个唯一的调度员,我们在代码中所做的工作其实是向这个调度员发出指令,然后它再帮我们做。这样理解就对了。

我们的窗体是在主线程创建出来的,里面的控件自然也是如此。我们之前解释过WPF的应用程序也是单线程模型的(STAThread),所以整个应用程序里面会有一个默认的Dispatcher,它负责调度主线程的工作。

其实,如果大家真有兴趣,可以看看Application.Run这个方法的实现,就能理解上面所说的话了

[SecurityCritical]
internal int RunInternal(Window window)
{
    base.VerifyAccess();
    EventTrace.NormalTraceEvent(EventTraceGuidId.APPRUNGUID, 0);
    if (this._appIsShutdown)
    {
        throw new InvalidOperationException(SR.Get("CannotCallRunMultipleTimes", new object[] { base.GetType().FullName }));
    }
    if (window != null)
    {
        if (!window.CheckAccess())
        {
            throw new ArgumentException(SR.Get("WindowPassedShouldBeOnApplicationThread", new object[] { window.GetType().FullName, base.GetType().FullName }));
        }
        if (!this.WindowsInternal.HasItem(window))
        {
            this.WindowsInternal.Add(window);
        }
        if (this.MainWindow == null)
        {
            this.MainWindow = window;
        }
        if (window.Visibility != Visibility.Visible)
        {
            base.Dispatcher.BeginInvoke(DispatcherPriority.Send, delegate (object obj) {
                (obj as Window).Show();
                return null;
            }, window);
        }
    }
    this.EnsureHwndSource();
    if (!BrowserInteropHelper.IsBrowserHosted)
    {
        this.RunDispatcher(null);
    }
    return this._exitCode;
}

 
[SecurityCritical]
private object RunDispatcher(object ignore)
{
    Invariant.Assert(!this._ownDispatcherStarted);
    this._ownDispatcherStarted = true;
    Dispatcher.Run();
    return null;
}

 
那么,为什么在Window中可以调用到Dispatcher属性呢?或者说在那些对象上面可以采用这种机制呢?大致上说,几乎所有的控件都可以,因为他们的基类一般都可以追溯到一个DispatcherObject

image

image

image

从这个角度来说,如果我们自己编写一个用于WPF的控件,那么也是需要按照这样的层次结构去继承的。这样在控件内部,才可以通过this.Dispatcher来更新一些界面元素了。

那么,如果我们是一个类库项目,就是一个标准的类型,它也需要更新到主线程中的一些元素怎么办?

此时可以通过Application.Current.Dispacther来实现,例如下面的例子

using System;
using System.Windows;

namespace WpfApplication1
{
    public class WindowHelper
    {
        public static void SomeMethod() {
            Application.Current.Dispatcher.Invoke(new Action(() => {
                Application.Current.MainWindow.Title = "我修改过的窗体标题";
            }));
        }
    }
}

其实,要认真讲的话,Application的这个Dispatcher与我们刚才用到的Window的Dispatcher是同一个对象。也就是说,每个线程只有一个。

而其实,使用Window的Dispatcher,与使用Button的Dispatcher也没有区别的

最后要说一点的是,Dispatcher除了Invoke方法之外,还有BeginInvoke方法。区别在于后者是异步执行的。如何使用异步的机制呢?

using System;
using System.Windows;
using System.Windows.Threading;

namespace WpfApplication1
{
    public class WindowHelper
    {
        public static void SomeMethod() {
            //Application.Current.Dispatcher.Invoke(new Action(() => {
            //    Application.Current.MainWindow.Title = "我修改过的窗体标题";
            //}));


            var task = Application.Current.Dispatcher.BeginInvoke(new Action(() => { Application.Current.MainWindow.Title = "我修改过的窗体标题"; }));
            task.Completed += new EventHandler(task_Completed);
            
        }

        static void task_Completed(object sender, EventArgs e)
        {
            MessageBox.Show("任务已经完成");
        }
    }
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在WPF,界面元素是通过主线程更新的,因此在使用线程更新UI时必须将操作转移到主线程。这是因为WPF是基于Windows消息循环的,任何对UI更新必须在主线程上进行。 为了在后台线程更新UI,可以使用Dispatcher类的Invoke或BeginInvoke方法。这两个方法允许我们将任务调度到UI线程上执行。 Invoke方法会阻塞当前线程,直到UI线程执行完毕,而BeginInvoke方法则是异步执行,不会阻塞当前线程。使用这两个方法,可以将UI更新代码包装在委托,然后将其传递给Dispatcher对象,以便在UI线程上执行。 例如,以下是一个在后台线程更新UI的示例: ```csharp using System; using System.Threading; using System.Windows; using System.Windows.Controls; namespace WPFThreadUpdateUI { public partial class MainWindow : Window { private Thread workerThread; private TextBox textBox; public MainWindow() { InitializeComponent(); } private void StartButton_Click(object sender, RoutedEventArgs e) { workerThread = new Thread(WorkerThreadMethod); workerThread.Start(); } private void WorkerThreadMethod() { // 后台线程更新UI Dispatcher.Invoke(() => { textBox = new TextBox(); textBox.Text = "UI更新成功"; mainGrid.Children.Add(textBox); }); } } } ``` 在上述示例,当点击"Start"按钮时,会创建一个后台线程并执行WorkerThreadMethod方法。在方法,通过Dispatcher.Invoke方法将UI更新的代码封装在委托,然后传递给Dispatcher对象以在UI线程上执行。这样就可以在后台线程更新UI。 需要注意的是,当使用Dispatcher.Invoke或BeginInvoke方法更新UI时,一定要确保在访问UI元素之前检查调用线程UI线程之间的关系,以避免线程冲突和UI更新问题。 ### 回答2: 在WPF(Windows Presentation Foundation)UI更新通常需要在UI线程上执行。这是因为UI元素和控件只能在其创建线程上进行更新,如果在其他线程上尝试更新UI,将会引发异常。 要在线程更新UI,可以使用Dispatcher对象的Invoke或BeginInvoke方法。Dispatcher对象提供了对UI线程的访问,这样就可以在其他线程UI线程发送消息并更新UI。 示例代码如下: ```csharp // 在其他线程更新UI private void UpdateUI() { Dispatcher.Invoke(() => { // 在UI线程上执行更新操作 // 这里可以更新UI元素或控件的属性、内容等 }); } // 启动一个新线程,并在其更新UI private void StartUpdateThread() { Thread updateThread = new Thread(() => { // 执行其他任务... // 更新UI UpdateUI(); // 执行其他任务... }); updateThread.Start(); } ``` 在上述示例,我们首先创建了一个新的线程`updateThread`,然后在其调用`UpdateUI`方法,它通过`Dispatcher.Invoke()`方法将UI更新操作发送到UI线程。由于采用了Invoke方法,因此更新操作会等待UI线程完成操作后才会继续执行。 通过调用`StartUpdateThread`方法,我们可以在其他线程启动一个新的线程更新UI。 总结而言,通过使用Dispatcher对象的Invoke或BeginInvoke方法,可以在WPF的其他线程更新UI。这确保了UI更新操作的线程安全性,并避免了潜在的异常情况。 ### 回答3: 在WPF,要更新UI,我们必须确保所有对UI元素的更改都发生在主线程上,这是因为WPFUI元素线程的单元,只能由创建它的线程进行访问和修改。如果我们试图在非主线程更新UI,可能会导致异常或者无法正确更新UI。 为了在线程之间更新UI,我们可以使用Dispatcher对象。DispatcherWPF应用程序的一个功能强大的工具,它允许我们在主线程上执行操作,包括更新UI元素。 我们可以通过调用Dispatcher对象的Invoke或BeginInvoke方法来跨线程更新UI。Invoke方法是同步的,会阻塞非主线程直到UI更新完成,而BeginInvoke方法是异步的,允许非主线程继续执行而无需等待UI更新完成。 例如,我们可以使用以下代码在非主线程更新UI元素的内容: Dispatcher.Invoke(() => { // 在主线程更新UI的代码 label1.Content = "更新后的内容"; }); 通过这种方式,我们可以安全地在WPF的任何线程更新UI,而无需担心线程冲突或异常。然而,需要注意的是,频繁地在非主线程更新UI可能会导致性能下降,因此我们应该尽量减少跨线程更新UI的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值