How to improve TreeView’s performance – Part II

In my previous post, I discussed some of the performance limitations of TreeView. In particular, I mentioned the three facts about our current implementation that may lead to performance issues, depending on your scenario:
- UI elements stay in memory even when collapsed.
- There is no UI virtualization.
- There is no data virtualization.
Today I will talk about a trick that avoids the first problem and partially fixes the third. In my previous post, I always kept the data for all levels of the TreeView in memory. In this project, I only load subkey data items when their parent key node is expanded, and I discard those data items when their parent node is collapsed. You can think of the class that manages which portions of the data source stay in memory as an intermediate custom source, which sits between your UI and the complete source of your items. In this case, the complete source for the registry keys consists of the APIs used to load them into memory, but you can easily imagine how this could be a SQL database or a webservice. If you’ve read John Gossman’s thoughts on the Model-View-ViewModel pattern, you can think of this intermediate class as the “ViewModel” section.
Let’s start by looking at the custom data source. Similarly to my previous post, I have a RegistryKeyHolder2 class that contains a ShortName property and another property that holds the SubKeys collection of type ObservableCollection<RegistryKeyHolder2>. I also have a PopulateSubKeys() method that fills the SubKeys collection with instances of the children keys, which I showed in my previous post. The only new method I added to this class is ClearSubKeys(), which I will use to discard items from memory when I collapse a TreeViewItem:
public int ClearSubKeys()
{
int subKeyCount = CountSubKeys(this);
this.subKeys.Clear();
return subKeyCount;
}
Just like last week’s sample, I have a RegistryData2 class that contains a RootKeys property of type ObservableCollection<RegistryKeyHolder2>. This will hold the first level of keys displayed in the TreeView, as well as the whole hierarchy of keys that is displayed underneath it. However, unlike my previous post, this class has methods that will populate and clear the keys of just one level, and not the whole hierarchy. I implemented it this way because I will populate or clear items only when the TreeViewItems are expanded and collapsed, and that will only ever affect one level at a time.
public void PopulateSubKeys(RegistryKeyHolder2 keyHolder)
{
int itemsAddedCount = keyHolder.PopulateSubKeys();
…
}
public void ClearSubKeys(RegistryKeyHolder2 keyHolder)
{
int itemsClearedCount = keyHolder.ClearSubKeys();
…
}
The next step is to cause these methods to be called when TreeViewItems are expanded and collapsed. This can be done easily with the following code and XAML:
<TreeView
…
TreeViewItem.Collapsed="ItemCollapsedOrExpanded2"
TreeViewItem.Expanded="ItemCollapsedOrExpanded2">
…
</TreeView>
private void ItemCollapsedOrExpanded2(object sender, RoutedEventArgs e)
{
TreeViewItem tvi = (TreeViewItem)e.OriginalSource;
RegistryKeyHolder2 keyHolder = (RegistryKeyHolder2)tvi.Header;
RegistryData2 registryData = (RegistryData2)this.grid2.DataContext;
if (e.RoutedEvent == TreeViewItem.ExpandedEvent)
{
registryData.PopulateSubKeys(keyHolder);
}
else if (e.RoutedEvent == TreeViewItem.CollapsedEvent)
{
registryData.ClearSubKeys(keyHolder);
}
this.InvokeUpdateVisualCount(this.treeView2);
e.Handled = true;
}
This is basically all that needs to be done at the data level. If you use this source with the default style for TreeViewItem, however, you will see that you will not be provided with the UI to expand the first level of TreeViewItems. This happens because the expander arrow (or plus sign, depending on your theme) is only visible when the HasItems property of TreeViewItem is true. How did I figure this out? Easy, I used Blend to look at the default template for TreeViewItem. I started by creating a new project and adding a TreeView to it. Then I went to the “Object” menu, clicked on “Edit other styles”, “Edit ItemContainerStyle”, “Edit a Copy”, gave it a name and clicked OK. And that’s it, you can look at the default style for TreeViewItem in the XAML tab. Here is the part of the XAML that causes the undesired behavior:
<ControlTemplate TargetType="{x:Type TreeViewItem}">
…
<ControlTemplate.Triggers>
…
<Trigger Property="HasItems" Value="false">
<Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
</Trigger>
…
</ControlTemplate.Triggers>
</ControlTemplate>
The solution for this problem is to have the visibility of the expander of a particular key be controlled by the count of its subkeys. Since the subkeys are not loaded in memory when the parent key is created, you can not use the “Count” property of the “SubKeys” collection to retrieve this information (it is always 0). Fortunately, the “RegistryKey” class in the CLR contains a “SubKeyCount” property that we can use for this purpose. Here is the replacement for the XAML above:
<ControlTemplate TargetType="{x:Type TreeViewItem}">
…
<ControlTemplate.Triggers>
…
<DataTrigger Binding="{Binding Path=Key.SubKeyCount}" Value="0">
<Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
</DataTrigger>
…
</ControlTemplate.Triggers>
</ControlTemplate>
And finally, here is the XAML I used to declare the TreeView:
<TreeView ItemsSource="{Binding Path=RootKeys}"
…
TreeViewItem.Collapsed="ItemCollapsedOrExpanded2"
TreeViewItem.Expanded="ItemCollapsedOrExpanded2">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:RegistryKeyHolder2}" ItemsSource="{Binding Path=SubKeys}">
<TextBlock Text="{Binding Path=ShortName}" />
</HierarchicalDataTemplate>
… Here I added the default styles and templates for TreeView that I copied from Blend, modified with the DataTrigger above…
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<StaticResourceExtension ResourceKey="tvStyle1" />
</TreeView.ItemContainerStyle>
</TreeView>
At the beginning of this post, I mentioned that the solution I presented here avoids keeping elements in memory after they’ve been collapsed and partially fixes data virtualization. Is this really the case?
Visuals no longer stay in memory when collapsed
The fact that I’m discarding the data items when a TreeViewItem is collapsed causes the UIElement associated with those items to also be discarded. You can confirm this by looking at the visual count that I added to the sample, after expanding and collapsing the first item:
The visual count of the TreeView starts out as 49. After expanding the first item, the count increases to 169, and after collapsing it, the count becomes 52. There’s a difference in the number of visuals before and after for two reasons:
- The StackPanel that wraps the subkey items stays in memory.
- After the vertical scroll bar becomes interactive and then disabled again, two visuals are left behind. I tried to minimize the difference of visuals caused by the scroll bar by making it present (but disabled) from the beginning.
I would like you to focus on the big numbers, and not the details. With this solution, after collapsing a TreeViewItem, almost all of the visuals created when the TreeViewItem was expanded are not kept in memory any longer.
Some of the data is virtualized
The initial count of data items for this TreeView scenario is 2. Once the first item is expanded that number goes up to 15, but when you collapse that item, the number of data items goes back down to 2.
Note that I am by no means claiming to provide a full solution to data virtualization. My solution only listens to expand and collapse events. True data virtualization for TreeView would take into account not only expanding and collapsing, but also scrolling events. Imagine a scenario where you have many TreeViewItems expanded so that the total number of items expanded in the TreeView is a thousand (not all of them visible, of course). In this case, my solution will keep those thousand items in memory, while a true data virtualization solution would only keep in memory the few items displayed on the screen. As the user scrolls the TreeView, data virtualization would figure out which items should be swapped in and out of memory.
However, I believe that the solution here offers the biggest bang for the buck. It’s extremely simple to implement and it helps with a very common usage scenario for TreeViews: TreeViews can be bound to extremely large data sets, but users typically only have a small subset of that data expanded at a particular time.
Here you can find the VS project with this sample code. This works with Orcas Beta2 bits.
Duncan Millard
Hi Beatriz,
My question isn’t directly related to the post but instead is a problem with data validation that’s been asked on the MSDN forums but as far as I know not resolved yet.
A good description is at: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2079199&SiteID=1
The simple case is, I want to start with an empty data-entry form. Some fields are mandatory. If I simply submit my data, the controls will be considered valid, because there has been no update of the underlying data source and hence no validation rules fired.
It would be great to have a good solution to this - and preferably to take this further and be able to do validation on non-bound controls, rather like in ASP.NET.
I hope this feedback is useful to the WPF team!
thanks,
Duncan Millard
http://geekswithblogs.net/dmillard
September 13, 2007 at 8:24 am
Bea
Hi Duncan,
I’m not sure that I completely understand your scenario. If you start with an empty data entry form, submit the data, and your validation rules fail for null/empty string values, your controls should now be invalid. Validation should fire every time your data is submitted to the source. You can find here a simple project I put together hoping to repro the behavior you describe. I would like to understand your expectations better because we know we have some rough edges in validation, and the more concrete feedback we get about it, the easier it is for us to fix it.
We already have an internal work item tracking adding support for validation on non-bound controls. The priority of that work will grealy depend on how much customers ask for it. I would personally love to see this feature get implemented!
Thanks,
Bea
September 18, 2007 at 5:22 pm
Ivan
Hi Bea,
You have explained very well in your blog about TreeView (It is cool, thank you!). But how about the Menu… do I need to take some actions if I want to display large hierarchical information using Menu? My question probably seems evident for all, but I’m not a WPF expert.
Also, I have found impossible to use Snoop for browsing internals of Menu… First, it seems like menu is not in the visual tree of window. Second, it is hard (or very inconvenient) to do something with Menu, because it is closed each time when i activate another window. Probably you could recommend some technique of using Snoop or some other tool.
Thanks,
Ivan
October 16, 2007 at 2:24 pm
Bea
Hi Ivan,
Here are some thoughts on Menu:
- First of all, I haven’t seen people use Menu to display large amounts of information. Is this the right control for what you’re trying to do? What is your scenario? How many items and layers do you have?
- Yeah, I’ve come across the same issue with debugging MenuItems as you mention. I opened the Menu, clicked on Snoop, and the Menu closed. I don’t know of any other tool or way to solve this problem. I’m not sure if there’s anything that WPF can do, since this is the real behavior of Menu. I’ll let Pete (the guy who wrote Snoop) know about this - maybe he can do something in Snoop to improve the experience.
- And last, replying to your question, I think it will be really hard for you to implement the third solution in my series with Menu (the one with full UI virtualization and discarding data on collapse). For TreeView it was quite easy because the layout of a TreeView is very similar to the layout of a ListBox. Menu, however, is very different, so I don’t think it would be possible to leverage the full UI virtualization feature from ListBox. The second solution (the one that discards data on collapse) should be possible, by following similar steps. You may need to copy and change the default template for MenuItem instead of TreeViewItem, and you will have to find different events that will trigger when you attempt to open and close the menu. This will probably not be as straight forward as the TreeView sample, but it should be possible.
Thanks for your comment,
Bea
October 30, 2007 at 11:20 pm