如何使用数据绑定进行处理并保持GUI刷新?

问题的历史

这是我之前的问题的延续

如何启动线程以保持GUI刷新?

但是,由于Jon对这个问题有了新的认识,我将不得不完全重写原始问题,这会使该主题难以理解。 所以,新的,非常具体的问题。

问题

两件:

  • CPU饥饿的重量级处理作为库(后端)
  • 带有数据绑定的WPF GUI,用作处理的监视器(前端)

当前情况 – 库发送了很多关于数据更改的通知,尽管它在自己的线程中工作,它完全阻塞了WPF数据绑定机制,结果不仅监视数据不起作用(它没有刷新),但整个GUI被冻结在处理数据时。

目标 – 精心设计,抛光的方式保持GUI最新 – 我不是说它应该立即显示数据(它甚至可以跳过一些更改),但它不能在进行计算时冻结。

这是简化示例,但它显示了问题。

XAML部分:

    

C#部分(请注意这是一个代码,但它有两个部分):

 public partial class MainWindow : Window,INotifyPropertyChanged { // GUI part public MainWindow() { InitializeComponent(); DataContext = this; } private void Button_Click(object sender, RoutedEventArgs e) { var thread = new Thread(doProcessing); thread.IsBackground = true; thread.Start(); } // this is non-GUI part -- do not mess with GUI here public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(string property_name) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(property_name)); } long counter; public long Counter { get { return counter; } set { if (counter != value) { counter = value; OnPropertyChanged("Counter"); } } } void doProcessing() { var tmp = 10000.0; for (Counter = 0; Counter < 10000000; ++Counter) { if (Counter % 2 == 0) tmp = Math.Sqrt(tmp); else tmp = Math.Pow(tmp, 2.0); } } } 

已知的解决方法

(请不要将它们作为答案重新发布)

我根据我喜欢的解决方法排序列表,即它需要多少工作,限制等等。

  1. 这是我的,它是丑陋的,但简单的杀死 – 在发送通知冻结线程之前 – Thread.Sleep(1) – 让潜在的接收器“呼吸” – 它工作,它是简约的,它是虽然很难看,但即使没有GUI,它总是会减慢计算速度
  2. 基于Jon的想法 – 放弃数据绑定COMPLETELY (一个带有数据绑定的小部件足以进行干扰),而是不时地检查数据并手动更新GUI – 好吧,我没有学习WPF只是为了放弃现在用它;-)
  3. 托马斯的想法 – 在库和前端之间插入代理,它将接收来自库的所有通知,并将其中的一些传递给WPF,例如每秒 – 缺点是你必须复制发送通知的所有对象
  4. 基于Jon的想法 – 将GUI调度程序传递给库并使用它来发送通知 – 为什么它很难看? 因为它可能根本就没有GUI

我目前的“解决方案”是在主循环中添加Sleep。 减速可以忽略不计,但它足以刷新WPF(所以它甚至比每次通知之前的睡眠更好)。

对于真正的解决方案,我都是耳朵,而不是一些技巧。

备注

备注放弃数据绑定 – 对我而言,它的设计被打破,在WPF中你有单一的通信渠道,你不能直接绑定到变更的来源。 数据绑定根据名称过滤源(字符串!)。 即使您使用一些聪明的结构来保留所有字符串,这也需要一些计算。

编辑: 关于抽象的评论 – 叫我老计时器,但我开始学习计算机说服,计算机应该帮助人类。 重复性任务是计算机领域,而不是人类。 无论你怎么称呼它 – MVVM,抽象,接口,单inheritance,如果你一遍又一遍地编写相同的代码,并且你没有办法自动化你所做的事情,你就使用了破坏的工具。 所以例如lambdas很棒(对我来说工作量少)但单一inheritance不是(对我来说更多的工作),数据绑定(作为一个想法)很棒(工作量少)但是对于我绑定的每个库需要代理层是破碎的想法,因为它需要大量的工作。

在我的WPF应用程序中,我不直接将属性更改从模型发送到GUI。 它总是通过代理(ViewModel)。

属性更改事件放在队列中,该队列从计时器上的GUI线程读取。

不明白这可以做多么多的工作。 您只需要另一个用于模型的propertychange事件的侦听器。

使用“Model”属性创建一个ViewModel类,该属性是您当前的datacontext。 将数据绑定更改为“Model.Property”并添加一些代码以挂接事件。

它看起来像这样:

 public MyModel Model { get; private set; } public MyViewModel() { Model = new MyModel(); Model.PropertyChanged += (s,e) => SomethingChangedInModel(e.PropertyName); } private HashSet _propertyChanges = new HashSet(); public void SomethingChangedInModel(string propertyName) { lock (_propertyChanges) { if (_propertyChanges.Count == 0) _timer.Start(); _propertyChanges.Add(propertyName ?? ""); } } // this is connected to the DispatherTimer private void TimerCallback(object sender, EventArgs e) { List changes = null; lock (_propertyChanges) { _Timer.Stop(); // doing this in callback is safe and disables timer if (!_propertyChanges.Contain("")) changes = new List(_propertyChanges); _propertyChanges.Clear(); } if (changes == null) OnPropertyChange(null); else foreach (string property in changes) OnPropertyChanged(property); } 

这本身并不是WPF问题。 如果您有一个长时间运行的操作,可以快速更新一组数据,保持UI更新 – 任何UI,无论是WPF还是WinForms,还是只是VT100仿真 – 都会出现同样的问题。 UI更新相对缓慢且复杂,并且将它们与快速变化的复杂过程集成而不会损害该过程需要两者之间的清晰分离。

在WPF中,这种干净的分离更为重要,因为UI和长时间运行的操作需要在不同的线程上运行,以便在操作运行时UI不会冻结。

你如何实现清洁分离? 通过独立实现它们,提供一种机制,用于在长时间运行的进程中定期更新UI,然后测试所有内容以确定应该调用该机制的频率。

在WPF中,您将有三个组件:1)视图,它是UI的物理模型,2)视图模型,它是UI中显示的数据的逻辑模型,并推动了更改通过更改通知将数据输出到UI,以及3)长时间运行的进程。

长时间运行的过程几乎完全不知道UI,只要它做两件事。 它需要公开公共属性和/或方法,以便视图模型可以检查其状态,并且每当应该更新UI时都需要引发事件。

视图模型侦听该事件。 引发事件时,它会将状态信息从进程复制到其数据模型,并且其内置的更改通知会将这些信息推送到UI。

multithreading使这复杂化,但只是一点点。 该过程需要在与UI不同的线程上运行,并且当处理其进度报告事件时,其数据将跨线程复制。

一旦构建了这三个部分,使用WPF的BackgroundWorker完成multithreading非常简单。 您创建将要运行该进程的对象,使用BackgroundWorkerReportProgress事件将其进度报告事件连接起来,并将对象属性中的数据ReportProgress送到该事件处理程序中的视图模型。 然后在BackgroundWorkerDoWork事件处理程序中触发对象的长时间运行方法,你就可以了。

比人眼可以观察到的更快的用户界面(~25次更新/秒)不是可用的用户界面。 一个典型的用户将在完全放弃之前观察最多一分钟的奇观。 如果您使UI线程冻结,那么你已经过去了。

你必须为人而不是机器设计。

由于UI处理的通知太多,为什么不只是略微限制通知? 这似乎工作正常:

  if (value % 500 == 0) OnPropertyChanged("Counter"); 

您还可以使用计时器限制通知的频率:

  public SO4522583() { InitializeComponent(); _timer = new DispatcherTimer(); _timer.Interval = TimeSpan.FromMilliseconds(50); _timer.Tick += new EventHandler(_timer_Tick); _timer.Start(); DataContext = this; } private bool _notified = false; private DispatcherTimer _timer; void _timer_Tick(object sender, EventArgs e) { _notified = false; } ... long counter; public long Counter { get { return counter; } set { if (counter != value) { counter = value; if (!_notified) { _notified = true; OnPropertyChanged("Counter"); } } } } 

编辑:如果您不能跳过通知,因为它们被代码的其他部分使用,这里的解决方案不需要对代码进行大的更改:

  • 创建一个新属性UICounter ,它会限制通知,如上所示
  • Counter setter中,更新UICounter
  • 在您的UI中,绑定到UICounter而不是Counter

UI和库之间的层是必要的。 这将确保您能够进行交互测试,并且允许您在将来更换库中的其他实现,而无需进行太多更改。 这不是重复,而是为UI层提供通信接口的一种方式。 该层将接受来自库的对象,将它们转换为特定的数据传输对象,并将它们传递到另一个层,该层将负责限制更新并将它们转换为特定的VM对象。 我的观点是虚拟机应该尽可能愚蠢,它们唯一的责任应该是为视图提供数据。

您的qestion听起来类似于slow-down-refresh-of-bound-datagrid 。 至少答案是相似的

  • 将数据的卷影副本绑定到gui元素,而不是绑定原始数据。
  • 添加一个事件处理程序,用于从原始数据中以一定的延迟更新阴影副本。

您需要断开通知源与通知目标的连接。 现在你设置它的方式,每次值改变时,你都会经历一个完整的刷新周期(我相信这会阻止你的处理function继续进行)。 这不是你想要的。

为您的处理函数提供一个输出流,用于编写其通知。

在监视端,将输入流附加到该输出流,并将其用作UI组件的数据源。 这种方式根本没有任何通知事件处理 – 处理尽可能快地运行,将监视器数据输出到您提供的输出流。 您的监视器UI只是渲染它在输入流中接收的任何内容。

您将需要一个线程来连续读取输入流。 如果没有可用数据,那么它应该阻止。 如果它读取一些数据,它应该将其转储到UI中。

问候,

罗德尼