这是我之前的问题的延续
How to start a thread to keep GUI refreshed?
但是由于Jon对这个问题有了新的认识,我将不得不完全重写原始问题,这会使该主题难以理解。所以,新的,非常具体的问题。
两件:
当前情况 - 库发送了很多关于数据更改的通知,尽管它在自己的线程中工作,它完全阻塞了WPF数据绑定机制,结果不仅监视数据不起作用(它没有刷新)而且整个处理数据时会冻结GUI。
目标 - 精心设计,抛光的方式使GUI保持最新 - 我不是说它应该立即显示数据(它甚至可以跳过一些更改),但它在计算时不能冻结。
这是简化示例,但它显示了问题。
XAML部分:
<StackPanel Orientation="Vertical">
<Button Click="Button_Click">Start</Button>
<TextBlock Text="{Binding Path=Counter}"/>
</StackPanel>
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);
}
}
}
(请不要将它们作为答案重新发布)
我根据我喜欢的解决方法对列表进行了排序,即它需要多少工作,限制等等。
我当前的“解决方案”是在主循环中添加Sleep。减速可以忽略不计,但它足以刷新WPF(因此它甚至比每次通知前睡觉更好)。
我全神贯注于真正的解决方案,而不是一些技巧。
说明
备注放弃数据绑定 - 对我来说,它的设计被打破了,在WPF中你有单一的通信渠道,你不能直接绑定到变更源。数据绑定根据名称过滤源(字符串!)。即使您使用一些聪明的结构来保留所有字符串,这也需要一些计算。
编辑:备注抽象 - 叫我老计时器,但我开始学习计算机,确信计算机应该帮助人类。重复性任务是计算机领域,而不是人类。无论你怎么称呼它 - MVVM,抽象,接口,单继承,如果你一遍又一遍地编写相同的代码,并且你没有办法自动化你所做的事情,你就会使用破坏的工具。所以例如lambdas很棒(对我来说工作量少)但单一继承不是(对我来说更多的工作),数据绑定(作为一个想法)很棒(工作量少)但需要代理层 EVERY 我绑定的库是破碎的想法,因为它需要大量的工作。
答案 0 :(得分:5)
在我的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<string> _propertyChanges = new HashSet<string>();
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<string> changes = null;
lock (_propertyChanges) {
_Timer.Stop(); // doing this in callback is safe and disables timer
if (!_propertyChanges.Contain(""))
changes = new List<string>(_propertyChanges);
_propertyChanges.Clear();
}
if (changes == null)
OnPropertyChange(null);
else
foreach (string property in changes)
OnPropertyChanged(property);
}
答案 1 :(得分:4)
这本身并不是WPF问题。如果您有一个长时间运行的操作,可以快速更新一组数据,保持UI更新 - 任何UI,无论是WPF还是WinForms,还是只是VT100仿真 - 都会出现同样的问题。 UI更新相对缓慢且复杂,并且将它们与快速变化的复杂过程集成而不会损害该过程需要两者之间的清晰分离。
在WPF中,干净的分离更为重要,因为UI和长时间运行的操作需要在不同的线程上运行,以便在操作运行时UI不会冻结。
你如何实现清洁分离?通过独立实现它们,提供一种机制,用于在长时间运行的进程中定期更新UI,然后测试所有内容以确定应该调用该机制的频率。
在WPF中,您将拥有三个组件:1)视图,它是UI的物理模型,2)视图模型,它是UI中显示的数据的逻辑模型,以及通过更改通知将数据中的更改推送到UI,以及3)长时间运行的进程。
长时间运行的过程几乎完全不知道UI,只要它做两件事。它需要公开公共属性和/或方法,以便视图模型可以检查其状态,并且只要应该更新UI就需要引发事件。
视图模型侦听该事件。引发事件时,它会将状态信息从进程复制到其数据模型,并且其内置的更改通知会将这些信息推送到UI。
多线程使这复杂化,但只是一点点。该过程需要在与UI不同的线程上运行,并且当处理其进度报告事件时,其数据将跨线程复制。
一旦构建了这三个部分,使用WPF BackgroundWorker
完成多线程非常简单。您创建将要运行流程的对象,使用BackgroundWorker
的{{1}}事件连接其进度报告事件,并将对象属性中的数据封送到该事件处理程序中的视图模型。然后在ReportProgress
的{{1}}事件处理程序中触发对象的长时间运行方法,你就可以了。
答案 2 :(得分:3)
比人眼可以观察到的更快的用户界面(~25次更新/秒)不是可用的用户界面。一个典型的用户将在完全放弃之前观察最多一分钟的景观。如果你让UI线程冻结,你就已经过去了。
你必须为人而不是机器设计。
答案 3 :(得分:1)
由于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
UICounter
而不是Counter
答案 4 :(得分:0)
UI和库之间的层是必要的。这将确保您能够进行交互测试,并允许您在将来更换库中的其他实现,而无需进行太多更改。这不是重复,而是为UI层提供通信接口的一种方式。该层将接受来自库的对象,将它们转换为特定的数据传输对象,并将它们传递到另一个层,该层将负责限制更新并将其转换为特定的VM对象。 我的观点是虚拟机应该尽可能地愚蠢,它们唯一的责任应该是为视图提供数据。
答案 5 :(得分:0)
您的问题听起来与slow-down-refresh-rate-of-bound-datagrid类似。 至少答案是相似的
答案 6 :(得分:0)
您需要断开通知源与通知目标的连接。现在你设置它的方式,每次值改变时,你都会经历一个完整的刷新周期(我相信这会阻止你的处理功能继续进行)。这不是你想要的。
为您的处理函数提供一个输出流,用于编写通知。
在监视端,将输入流附加到该输出流,并将其用作UI组件的数据源。这种方式根本没有任何通知事件处理 - 处理尽可能快地运行,将监视器数据输出到您提供的输出流。您的监视器UI只是渲染它在输入流中收到的任何内容。
您需要一个线程来连续读取输入流。如果没有可用的数据,那么它应该阻止。如果它读取了一些数据,它应该将其转储到UI中。
此致
罗德尼