????????這篇文章并非討論 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 萬條記錄可能不太現實。
如果您喜歡此文章,請收藏、點贊、評論,謝謝,祝您快樂每一天。?