C# WPF跨线程更新绑定元素的数据集

文章目录

  • 写在前面
  • 问题描述
  • 归纳
    • BeginInvoke &Invoke
      • Control.BeginInvoke
      • Control.Invoke
      • Control.InvokeRequired
      • Dispatcher
      • Dispatcher.BeginInvoke
      • Dispatcher.Invoke
  • 问题再现
    • Solution A
  • 总结

写在前面

  • 一点共识:不能从其他线程修改UI界面的属性,比如不能通过其他线程修改Button的Content等等;
  • WPF中存在的一种可能:通过数据绑定,可以很大程度上避免直接修改UI界面的元素属性,但仍存在其他线程需要访问修改被绑定的数据集合的问题的可能,本文即对这一问题做解答。

问题描述

        public override void SomeLogicMethod()
        {
            if (ElementViewDataContext != null)
            {
                var dt = (DataDeliverViewModel)ElementViewDataContext;
                dt.brefreshed = false;
                MainWindowViewModel.GetInstance().gMainWindow.Dispatcher.BeginInvoke((Action)(() =>
                {
                    dt.init();//涉及到对UI界面元素集合的修改
                }));

                Log.Logger.Debug("等待数据刷新完成...");
                while (!dt.brefreshed)
                {
                    Thread.Sleep(50);
                }
                Log.Logger.Debug("数据刷新完成!");
                dt.brefreshed = false;
            }
            var b = GetSentValues();
            SendValues(b);
            IsExecuteDone = true;
        }

这里由于涉及到UI界面的修改,所以考虑使用Dispatcher.BeginInvoke,我们简单回顾一下Dispatcher.BeginInvokeDispatcher.Invoke

归纳

BeginInvoke &Invoke

众所周知,BeginInvoke为异步操作(asyn),Invoke为同步操作。不知道的是,竟然存在两种不同的BeginInvokeInvoke组合!!来看这篇文章:why there is no EndInvoke in Cross thread UI component call ?
在这里插入图片描述
Control.BeginInvokeDispatcher.BeginInvoke是不同的。

Control.BeginInvoke

MSDN描述:

Executes a delegate asynchronously on the thread that the control’s underlying handle was created on.

在创建该控件的线程上执行异步委托。这样就解决了跨线程更新的问题。既然异步,那么必然有异步等待的问题,C# WPF跨线程更新绑定元素的数据集_第1张图片
我们可以通过调用EndInvoke来确保跨线程执行完毕,如果必要的话。(大多数情况下是没必要的,让UI界面更新的命令下发就可以了,什么时候更新我们并不关心,但是若是涉及到一些数据操作,而我们又需要这部分数据,那么就加等待。以前我的做法是异步+while指定条件手动实现异步等待,现在可以使用EndInvoke,效率会比自己的阻塞等待要高)。
这里是一个EndInvoke的例子。

Control.Invoke

尽管文章并没有提到Invoke是同步的。。
写到这里,有一个属性是绕不开的:

Control.InvokeRequired

主要讲了这几个意思:

  1. InvokeRequired可以帮助我们向上寻找窗口句柄以执行委托,如果没有找到,则返回false;
  2. 返回false有两个含义:可能是当前并不需要invoke,也就是说当前上下文就是创建控件的线程,可以直接操作,无须委托;还有一层意思就是1中提到的,没找到,控件在另一个线程上创建了,但是控件的句柄还没有创建好。
  3. 如果是控件的句柄没有创建好,那就不能随便调用该控件的属性方法和事件,这样会造成控件的句柄可能是后台线程(与UI线程)相对创建的,使得控件被孤立,无法进入消息队列响应,进一步导致应用不稳定。
  4. 为了避免这种情况的发生,可以在InvokeRequired返回false之后检查IsHandleCreated的值,如果控件句柄未创建,则在调用BeginInvoke和Invoke之前必须一直等待。这种情况(控件句柄未创建)仅发生在后台线程创建在应用的主窗体构造函数中,在form显示之前或者Application.Run调用之前。
  5. 一种解决办法是在启动后台线程之前一直等待窗体句柄创建完成;
  6. 还有一种更佳的办法就是使用SynchronizationContext返回的同步上下文(SynchronizationContext),而不是跨线程分配的控件。本文要提到的方法,就是应用了SynchronizationContext
    以上Control部分,主要是针对WinForm,接下来的要讲的就是WPF的应用了。

Dispatcher

有以下几点需要注意的:

  1. Dispatcher为某一线程提供工作项队列管理的服务;
  2. C# WPF跨线程更新绑定元素的数据集_第2张图片
    一个线程仅有唯一对应的Dispatcher,也就是说用另一个控件的dispatcher调用当前操作控件的UI是没有问题的
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Thread thread = new Thread(DoWork);
            thread.Start();
        }

        private void DoWork()
        {
            bt1.Dispatcher.BeginInvoke((Action)(() =>
            {
                bt2.Content = "New Content";//这里b1,b2都ok
            }));
        }
    }

Dispatcher.BeginInvoke(此处仅简单介绍,后续会出文章详解)

C# WPF跨线程更新绑定元素的数据集_第3张图片

Dispatcher.Invoke

问题再现

        public override void SomeLogicMethod()
        {
            if (ElementViewDataContext != null)
            {
                var dt = (DataDeliverViewModel)ElementViewDataContext;
                dt.brefreshed = false;
                // MainWindowViewModel.GetInstance().gMainWindow.Dispatcher.BeginInvoke((Action)(() =>
                // {
                    dt.init();//涉及到对UI界面元素集合的修改
                // }));

                Log.Logger.Debug("等待数据刷新完成...");
                // while (!dt.brefreshed)
                // {
                    // Thread.Sleep(50);
                // }
                Log.Logger.Debug("数据刷新完成!");
                dt.brefreshed = false;
            }
            var b = GetSentValues();
            SendValues(b);
            IsExecuteDone = true;
        }

如果不用Dispatcher,就会报错:

该类型的 CollectionView 不支持从调度程序线程以外的线程对其 SourceCollection 进行的更改。

我们使用另一种方法解决:

```csharp
        public override void SomeLogicMethod()
        {
            if (ElementViewDataContext != null)
            {
                var dt = (DataDeliverViewModel)ElementViewDataContext;
                dt.brefreshed = false;
				
				
			ThreadPool.QueueUserWorkItem(delegate
            {
                SynchronizationContext.SetSynchronizationContext(new
                    DispatcherSynchronizationContext(System.Windows.Application.Current.Dispatcher));
                SynchronizationContext.Current.Post(pl =>
                {
                    dt.init();//涉及到对UI界面元素集合的修改
                }, null);
            });
                // MainWindowViewModel.GetInstance().gMainWindow.Dispatcher.BeginInvoke((Action)(() =>
                // {
                //    dt.init();//涉及到对UI界面元素集合的修改
                // }));

                Log.Logger.Debug("等待数据刷新完成...");
                // while (!dt.brefreshed)
                // {
                    // Thread.Sleep(50);
                // }
                Log.Logger.Debug("数据刷新完成!");
                dt.brefreshed = false;
            }
            var b = GetSentValues();
            SendValues(b);
            IsExecuteDone = true;
        }

这时会出现结果没运行结束就跳出的问题,我们需要做一个等待操作,参照这里给出最终解决方案:

Solution A

        public override void SomeLogicMethod()
        {
			var resetEvent = new ManualResetEvent(false);
            if (ElementViewDataContext != null)
            {
                var dt = (DataDeliverViewModel)ElementViewDataContext;
                dt.brefreshed = false;
				
				
			ThreadPool.QueueUserWorkItem(delegate
            {
                SynchronizationContext.SetSynchronizationContext(new
                    DispatcherSynchronizationContext(System.Windows.Application.Current.Dispatcher));
                SynchronizationContext.Current.Post(pl =>
                {
                    dt.init();//涉及到对UI界面元素集合的修改
					resetEvent.Set();
                }, null);
            });
			resetEvent.WaitOne();
                // MainWindowViewModel.GetInstance().gMainWindow.Dispatcher.BeginInvoke((Action)(() =>
                // {
                //    dt.init();//涉及到对UI界面元素集合的修改
                // }));

                Log.Logger.Debug("等待数据刷新完成...");
                // while (!dt.brefreshed)
                // {
                    // Thread.Sleep(50);
                // }
                Log.Logger.Debug("数据刷新完成!");
                dt.brefreshed = false;
            }
            var b = GetSentValues();
            SendValues(b);
            IsExecuteDone = true;
        }

总结

  • 查资料的时候偶尔读到一篇博文,文章中提到,BeginInvoke已经过时了。。。哎,我这才学呢,技术更新太快了啊~
  • 一个想法,Task.Run()的异步写法,可以避免将所有涉及到异步的函数调用都声明为async,但是需要记住的是,Task.Run()新开了一个线程跑,不会等待返回值的,这里的await仅仅是在另一个线程里的等待:
                Task.Run(async () =>
                {
                    await MainWindowViewModel.GetInstance().gMainWindow.Dispatcher.BeginInvoke((Action)(() =>
                    {
                        dt.init();//涉及到对UI界面元素集合的修改
                    }));

                    Log.Logger.Debug("等待数据刷新完成...");
                    var b = GetSentValues();
                    SendValues(b);
                    IsExecuteDone = true;
                }
                );
  • 永远不要让UI线程做太多事。

你可能感兴趣的:(C#)