簡體   English   中英

快速更改的集合MVVM WPF-高CPU使用率和UI幾乎凍結

[英]Fast changing collection MVVM WPF - high CPU usage & UI almost freezes

我正在開發一個帶有數據網格的應用程序,該應用程序顯示某些正在運行的Windows進程(在我的示例Chrome進程中)。 選中復選框后,將在數據網格中加載進程。

要求:

  • 顯示每個進程的名稱,內存使用情況(專用工作集)的“實時”信息,就像在Windows任務管理器的“進程”選項卡中一樣。
  • 監視退出的進程並將其從數據網格中刪除。
  • 監視啟動的某些進程。

二手技術:

發行人:

  • 加載進程后,CPU使用率非常高,UI幾乎凍結。
  • 即使調用ManagerService.Stop() CPU使用率仍然很高。
  • 有時,當從集合中刪除某個進程時,將引發System.InvalidOperationException - Cannot change ObservableCollection during a CollectionChanged event異常System.InvalidOperationException - Cannot change ObservableCollection during a CollectionChanged event

我該如何解決此問題? 我的方法也是一種“良好實踐”嗎?

任何幫助將不勝感激! 我已經在這個問題上花了很多時間。

更新1

並沒有幫助,刪除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));
    }
}

更新2

按照Evk的建議,我已更新

  • 使用常規的ObservableCollection
  • 將計時器移至視圖模型

現在,CPU使用率要低得多。 但是我有時會得到一個Process with an ID of ... is not runningProcess with an ID of ... is not runningOnProcessStarted() Process with an ID of ... is not running異常 在此處輸入圖片說明

視圖模型

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)
            {

            }
        }

模型

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));
                }
            }
        }

服務

        private void OnProcessNotification(NotificationMessage<Process> notMessage)
        {
            if (notMessage.Notification == "exited")
            {
                _processes.Remove(p => p.ProcessObject.Id == notMessage.Content.Id, DispatcherHelper.UIDispatcher);
            }

        }

原始碼

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代碼

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();
    }
}

視圖模型

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
    }

服務

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)
        {

        }

    }

模型

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;
            }
        }
    }

寫入GUI非常昂貴。 如果每個用戶觸發的事件僅執行一次,您將不會注意到它。 但是,一旦您從任何類型的循環(包括在另一個線程上運行的循環)進行寫入,您都會注意到它。 我什至為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");
        }
    }
}

您的代碼甚至稍差一些,因為您允許在每次更新之間運行Update和Layout代碼。 盡管它確實可以使UI保持響應,但要運行的代碼更多。

您將無法限制更新。 我將這些限制明確地放在“查看側”上。 我個人更喜歡這樣:

  1. 不要注冊變為Observable集合的Change Notificaiton事件
  2. 設置一個計時器,以使用Collection的當前值定期更新UI。 將計時器設置為每秒60次更新。 對於人類來說,那應該足夠快。
  3. 您可能需要在編寫Collection和訪問者代碼的代碼中添加某種形式的Locking以避免競爭情況。

一些注意事項:

我的寵兒Peeve是Exception Hanlding。 我在那里看到一些致命異常。 您確實應該盡快解決該問題。 線程可能會意外吞下異常,這非常糟糕,您不應該為此編寫其他代碼。 這是我經常鏈接的兩篇文章: 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

其次,眾所周知,ObservableColelctions具有完全的返工性。 它缺少添加范圍功能。 因此,每次更改都會觸發更新。 我通常的解決方法是:1.給屬性公開“集合更改通知”。2.在任何更新上都不要使用公開的集合。 3.而是使用后台集合。 僅當此新狀態完成時,才公開它。

不用您自己更新/刷新UI,而是利用通過DataBindingPropertyChanged事件實現的WPF更改通知系統。

正如MSDN所言-

INotifyPropertyChanged接口用於通知客戶端(通常是綁定客戶端)屬性值已更改。

例如,考慮一個具有名為FirstName的屬性的Person對象。 為了提供通用的屬性更改通知, Person類型實現INotifyPropertyChanged接口,並在更改FirstName時引發PropertyChanged事件。

更多細節在這里

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM