Home > Blockchain >  WPF DataGrid.items.Refresh() memory leak
WPF DataGrid.items.Refresh() memory leak

Time:12-24

Good afternoon. Help in solving the following problem. If I set some DataTable for DataGrid.ItemsSource, which I will update in another thread, then periodically calling DataGrid.Items.Refresh() I will have a memory leak. Is there a way to avoid this?

<Window x:Class="TestDataGrid.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:TestDataGrid"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="7*"/>
        </Grid.ColumnDefinitions>
        <DataGrid x:Name="DataGrid1" d:ItemsSource="{d:SampleData ItemCount=5}" Grid.ColumnSpan="2"/>

    </Grid>
</Window>

MainWindow

using System;
using System.Windows;
namespace TestDataGrid
{    
    public partial class MainWindow : Window
    {
        DataWorker data = new DataWorker();
        public MainWindow()
        {
            InitializeComponent();            
            DataGrid1.ItemsSource = data.dt.DefaultView;
            data.DataChanged  = OnDataChanged;
            data.RunThread();
        }

        private void OnDataChanged(object obj, EventArgs e)
        {
            Dispatcher.Invoke(new Action(() =>
            {
                DataGrid1.Items.Refresh();
                //this.Title = data.dt.Rows[0].ItemArray[0].ToString();
            }));
        }
    }
}

DataWorker.cs

using System;
using System.Data;
using System.Threading;

namespace TestDataGrid
{
    internal class DataWorker
    {
        public event EventHandler DataChanged;
        public DataWorker()
        {
            column.DataType = Type.GetType("System.UInt64");
            column.ColumnName = "DATA";
            dt.Columns.Add(column);
        }

        public DataTable dt = new DataTable();
        private DataColumn column = new DataColumn();        

        public void RunThread()
        {
            Thread th = new Thread(DataChange);
            th.Start();
        }
        private void DataChange()
        {
            while (true)
            {
                dt.Clear();
                dt.Rows.Add(new object[] { DateTime.Now.Ticks });
                DataChanged?.Invoke(this, EventArgs.Empty);
                Thread.Sleep(1000);
            }
                
        }

    }
}

If I change DataGrid1.Items.Refresh() to this.Title = data.dt.Rows[0].ItemArray[0].ToString() for exemple, then memory OK.

Start Screen

2 min later Screen

Project: GitHub

CodePudding user response:

Not sure how

this.Title = data.dt.Rows[0].ItemArray[0].ToString();

relates to

DataGrid1.Items.Refresh();

This are two different operations targeting two different target objects.

The following simply assigns the cell value to the Window.Title property:

this.Title = data.dt.Rows[0].ItemArray[0].ToString();

Whereas

DataGrid1.Items.Refresh();

instructs the DataView to recreate the view. This means cached data is discarded, but still resides in the memory. It takes time until the garbage collector will collect this elegible objects.
This is not a memory leak.
There's some serious business going on under the hood of CollectionView.Refresh.
Just because it is a one liner doesn't mean you can compare its metrics to any random code line.

If the memory usage is an issue for you, consider to use a ObservableCollection instead of the DataTable.

If your depicted scenario is a real scenario, then using a ObservableCollection is more convenient than using a DataTable that has a single row and a single column. In your case, you would add the timestamp directly to the ObservableCollection.

Some suggestions to improve your code:

  1. Don't use Thread and Thread.Sleep. Always use the modern Task.Run and await Task.Delay instead
  2. Avoid while(true) constructs
  3. Use a timer for periodic operations
  4. Don't define public fields. Use properties instead. In your case the property should be read-only.
  5. Prefer the delegate EventHandler over EventHandler<EventArgs>:
event EventHandler MyEvent;

over

event EventHandler<EventArgs> MyEvent;

The improved implementation looks like this:

DataWorker.cs

internal class DataWorker
{
  private DispatcherTimer Timer { get; }
  public DataTable Dt { get; } = new DataTable();
  private DataColumn Column { get; } = new DataColumn();
  public event EventHandler DataChanged;

  public DataWorker()
  {
    this.Column.DataType = Type.GetType("System.UInt64");
    this.Column.ColumnName = "DATA";
    this.Dt.Columns.Add(this.Column);

    this.Timer = new DispatcherTimer(
      TimeSpan.FromSeconds(1), 
      DispatcherPriority.Normal, 
      OnTimerElapsed, 
      Application.Current.Dispatcher);
  }

  private void OnTimerElapsed(object? sender, EventArgs e)
  {
    this.Dt.Clear();
    this.Dt.Rows.Add(new object[] { DateTime.Now.Ticks });
    this.DataChanged?.Invoke(this, EventArgs.Empty);
  }
}

MainWindow.xaml.cs

public MainWindow(TestViewModel dataContext)
{
  var data = new DataWorker();
  DataGrid.ItemsSource = data.dt.DefaultView;
  data.DataChanged  = OnDataChanged;
}

private void OnDataChanged(object obj, EventArgs e)
{
  DataGrid.Items.Refresh();
}

How to identify a memory leak

It is not enough to watch the total memory allocation. There are many reasons why the memory allocation increases over time. What characterizes a memory leak is that the momory contains objects that are never collected, because they are always reachable. The reasons for this a various. It can be because of static references or object lifetime management of event sources, data binding etc.

To identify the leak, you must inspect the objects on the heap. You do this by comparing snapshots of the memory. And by forcing garbage collection like this:

for (int i = 0; i < 4; i  )
{
    GC.Collect(2, GCCollectionMode.Forced, true);
    GC.WaitForPendingFinalizers();
}

If objects are still on the heap, although you expect them to be collected because you have removed all references, you probably have a leak.

Each good profiler allows you to expand the reference tree in order to identify the class that causes the object to be reachable. You the have inspect this class to find the cause for the strong reference.

  • Related