[英]Fast changing collection MVVM WPF - high CPU usage & UI almost freezes
I am developing an app with a datagrid of that displays certain running Windows processes (in my example Chrome processes). 我正在开发一个带有数据网格的应用程序,该应用程序显示某些正在运行的Windows进程(在我的示例Chrome进程中)。 The datagrid is loaded with processes when a checkbox is checked.
选中复选框后,将在数据网格中加载进程。
Requirements: 要求:
Used techniques: 二手技术:
Issue(s): 发行人:
ManagerService.Stop()
is called. ManagerService.Stop()
CPU使用率仍然很高。 System.InvalidOperationException - Cannot change ObservableCollection during a CollectionChanged event
exception is thrown when a process is removed from the collection. System.InvalidOperationException - Cannot change ObservableCollection during a CollectionChanged event
异常System.InvalidOperationException - Cannot change ObservableCollection during a CollectionChanged event
。 How can I fix this issues? 我该如何解决此问题? Also is my approach a 'good practice' one?
我的方法也是一种“良好实践”吗?
Any help would be greatly appreciated! 任何帮助将不胜感激! I've already spent a lot of time on this issue.
我已经在这个问题上花了很多时间。
Update 1 更新1
Didn't help, removing OnRendering()
and implementing INotifyPropertyChanged
并没有帮助,删除
OnRendering()
并实现INotifyPropertyChanged
public class CustomProcess : INotifyPropertyChanged
{
private double _memory;
public double Memory
{
get { return _memory; }
set
{
if (_memory != value)
{
_memory = value;
OnPropertyChanged(nameof(Memory));
}
}
}
private bool _isChecked;
public bool IsChecked
{
get { return _isChecked; }
set
{
if (_isChecked != value)
{
_isChecked = value;
OnPropertyChanged(nameof(IsChecked));
}
}
Update 2 更新2
Following Evk advice I've updated 按照Evk的建议,我已更新
CPU usage is much lower now. 现在,CPU使用率要低得多。 However I sometimes get an
Process with an ID of ... is not running
exception in the OnProcessStarted()
但是我有时会得到一个
Process with an ID of ... is not running
的Process with an ID of ... is not running
在OnProcessStarted()
Process with an ID of ... is not running
异常
Viewmodel 视图模型
public class MainViewModel
{
System.Threading.Timer timer;
private ObservableCollection<CustomProcess> _processes;
public ObservableCollection<CustomProcess> Processes
{
get
{
if (_processes == null)
_processes = new ObservableCollection<CustomProcess>();
return _processes;
}
}
private void OnBooleanChanged(PropertyChangedMessage<bool> propChangedMessage)
{
if (propChangedMessage.NewValue == true)
{
_managerService.Start(_processes);
timer = new System.Threading.Timer(OnTimerTick, null, 0, 200); //every 200ms
ProcessesIsVisible = true;
}
else
{
timer.Dispose();
_managerService.Stop();
ProcessesIsVisible = false;
}
}
private void OnTimerTick(object state)
{
try
{
for (int i = 0; i < Processes.Count; i++)
Processes[i].UpdateMemory();
}
catch (Exception)
{
}
}
Model 模型
public class CustomProcess : INotifyPropertyChanged
{
public void UpdateMemory()
{
if (!ProcessObject.HasExited)
Memory = Process.GetProcessById(ProcessObject.Id).PagedMemorySize64;
}
private double _memory;
public double Memory
{
get { return _memory; }
set
{
if (_memory != value)
{
_memory = value;
OnPropertyChanged(nameof(Memory));
}
}
}
Service 服务
private void OnProcessNotification(NotificationMessage<Process> notMessage)
{
if (notMessage.Notification == "exited")
{
_processes.Remove(p => p.ProcessObject.Id == notMessage.Content.Id, DispatcherHelper.UIDispatcher);
}
}
Original code 原始码
XAML XAML
<DataGrid ItemsSource="{Binding Processes}">
<DataGridTextColumn Header="Process name"
Binding="{Binding ProcessObject.ProcessName}"
IsReadOnly='True'
Width='Auto' />
<DataGridTextColumn Header="PID"
Binding="{Binding ProcessObject.Id}"
IsReadOnly='True'
Width='Auto' />
<DataGridTextColumn Header="Memory"
Binding='{Binding Memory}'
IsReadOnly='True'
Width='Auto' />
</DataGrid>
XAML Code behind 后面的XAML代码
public MainWindow()
{
InitializeComponent();
DataContext = SimpleIoc.Default.GetInstance<MainViewModel>();
CompositionTarget.Rendering += OnRendering;
}
private void OnRendering(object sender, EventArgs e)
{
if (DataContext is IRefresh)
((IRefresh)DataContext).Refresh();
}
}
ViewModel 视图模型
public class MainViewModel : Shared.ViewModelBase, IRefresh
{
private AsyncObservableCollection<CustomProcess> _processes;
public AsyncObservableCollection<CustomProcess> Processes
{
get
{
if (_processes == null)
_processes = new AsyncObservableCollection<CustomProcess>();
return _processes;
}
}
private readonly IManagerService _managerService;
public MainViewModel(IManagerService managerService)
{
_managerService = managerService;
Messenger.Default.Register<PropertyChangedMessage<bool>>(this, OnBooleanChanged);
}
#region PropertyChangedMessage
private void OnBooleanChanged(PropertyChangedMessage<bool> propChangedMessage)
{
if (propChangedMessage.NewValue == true)
{
_managerService.Start(_processes);
}
else
{
_managerService.Stop();
}
}
public void Refresh()
{
foreach (var process in Processes)
RaisePropertyChanged(nameof(process.Memory)); //notify UI that the property has changed
}
Service 服务
public class ManagerService : IManagerService
{
AsyncObservableCollection<CustomProcess> _processes;
ManagementEventWatcher managementEventWatcher;
public ManagerService()
{
Messenger.Default.Register<NotificationMessage<Process>>(this, OnProcessNotification);
}
private void OnProcessNotification(NotificationMessage<Process> notMessage)
{
if (notMessage.Notification == "exited")
{
//a process has exited. Remove it from the collection
_processes.Remove(p => p.ProcessObject.Id == notMessage.Content.Id);
}
}
/// <summary>
/// Starts the manager. Add processes and monitor for starting processes
/// </summary>
/// <param name="processes"></param>
public void Start(AsyncObservableCollection<CustomProcess> processes)
{
_processes = processes;
_processes.CollectionChanged += OnCollectionChanged;
foreach (var process in Process.GetProcesses().Where(p => p.ProcessName.Contains("chrome")))
_processes.Add(new CustomProcess(process));
MonitorStartedProcess();
Task.Factory.StartNew(() => MonitorLogFile());
}
/// <summary>
/// Stops the manager.
/// </summary>
public void Stop()
{
_processes.CollectionChanged -= OnCollectionChanged;
managementEventWatcher = null;
_processes = null;
}
private void MonitorLogFile()
{
//this code monitors a log file for changes. It is possible that the IsChecked property of a CustomProcess object is set in the Processes collection
}
/// <summary>
/// Monitor for started Chrome
/// </summary>
private void MonitorStartedProcess()
{
var qStart = "SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName like '%chrome%'";
ManagementEventWatcher managementEventWatcher = new ManagementEventWatcher(new WqlEventQuery(qStart));
managementEventWatcher.EventArrived += new EventArrivedEventHandler(OnProcessStarted);
try
{
managementEventWatcher.Start();
}
catch (Exception)
{
}
}
private void OnProcessStarted(object sender, EventArrivedEventArgs e)
{
try
{
int pid = Convert.ToInt32(e.NewEvent.Properties["ProcessID"].Value);
_processes.Add(new CustomProcess(Process.GetProcessById(pid))); //add to collection
}
catch (Exception)
{
}
}
Model 模型
public class CustomProcess
{
public Process ProcessObject { get; }
public CustomProcess(Process process)
{
ProcessObject = process;
try
{
ProcessObject.EnableRaisingEvents = true;
ProcessObject.Exited += ProcessObject_Exited;
Task.Factory.StartNew(() => UpdateMemory());
}
catch (Exception)
{
}
}
private void ProcessObject_Exited(object sender, EventArgs e)
{
Process process = sender as Process;
NotificationMessage<Process> notMessage = new NotificationMessage<Process>(process, "exited");
Messenger.Default.Send(notMessage); //send a notification that the process has exited
}
private void UpdateMemory()
{
while (!ProcessObject.HasExited)
{
try
{
Memory = Process.GetProcessById(ProcessObject.Id).PagedMemorySize64;
}
catch (Exception)
{
}
}
}
private double _memory;
public double Memory
{
get { return _memory; }
set
{
if (_memory != value)
{
_memory = value;
}
}
}
private bool _isChecked;
public bool IsChecked
{
get { return _isChecked; }
set
{
if (_isChecked != value)
{
_isChecked = value;
}
}
}
Writing to a GUI is expensive. 写入GUI非常昂贵。 If you only do it once per user triggered event you will not notice it.
如果每个用户触发的事件仅执行一次,您将不会注意到它。 But once you write from any kind of loop - including a loop running on another thread - you will notice it.
但是,一旦您从任何类型的循环(包括在另一个线程上运行的循环)进行写入,您都会注意到它。 I even wrote some example code for Windows Forms to showcase this:
我什至为Windows Forms编写了一些示例代码来说明这一点:
using System;
using System.Windows.Forms;
namespace UIWriteOverhead
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
int[] getNumbers(int upperLimit)
{
int[] ReturnValue = new int[upperLimit];
for (int i = 0; i < ReturnValue.Length; i++)
ReturnValue[i] = i;
return ReturnValue;
}
void printWithBuffer(int[] Values)
{
textBox1.Text = "";
string buffer = "";
foreach (int Number in Values)
buffer += Number.ToString() + Environment.NewLine;
textBox1.Text = buffer;
}
void printDirectly(int[] Values){
textBox1.Text = "";
foreach (int Number in Values)
textBox1.Text += Number.ToString() + Environment.NewLine;
}
private void btnPrintBuffer_Click(object sender, EventArgs e)
{
MessageBox.Show("Generating Numbers");
int[] temp = getNumbers(10000);
MessageBox.Show("Printing with buffer");
printWithBuffer(temp);
MessageBox.Show("Printing done");
}
private void btnPrintDirect_Click(object sender, EventArgs e)
{
MessageBox.Show("Generating Numbers");
int[] temp = getNumbers(1000);
MessageBox.Show("Printing directly");
printDirectly(temp);
MessageBox.Show("Printing done");
}
}
}
Your code is even slightly worse, as you allow the Update and thus Layout code to run between each update. 您的代码甚至稍差一些,因为您允许在每次更新之间运行Update和Layout代码。 While it does keep the UI responsive, it is more code to run.
尽管它确实可以使UI保持响应,但要运行的代码更多。
You will not get around limiting the updates. 您将无法限制更新。 I would put these kinds of Limitations clearly on the View Side.
我将这些限制明确地放在“查看侧”上。 Personally I prefer this way:
我个人更喜欢这样:
A few side notes: 一些注意事项:
A pet Peeve of mine is Exception Hanlding. 我的宠儿Peeve是Exception Hanlding。 And I see some swallowing of Fatal Exceptions there.
我在那里看到一些致命异常。 You really should fix that ASAP.
您确实应该尽快解决该问题。 It is bad enough that Threads can accidentally swallow exceptions, you should not write additional code for this.
线程可能会意外吞下异常,这非常糟糕,您不应该为此编写其他代码。 Here are two articles I link a lot: http://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx |
这是我经常链接的两篇文章: http : //blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx | http://www.codeproject.com/Articles/9538/Exception-Handling-Best-Practices-in-NET
http://www.codeproject.com/Articles/9538/Exception-Handling-Best-Practices-in-NET
Secondly, ObservableColelctions are notoriously bad with complete reworks. 其次,众所周知,ObservableColelctions具有完全的返工性。 It lacks a add-range Function.
它缺少添加范围功能。 So every single change will trigger an update.
因此,每次更改都会触发更新。 My usual workaround is: 1. Give the property exposing the Collection Change Notification 2. Do not work with the exposed collection on any update.
我通常的解决方法是:1.给属性公开“集合更改通知”。2.在任何更新上都不要使用公开的集合。 3. Instead work with a background collection.
3.而是使用后台集合。 Only when this new state is finished, do you expose it.
仅当此新状态完成时,才公开它。
Instead of you updating/refresing the UI yourself, make use of the WPF change notification system achieved using DataBinding
& PropertyChanged
event. 不用您自己更新/刷新UI,而是利用通过
DataBinding
& PropertyChanged
事件实现的WPF更改通知系统。
As MSDN quotes - 正如MSDN所言-
The INotifyPropertyChanged
interface is used to notify clients, typically binding clients, that a property value has changed. INotifyPropertyChanged
接口用于通知客户端(通常是绑定客户端)属性值已更改。
For example, consider a Person
object with a property called FirstName
. 例如,考虑一个具有名为
FirstName
的属性的Person
对象。 To provide generic property-change notification, the Person
type implements the INotifyPropertyChanged
interface and raises a PropertyChanged
event when FirstName
is changed. 为了提供通用的属性更改通知,
Person
类型实现INotifyPropertyChanged
接口,并在更改FirstName
时引发PropertyChanged
事件。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.