Introduction
This article will introduce the hand-on experience the author and his team members used to solve the slow performance of WPF Treeview
Control which contains two levels of broad data.
Background
Currently, I am working on enhancing performance with a Job page. This page has a two levels depth TreeView
control. The top level displays the list of jobs, and the second level displays the number of job associated items.
Here is the initial page of jobs. You can see the job associated items when you expand the job.
In the current application, the numbers of jobs get increased by 10 everyday, and each job has around 150 Job items on average.
This page gets slower and slower as more and more jobs gets created until it reaches the maximum acceptance threshold.
Our development team looked at that page, and we discovered that we load all the jobs and associated job items at once, and passed it to the client, and client uses TreeView
and HierarchicalDataTemplate
to display the job and job items. As a consequence, the more job and job items get added, the slower is will be for sure.
I did some investigation/Googling/experiments to see if we can lazy load tree without any additional change. I found that the current WPF TreeView
control will eager load two level node, i.e., If you expand a node, it will load all the child and grand child nodes, but will not load any node which is below the grand child nodes. It doesn’t help us because we have only two levels of nodes.
After a discussion with team members, we comes up with two possible approaches:
Approach 1: There is no need to use WPF TreeView
control since we have only two levels of data. We could use ListView
control to display job collection, after user clicks on the job, the associated job item can be retrieved from server, and be displayed.
Approach 2: We still use TreeView
control; however, we will overwrite the ExpandEvent
. It will allows us to lazyload Job Items. I found a fix from the Microsoft website here.
Approach 2 was chosen since we would like to make a minimum change to the current application.
Using the Code
In the example, you can switch between my original implementation and the performance improved implementation. You can open the App.xaml, and change the StartupUri
value. If StartupUrl="Job.xaml"
, then it is the original implementation, and the performance is poor, if StartupUrl="LazyJob.xaml"
, then the performance is very fast.
Here is the sample code for approach 2:
Job.xaml
Job UI xaml page hooks up two events, Loaded
and Unloaded
event. It also defines two templates, JobItemTemplate
and JobTemplate
. Each template contains a checkbox
control and a TextBlock
control. Xaml page also has a empty TreeView
which will be updated later dynamically.
<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>
Here is Job.xmls.cs code behind file.
While the page gets loaded, it hooks up TreeViewItem
expand event with customized eventhandler
(OnTreeItemExpanded
), it also loads the first level of node (LoadRootNodes
). Once use clicks and expands on the tree, the OnTreeItemExpanded
gets invoked, and it loads the associated 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
class has two methods, one of loading only the Job object collection, and another method will load JobItemCollection
for expanded job.
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); }
}
}
That is it. In approach 2, we load job items only when we need it. After implementing the change, it becomes about 150 times faster to get this page displayed.
Reference