WPF Datagrid 数据加载和性能
这篇文章并非讨论 WPF Datagrid 的性能数据,而只是简单介绍一下为了使其性能良好,你需要注意哪些方面。我不太想使用性能分析器来展示实际数据,而是尽可能地使用了 Stopwatch 类。这篇文章不会深入探讨处理海量数据的技术,例如分页或如何实现分页,而是专注于如何让 Datagrid 处理大数据。
这是生成我想要加载到 Datagrid 中的数据的 C# 类。
public class DataItem
{
public long Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public long Age { get; set; }
public string City { get; set; }
public string Designation { get; set; }
public string Department { get; set; }
}
public static class DataGenerator
{
private static int _next = 1;
public static IEnumerable GetData(int count)
{
for (var i = 0; i < count; i++)
{
string nextRandomString = NextRandomString(30);
yield return new DataItem
{
Age = rand.Next(100),
City = nextRandomString,
Department = nextRandomString,
Designation = nextRandomString,
FirstName = nextRandomString,
LastName = nextRandomString,
Id = _next++
};
}
}
private static readonly Random rand = new Random();
private static string NextRandomString(int size)
{
var bytes = new byte[size];
rand.NextBytes(bytes);
return Encoding.UTF8.GetString(bytes);
}
}
ViewModel 定义如下所示:
public class MainWindowViewModel : INotifyPropertyChanged
{
private void Notify(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
public event PropertyChangedEventHandler PropertyChanged;
private Dispatcher _current;
public MainWindowViewModel()
{
_current = Dispatcher.CurrentDispatcher;
DataSize = 50;
EnableGrid = true;
_data = new ObservableCollection();
}
private int _dataSize;
public int DataSize
{
get { return _dataSize; }
set
{
LoadData(value - _dataSize);
_dataSize = value;
Notify("DataSize");
}
}
private ObservableCollection _data;
public ObservableCollection Data
{
get { return _data; }
set
{
_data = value;
Notify("Data");
}
}
private bool _enableGrid;
public bool EnableGrid
{
get { return _enableGrid; }
set { _enableGrid = value; Notify("EnableGrid"); }
}
private void LoadData(int more)
{
Action act = () =>
{
EnableGrid = false;
if (more > 0)
{
foreach (var item in DataGenerator.GetData(more))
_data.Add(item);
}
else
{
int itemsToRemove = -1 * more;
for (var i = 0; i < itemsToRemove; i++)
_data.RemoveAt(_data.Count - i - 1);
}
EnableGrid = true;
};
//act.BeginInvoke(null, null);
_current.BeginInvoke(act, DispatcherPriority.ApplicationIdle);
}
}
如您所见,随着 DataSize 的改变,数据也会被加载。目前我使用滑块来调整加载大小。这一切都非常简单,而且有趣的事情从 XAML 开始。
为了将此“数据”应用到我的WPF数据网格,我将这个ViewModel实例应用到我的类的DataContext中。请参阅下面的窗口代码:
public partial class MainWindow : Window
{
private MainWindowViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = new MainWindowViewModel();
this.Loaded += (s, e) => DataContext = vm;
}
}
让我们从以下 XAML 开始:
<stackpanel>
<slider maximum="100" minimum="50" value="{Binding DataSize}" />
<label grid.row="1" content="{Binding DataSize}">
<datagrid grid.row="2" isenabled="{Binding EnableGrid}" itemssource="{Binding Data}">
</datagrid>
</stackpanel>
现在构建应用程序并运行。结果如下所示:
如上所示,我加载了 100 个项目,却看不到滚动条。让我们将滑块的 Maximum 属性从 100 改为 1000,然后重新运行应用程序。一次性将滑块拖到 1000。所以,即使加载了 1000 个项目,网格的响应也不太好。
让我们看一下内存使用情况:
对于一个只加载了 1000 条数据的应用程序来说,这已经相当繁重了。那么,究竟是什么占用了这么多内存呢?你可以连接内存分析器或使用 Windbg 查看内存内容,但由于我已经知道导致这个问题的原因,所以就不赘述了。
这个问题是由于 DataGrid 被放置在 StackPanel 中。垂直堆叠时,StackPanel 基本上会为其子项分配所需的所有空间。这使得 DataGrid 创建 1000 行(每行每列所需的所有 UI 元素!)并进行渲染。DataGrid 的虚拟化功能在这里没有发挥作用。
让我们做一个简单的修改,将 DataGrid 放入网格中。其 XAML 代码如下所示:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Slider Value="{Binding DataSize}" Minimum="50" Maximum="1000"/>
<Label Content="{Binding DataSize}" Grid.Row="1"/>
<DataGrid ItemsSource="{Binding Data}" Grid.Row="2" IsEnabled="{Binding EnableGrid}">
</DataGrid>
</Grid>
当我运行应用程序时,你会注意到,当我加载 1000 个项目时,同一个应用程序的性能(除了我刚才提到的 XAML 代码之外,没有任何代码更改)比以前好了很多。而且我还看到了漂亮的滚动条。
让我们看一下内存使用情况:
哇!差别简直是十倍!可以参考WPF虚拟化的文章:https://blog.csdn.net/hefeng_aspnet/article/details/147305605
那么我在这里还要谈论什么呢?
如果你注意到 ViewModel 的代码,你应该会看到我在加载数据时禁用了网格,并在完成后重新启用它。我还没有真正测试过这项技术是否有用,但我在 HTML 页面中使用过这项技术,当时列表框中的大量项目都需要被选中,这项技术非常有用。
在我展示的所有截图中,网格都是排序的。因此,当数据发生变化时,网格必须继续对数据进行排序,并根据您选择的排序方式进行显示。我认为这会造成很大的开销。如果可行的话,在更改数据之前,请考虑移除数据网格的排序功能,并且这样做不会对最终用户造成影响。我还没有测试过这一点,但分组功能应该也应该如此(大多数情况下,分组功能无法简单地移除)。
只需将 DataGrid 加载到任何其他面板(例如 Grid)而不是 StackPanel 中,您就能看到很大的区别。只要您将网格的可视区域保持在较小的范围内,WPF DataGrid 的性能就很好。
下面显示的是我的网格,加载了近一百万个数据项。与加载的数据量相比,占用空间相当小。这意味着,要么是WPF控件占用大量内存,要么是WPF UI虚拟化带来了好处。
排序对 DataGrid 的影响
由于没有对数据网格进行排序,将 100 万个项目加载到我的集合中花了将近 20 秒。
启用排序后,加载一半的数据项本身就花了 2 分钟多,加载全部数据项则花了 5 分钟多,我甚至因为太麻烦而关掉了应用程序。这很重要,因为应用程序会一直忙于处理数据变化时必须进行的排序,从而占用大量 CPU 资源。因此,由于我直接将其放入可观察集合中,因此每次添加数据项都可能触发排序。
相反,考虑在后端进行排序而不是使用数据网格。
如果虚拟化得到正确利用,尽管网格绑定到 100 万个项目,我仍然可以滚动应用程序。
在数据网格上使用 BeginInit() 和 EndInit()。
修改了 ViewModel 的 LoadData() 方法,使其在开始加载数据时调用 BeginInit(),并在加载完成后调用 EndInit()。这确实很有帮助。加载 100 万个项目(网格上未进行任何排序)仅花费了大约 8 秒(之前需要 18 秒)。可惜的是,没有花足够的时间使用分析器来显示实际数据。
窗口更改后的后台代码如下所示:
public partial class MainWindow : Window
{
private MainWindowViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = new MainWindowViewModel();
this.Loaded += (s, e) => DataContext = vm;
vm.DataChangeStarted += () => dg.BeginInit();
vm.DataChangeCompleted += () => dg.EndInit();
}
}
我还必须将 DataChangeStarted 和 DataChangeCompleted 操作添加到 ViewModel 类中。ViewModel 类的更改部分如下所示:
public event Action DataChangeStarted ;
public event Action DataChangeCompleted;
private void LoadData(int more)
{
Action act = () =>
{
//Before the data starts change, call the method.
if (DataChangeStarted != null) DataChangeStarted();
var sw = Stopwatch.StartNew();
EnableGrid = false;
if (more > 0)
{
foreach (var item in DataGenerator.GetData(more))
_data.Add(item);
}
else
{
int itemsToRemove = -1 * more;
for (var i = 0; i < itemsToRemove; i++)
_data.RemoveAt(_data.Count - i - 1);
}
EnableGrid = true;
sw.Stop();
Debug.WriteLine(sw.ElapsedMilliseconds);
if (DataChangeCompleted != null) DataChangeCompleted();
};
//act.BeginInvoke(null, null);
_current.BeginInvoke(act, DispatcherPriority.ApplicationIdle);
}
您可以尝试一下并亲自观察性能差异。
如果在数据网格上进行排序,即使使用了上述技巧,性能仍然会受到影响。排序的开销抵消了调用 BeginInit 和 EndInit 所获得的性能提升。拥有 100 万条记录可能不太现实。
如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天。