LazyLoad WPF Treeview 处理大量两级宽数据
提高 WPF Treeview 控件在加载大量两级宽数据时的性能
引言
本文将介绍作者及其团队成员用来解决包含两级广域数据的 WPF Treeview
控件性能慢的实践经验。
背景
目前,我正在通过一个作业页面来增强性能。 此页面具有两级深度的 TreeView
控件。 顶层显示作业列表,第二层显示与作业关联的项目的数量。
这是作业的初始页面。当您展开作业时,可以看到与作业相关的项目。

在当前应用程序中,作业数量每天增加 10 个,每个作业平均有大约 150 个作业项目。
随着越来越多的作业被创建,此页面的速度会越来越慢,直到达到最大可接受阈值。
我们的开发团队查看了该页面,我们发现我们一次性加载了所有作业和相关的作业项目,并将它们传递给客户端,客户端使用 TreeView
和 HierarchicalDataTemplate
来显示作业和作业项目。 因此,添加的作业和作业项目越多,速度肯定会越慢。
我做了一些调查/谷歌搜索/实验,看看我们是否可以在没有任何额外更改的情况下进行懒加载树。 我发现当前的 WPF TreeView
控件会急切地加载二级节点,即,如果展开一个节点,它将加载所有子节点和孙节点,但不会加载孙节点以下的任何节点。 这对我们没有帮助,因为我们只有两层节点。
在与团队成员讨论后,我们提出了两种可能的方法
方法 1:由于我们只有两级数据,因此无需使用 WPF TreeView
控件。 我们可以使用 ListView
控件来显示作业集合,用户点击作业后,可以从服务器检索关联的作业项目并显示。
方法 2:我们仍然使用 TreeView
控件; 但是,我们将覆盖 ExpandEvent
。 这将允许我们进行懒加载作业项目。 我在 Microsoft 网站上找到了一个修复方法,这里。
选择了方法 2,因为我们希望对当前应用程序进行最小的更改。
Using the Code
在示例中,您可以在我的原始实现和性能改进的实现之间切换。 您可以打开 App.xaml,并更改 StartupUri
值。 如果 StartupUrl="Job.xaml"
,那么它是原始实现,性能很差,如果 StartupUrl="LazyJob.xaml"
,那么性能就非常快。
这是方法 2 的示例代码
Job.xaml
Job UI xaml 页面挂钩了两个事件,Loaded
和 Unloaded
事件。 它还定义了两个模板,JobItemTemplate
和 JobTemplate
。 每个模板包含一个 checkbox
控件和一个 TextBlock
控件。 Xaml 页面还有一个空的 TreeView
,稍后将动态更新。
<Window x:Class="JobLazyLoad.LazyJob"
Loaded="OnLoaded" Unloaded="OnUnloaded">
<Window.Resources>
<DataTemplate x:Key = "JobItemTemplate" >
<CheckBox IsChecked="{Binding Path=IsSelected}">
<TextBlock Text="{Binding Path=Name}"/>
</CheckBox>
</DataTemplate>
<DataTemplate x:Key = "JobTemplate">
<CheckBox IsChecked="{Binding Path=IsSelected}">
<TextBlock Text="{Binding Path=Name}"/>
</CheckBox>
</DataTemplate>
</Window.Resources>
<Grid>
<TreeView x:Name="JobTree"/>
</Grid>
</Window>
这是 Job.xmls.cs 后台代码文件。
当页面加载时,它使用自定义的 eventhandler
(OnTreeItemExpanded
) 挂钩 TreeViewItem
展开事件,它还加载了第一级节点 (LoadRootNodes
)。 一旦使用点击并在树上展开,OnTreeItemExpanded
被调用,它会加载关联的 JobItemCollection
。
public partial class LazyJob : Window
{
private object _dummyNode = null;
private JobViewModel _viewModel;
public LazyJob()
{
InitializeComponent();
InitializeViewModel();
}
private void InitializeViewModel()
{
_viewModel = new JobViewModel();
this.DataContext = _viewModel;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
JobTree.AddHandler(TreeViewItem.ExpandedEvent,
new RoutedEventHandler(OnTreeItemExpanded));
LoadRootNodes();
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
JobTree.RemoveHandler(TreeViewItem.ExpandedEvent,
new RoutedEventHandler(OnTreeItemExpanded));
}
private void OnTreeItemExpanded(object sender, RoutedEventArgs e)
{
TreeViewItem item = e.OriginalSource as TreeViewItem;
if (item != null && item.Items.Count == 1 && item.Items[0] == _dummyNode)
{
item.Items.Clear();
Domain.Job job = item.Header as Domain.Job;
foreach (var acquisitionItem in _viewModel.JobItemCollection)
{
TreeViewItem subItem = new TreeViewItem();
subItem.Header = acquisitionItem;
subItem.HeaderTemplate = FindResource(
"JobItemTemplate") as DataTemplate;
item.Items.Add(subItem);
}
}
}
private void LoadRootNodes()
{
JobTree.Items.Clear();
foreach (Domain.Job job in _viewModel.LazyJobCollection)
{
TreeViewItem item = new TreeViewItem();
item.Header = job;
item.HeaderTemplate = FindResource("JobTemplate") as DataTemplate;
item.Items.Add(_dummyNode);
JobTree.Items.Add(item);
}
}
}
JobViewModel
类有两种方法,一种只加载 Job 对象集合,另一种方法将为展开的作业加载 JobItemCollection
。
public class JobViewModel
{
public int CurrentJobId { get; set; }
public IList<Domain.Job> LazyJobCollection
{
get { return new JobService().GetLazyJobCollection(); }
}
public IList<JobItem> JobItemCollection
{
get { return new JobService().GetJobItemCollection(CurrentJobId); }
}
}
就是这样。 在方法 2 中,我们仅在需要时才加载作业项目。 在实现更改后,显示此页面的速度提高了大约 150 倍。