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.
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:
- Don't use
Thread
andThread.Sleep
. Always use the modernTask.Run
andawait Task.Delay
instead - Avoid
while(true)
constructs - Use a timer for periodic operations
- Don't define public fields. Use properties instead. In your case the property should be read-only.
- Prefer the delegate
EventHandler
overEventHandler<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.