Быстро меняющаяся коллекция MVVM WPF - высокая загрузка ЦП и пользовательский интерфейс практически зависает - PullRequest
0 голосов
/ 24 апреля 2018

Я занимаюсь разработкой приложения с сеткой данных, в которой отображаются определенные запущенные процессы Windows (в моем примере процессы Chrome).Сетка данных загружается с процессами, когда установлен флажок.

Требования:

  • Отображение «живой» информации для имени, использования памяти (частный рабочий набор) каждого процесса, как и в диспетчере задач Windows - вкладка «Процессы».
  • Отслеживание процессов, которые завершаются, и удаление их из таблицы данных.
  • Отслеживание определенных процессов, которые запускаются.

Используемые приемы:

Проблема:

  • Когда процессы загружаются, загрузка ЦП становится очень высокой, а пользовательский интерфейс почти зависает.
  • Загрузка ЦП остается высокой даже при вызове ManagerService.Stop().
  • Иногда System.InvalidOperationException - Cannot change ObservableCollection during a CollectionChanged event исключение выдается при удалении процесса из коллекцииn.

Как я могу решить эту проблему?Кроме того, мой подход - это «хорошая практика»?

Любая помощь будет принята с благодарностью!Я уже потратил много времени на эту проблему.

Обновление 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
  • переместил таймер в viewmodel

Использование ЦП теперь намного ниже.Однако иногда я получаю исключение Process with an ID of ... is not running в OnProcessStarted() enter image description here

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

Сервис

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

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
    }

Сервис

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

Ответы [ 2 ]

0 голосов
/ 24 апреля 2018

Вместо того, чтобы самостоятельно обновлять / обновлять пользовательский интерфейс, используйте систему уведомлений об изменениях WPF, полученную с помощью события DataBinding & PropertyChanged.

Как указано в MSDN -

The *Интерфейс 1007 * используется для уведомления клиентов, обычно привязывающих клиентов, об изменении значения свойства.

Например, рассмотрим объект Person со свойством FirstName.Для предоставления общего уведомления об изменении свойства тип Person реализует интерфейс INotifyPropertyChanged и вызывает событие PropertyChanged при изменении FirstName.

Подробнее здесь .

0 голосов
/ 24 апреля 2018

Запись в 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");
        }
    }
}

Ваш код даже немного хуже, так как вы разрешаете запускать обновление и, следовательно, код макета между каждым обновлением. Несмотря на то, что он поддерживает отзывчивость интерфейса, он выполняет больше кода.

Вы не сможете обойти ограничения по обновлениям. Я бы четко обозначил эти ограничения на стороне просмотра. Лично я предпочитаю так:

  1. Не регистрировать события Change Notificaiton, реализованные в коллекции Observable
  2. Создайте таймер, который регулярно обновляет пользовательский интерфейс с текущим значением Коллекции. Установите таймер примерно на 60 обновлений в секунду. Это должно быть достаточно быстро для людей.
  3. Возможно, вы захотите добавить некоторую форму блокировки к коду, пишущему коллекцию, и коду доступа, чтобы избежать условий гонки.

Несколько примечаний:

Моя любимая мозоль - Исключение Ханьлдинга. И я вижу глотание фатальных исключений там. Вы действительно должны исправить это как можно скорее. Достаточно плохо, что потоки могут случайно проглотить исключения, вы не должны писать дополнительный код для этого. Вот две статьи, на которые я много ссылаюсь: 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. Вместо этого работайте с фоновой коллекцией. Только когда это новое состояние закончено, вы выставляете его.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...