При обновлении процессов внутри DataGrid приложение подвисает

Рейтинг: 0Ответов: 1Опубликовано: 26.04.2023

Я тут писал некое подобие процесс менеджера, и написал такой код для обновления процессов внутри DataGrid:

public MainWindow()
{
    InitializeComponent();

    DispatcherTimer? timer;
    timer = new DispatcherTimer();
    timer.Interval = TimeSpan.FromSeconds(1);
    timer.Tick += new EventHandler(ticker);
    timer.Start();
}
        
private void ticker(object? sender, EventArgs e)
{
    Thread t = new Thread(new ThreadStart(timer_Tick));
    t.Start();
}

private void timer_Tick()
{
    proccessesgrid.Items.Clear();
    Process[] processes = Process.GetProcesses();
    proccessesgrid.Dispatcher.Invoke(() =>
    {
        foreach (Process process in processes)
        {
            proccessesgrid.Items.Add(new appendprocess { process_icon = "/test/", process_pid = process.Id.ToString(), process_name = process.ProcessName, process_cpu = "test%", process_ram = "testMB", process_internet = "test%" });
        }
    });
}

до того как я начал создавать новый поток для timer_tick все работало, однако раз в секунду приложение подвисало, однако после того как я добавил многопоточность, код вообще перестал работать, я попробовал добавить proccessesgrid.Dispatcher.Invoke однако это не помогло, при запуске приложения в DataGrid процессы попросту не добавляються, что делать?

Ответы

▲ 0Принят

Process[] processes = Process.GetProcesses() - замануха, это только кажется, что все проще простого. А как только начнете разбираться а как же отобразить память, а как же отобразить % занятого процессора, сразу возникнет куча неразрешимых вопросов. Поэтому скажу сразу: единственное решение получения данных для вашей задачи - PerformanceCounter.

Далее, обновляя таблицу руками вы подвешиваете UI, делаете это неоптимально. Все контролы в WPF заточены на работу с привязками данных, давайте использовать привязки данных.

Таймеры. При наличии такой великолепной штуки как асинхронное программирование с помощью async/await - использовать таймеры сложно, поэтому я пойду так как проще.

Ну и плюс ко всему что выше, вы полностью очищаете таблицу в то время как 99% процессов на каждое обновление остаются те же самые. То есть гораздо эффективнее будет обновлять те что уже там есть, добавлять новые и удалять те что были закрыты.

Диспетчер задач "на минималках"

Для реализации примера использую WPF приложение на базе .NET 6

Вот такой XAML получился

<Window x:Class="WpfAppProcessView.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfAppProcessView"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" d:DataContext="{d:DesignInstance local:MainWindow,IsDesignTimeCreatable=False}" Loaded="Window_Loaded" Closing="Window_Closing">
    <Grid>
        <DataGrid ItemsSource="{Binding Items}"
                  IsReadOnly="True"
                  AutoGenerateColumns="False"
                  VirtualizingPanel.IsVirtualizing="True"
                  VirtualizingPanel.VirtualizationMode="Recycling"
                  MinColumnWidth="80"
                  GridLinesVisibility="None"
                  AlternatingRowBackground="{StaticResource {x:Static SystemColors.ControlBrushKey}}">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Id}" Header="Id"/>
                <DataGridTextColumn Binding="{Binding DisplayName}" Header="Name" Width="*"/>
                <DataGridTextColumn Binding="{Binding Processor, StringFormat={}{0:F1}%}" Header="CPU"/>
                <DataGridTextColumn Binding="{Binding DisplayMemory}" Header="Memory" SortMemberPath="Memory"/>
            </DataGrid.Columns>
            <DataGrid.CellStyle>
                <Style TargetType="DataGridCell">
                    <Setter Property="Margin" Value="5,2"/>
                </Style>
            </DataGrid.CellStyle>
        </DataGrid>
    </Grid>
</Window>

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

public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

И унаследоваться от этой реализации в модели данных, кстати, вот она.

public sealed class ProcessInfo : NotifyPropertyChanged
{
    private long memory;
    private CounterSample processorTime;
    private double processor;

    public ProcessInfo(long id, string name)
    {
        Id = id;
        Name = name;
    }

    public long Id { get; }
    public string Name { get; }

    public string DisplayMemory
    {
        get
        {
            SizeUnit sizeUnit = SizeUnit.GetBestSizeUnit(Memory);
            return $"{SizeUnit.Convert(Memory, SizeUnit.B, sizeUnit):G4} {sizeUnit.Name}";
        }
    }

    public string DisplayName
    {
        get
        {
            int sharpIndex = Name.IndexOf('#');
            return sharpIndex > 0 ? Name.Remove(sharpIndex) : Name;
        }
    }

    public long Memory
    {
        get => memory;
        set
        {
            if (memory == value)
                return;
            memory = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(DisplayMemory));
        }
    }

    public double Processor
    {
        get => processor;
        private set
        {
            if (processor == value)
                return;
            processor = value;
            OnPropertyChanged();
        }
    }

    public CounterSample ProcessorTime
    {
        get => processorTime;
        set
        {
            long cpu = value.RawValue - processorTime.RawValue;
            long time = value.TimeStamp - processorTime.TimeStamp; 
            Processor = cpu / (double)time * 100;
            processorTime = value;
            OnPropertyChanged();
        }
    }
}

Для удобной конвертации объема занятой оперативной памяти я использовал это решение.

public class SizeUnit
{
    public string Name { get; }
    public long ByteAmount { get; }

    public SizeUnit(string name, long byteAmount)
    {
        Name = name;
        ByteAmount = byteAmount;
    }

    private const long BytesInKiloByte = 1024L;

    public static readonly SizeUnit B = new("B", 1L);
    public static readonly SizeUnit KB = new("KB", BytesInKiloByte);
    public static readonly SizeUnit MB = new("MB", BytesInKiloByte * BytesInKiloByte);
    public static readonly SizeUnit GB = new("GB", BytesInKiloByte * BytesInKiloByte * BytesInKiloByte);
    public static readonly SizeUnit TB = new("TB", BytesInKiloByte * BytesInKiloByte * BytesInKiloByte * BytesInKiloByte);
    public static readonly SizeUnit[] All = { B, KB, MB, GB, TB };

    public static SizeUnit GetBestSizeUnit(long value)
    {
        foreach (var sizeUnit in All)
        {
            if (value < sizeUnit.ByteAmount * BytesInKiloByte)
                return sizeUnit;
        }
        return All[^1];
    }

    public static double Convert(long value, SizeUnit from, SizeUnit to)
        => value * (double)from.ByteAmount / to.ByteAmount;
}

В итоге, код окна получился такой

public partial class MainWindow : Window
{
    private readonly ICollectionView _itemsView;
    private readonly PerformanceCounterCategory _pcc;
    private CancellationTokenSource _cts;

    public ObservableCollection<ProcessInfo> Items { get; }

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        Items = new();
        _itemsView = CollectionViewSource.GetDefaultView(Items);
        _pcc = new("Process");
    }

    private async void StartLoop()
    {
        if (_cts != null)
            return;
        try
        {
            using (_cts = new CancellationTokenSource())
            {
                await RunLoopAsync(_cts.Token);
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, ex.GetType().Name);
        }
        _cts = null;
    }

    private void StopLoop()
    {
        _cts?.Cancel();
    }

    private async Task RunLoopAsync(CancellationToken token)
    {
        try
        {
            while (true)
            {
                await UpdateProcesses();
                await Task.Delay(1000, token);
            }
        }
        catch (OperationCanceledException)
        { }
    }

    private async Task UpdateProcesses()
    {
        InstanceDataCollectionCollection counters = await Task.Run(_pcc.ReadCategory);
        InstanceDataCollection ids = counters["id process"];
        InstanceDataCollection mem = counters["working set - private"];
        InstanceDataCollection cpu = counters["% processor time"];

        foreach (InstanceData data in ids.Values)
        {
            if (data.InstanceName == "_Total" || data.InstanceName == "Idle")
                continue;
            long id = data.RawValue;
            ProcessInfo info = Items.FirstOrDefault(x => x.Id == id);
            if (info is null)
            {
                info = new ProcessInfo(id, data.InstanceName);
                Items.Add(info);
            }
            info.Memory = mem[data.InstanceName].RawValue;
            info.ProcessorTime = cpu[data.InstanceName].Sample;
        }
        foreach (ProcessInfo info in Items.Where(item => ids[item.Name] is null).ToArray())
        {
            Items.Remove(info);
        }
        _itemsView.Refresh();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        StartLoop();
    }

    private void Window_Closing(object sender, CancelEventArgs e)
    {
        StopLoop();
    }
}

Для реализации асинхронного бесконечного цикла, срабатывающего раз в секунду, я использовал это решение.

Запускаю, и получаю вот такой вывод

введите сюда описание изображения