Share via


WPF: Custom Virtualizing Panel For TreeView

I realized this month in the questions where OPs weren't happy with scrolling there was usually the TreeView control somewhere in the background and OPs had a large amount of data wished to be displayed.

Yes, the scrolling part in TreeView with UI Virtualization enabled could have been done better/faster by Microsoft but to be honest it's not freezing the UI that bad... I am probably right now making some of you pretty mad on me. Performance is a sensitive topic. However the TreeView works the way it works... Anyways I would like to show you in this article how to make a custom virtualizing panel for TreeView and how to use it instead of VirtualizingStackPanel. Some of you might think right now why diving this deep into WPF? To answer your question guys I wanted to show you that by creating a custom panel you can make TreeView virtualize, layout and scroll faster. The panel is the heart. Its the panel that you need to change to make the TreeView run smoother.

Note*: This article will require more than just basic WPF knowledge.*

Here are few facts about the TreeView which you might not have known yet:

  • TreeView allows scrolling by pixels. Which means its default "jump" value once for example scrolled down is 16 pixels.
    ListBox on the other side scrolls by jumping over items. That is called logical scrolling. I actually met few people who told me they let their data being hosted in a TreeView even though the data is a simple list because scrolling by pixels looks nicer and smoother than scrolling by units (items).
  • TreeView in .NET 4.0 by default doesn't have UI Virtualization enabled. In order to achieve this you will have to set IsVirtualizing property to True.
  • TreeView is the only control in WPF that represents tree alike data structure with a tree alike visual structure. What I mean is VisualTree will be build up similar to your tree data structure. This is very important for routed events and bindings... TreeView does not flatten its nodes to a list and presents them with an indent in order to achieve "tree look".

Lets get started...

1. The data must be coming from somewhere

First we need a tree data structure which we want to be displayed in our custom virtualizing panel.

01.public class City
02.{
03.    public string CityName
04.    {
05.        get;
06.        set;
07.    }
08. 
09.    public List<Company> Companies
10.    {
11.        get;
12.        set;
13.    }
14.}
15. 
16.public class Company
17.{
18.    public string CompanyName
19.    {
20.        get;
21.        set;
22.    }
23. 
24.    public List<Person> Employee
25.    {
26.        get;
27.        set;
28.    }
29.}
30. 
31.public class Person
32.{
33.    public string Name
34.    {
35.        get;
36.        set;
37.    }
38.}

Very simple, right? Now we all now know that there are going to be three different data types building the tree.
But how many items will each node hold? To answer that question please take a look at following code:

01.public MainWindow()
02.{
03.    InitializeComponent();
04. 
05.    List<City> cities = new List<City>();
06.    for (int i = 0; i < 5000; i++)
07.    {
08.        cities.Add(new City() { CityName  = "City " + i,  Companies = new List<Company>() });
09.        for (int j = 0; j < 100; j++)
10.        {
11.            cities[i].Companies.Add(new Company() { CompanyName  = "Company " + j, Employee =  new List<Person>() });
12.            for (int k = 0; k < 10; k++)
13.            {
14.                cities[i].Companies[j].Employee.Add(new Person() { Name = "Name " + k });
15.            }
16.        }
17.    }
18. 
19.    MessageBox.Show("Data loaded!");
20.    this.DataContext = cities;
21.}

I can't claim I was super creative when it comes to giving names to cities and companies but most important part here is that we will fill our tree with 5000 items and each of them will support second and third level.

2. Lets design something in XAML

Now that we have our data loaded in memory and set to DataContext we should move on to XAML part of the game.

01.<Grid>
02.    <local:MyTreeView ItemsSource="{Binding}" >
03.        <local:MyTreeView.ItemTemplate>
04.            <HierarchicalDataTemplate ItemsSource="{Binding Companies}">
05.                <HierarchicalDataTemplate.ItemTemplate>
06.                    <HierarchicalDataTemplate ItemsSource="{Binding Employee}">
07.                        <HierarchicalDataTemplate.ItemTemplate>
08.                            <DataTemplate>
09.                                <TextBlock Text="{Binding Name}"/>
10.                            </DataTemplate>
11.                        </HierarchicalDataTemplate.ItemTemplate>
12.                        <TextBlock Text="{Binding CompanyName}"/>
13.                    </HierarchicalDataTemplate>
14.                </HierarchicalDataTemplate.ItemTemplate>
15.                <TextBlock Text="{Binding CityName}"/>
16.            </HierarchicalDataTemplate>
17.        </local:MyTreeView.ItemTemplate>
18.    </local:MyTreeView>
19.</Grid>

I will take the freedom to skip the part explaining what HierarchicalDataTemplates are or how they are being used in wpf. Still here are links if you wish to read more about them:

http://msdn.microsoft.com/en-us/library/system.windows.hierarchicaldatatemplate(v=vs.110).aspx

http://msdn.microsoft.com/en-us/library/dd759035(v=vs.95).aspx

You might have noticed the MyTreeView tags and you are probably already thinking that I started from scratch creating completely a new TreeView. To answer your question, no I didn't. I have not created something completely new here.

3. Some styles and resources...

I created MyTreeView and MyTreeViewItem which futhermore inherits from ItemsControl and TreeViewItem in order to be able to have custom styles for those types. I like to have styles defined in a separate resource dictionary instead of stuffing everything inside Window.Resources.

01.class MyTreeView : ItemsControl
02.{
03.    static MyTreeView()
04.    {
05.        DefaultStyleKeyProperty.OverrideMetadata(typeof(MyTreeView), new FrameworkPropertyMetadata(typeof(MyTreeView)));
06.    }
07. 
08.    protected override DependencyObject GetContainerForItemOverride()
09.    {
10.        return new MyTreeViewItem();
11.    }
12.}
13. 
14.class MyTreeViewItem : TreeViewItem
15.{
16.    static MyTreeViewItem()
17.    {
18.        DefaultStyleKeyProperty.OverrideMetadata(typeof(MyTreeViewItem), new FrameworkPropertyMetadata(typeof(MyTreeViewItem)));
19.    }
20.}
01.<Style TargetType="{x:Type local:MyTreeView}">
02.    <Setter Property="ItemsPanel">
03.        <Setter.Value>
04.            <ItemsPanelTemplate>
05.                <local:MyVirtualizingPanel/>
06.            </ItemsPanelTemplate>
07.        </Setter.Value>
08.    </Setter>
09.    <Setter Property="Template">
10.        <Setter.Value>
11.            <ControlTemplate TargetType="{x:Type local:MyTreeView}">
12.                <ScrollViewer ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto"
13.                                  CanContentScroll="True">
14.                    <ItemsPresenter/>
15.                </ScrollViewer>
16.            </ControlTemplate>
17.        </Setter.Value>
18.    </Setter>
19.</Style>

As you can see below or above everything is pretty  simple so far. 

01.<Style TargetType="{x:Type local:MyTreeViewItem}">
02.    <Setter Property="ItemsPanel">
03.        <Setter.Value>
04.            <ItemsPanelTemplate>
05.                <local:MyVirtualizingPanel/>
06.            </ItemsPanelTemplate>
07.        </Setter.Value>
08.    </Setter>
09.    <Setter Property="Template">
10.        <Setter.Value>
11.            <ControlTemplate TargetType="{x:Type local:MyTreeViewItem}">
12.                <Grid>
13.                    <Grid.RowDefinitions>
14.                        <RowDefinition Height="16" />
15.                        <RowDefinition Height="*" />
16.                    </Grid.RowDefinitions>
17.                    <Grid.ColumnDefinitions>
18.                        <ColumnDefinition Width="26" />
19.                        <ColumnDefinition Width="*" />
20.                    </Grid.ColumnDefinitions>
21.                    <ToggleButton  IsChecked="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}, Path=IsExpanded, Mode=TwoWay}"
22.                                   Style="{StaticResource ExpandCollapseToggleStyle}"
23.                                   ClickMode="Press"/>
24.                    <Border Grid.Column="1"
25.                            Background="{TemplateBinding Background}"
26.                            BorderBrush="{TemplateBinding BorderBrush}"
27.                            BorderThickness="{TemplateBinding BorderThickness}">
28.                        <ContentPresenter ContentSource="Header" />
29.                    </Border>
30.                    <ItemsPresenter x:Name="ItemsPresenter" Grid.Column="1" Grid.Row="1" />
31.                </Grid>
32.                <ControlTemplate.Triggers>
33.                    <Trigger Property="IsExpanded" Value="False">
34.                        <Setter TargetName="ItemsPresenter" Property="Visibility" Value="Collapsed"/>
35.                    </Trigger>
36.                </ControlTemplate.Triggers>
37.            </ControlTemplate>
38.        </Setter.Value>
39.    </Setter>
40.</Style>

If you wish to see complete default TreeViewItem style I suggest you to take a look at following link:

http://msdn.microsoft.com/en-us/library/vstudio/ms788727(v=vs.90).aspx

4. The custom tree virtualizing panel..

Take a look at following code...

001.class MyVirtualizingPanel : VirtualizingPanel, IScrollInfo
002.{

  ....                ....


010.    /// <summary>
011.    /// Gets the virtualized height value.
012.    /// </summary>
013.    public static double GetVirtualizedHeight(DependencyObject obj)
014.    {
015.        return (double)obj.GetValue(VirtualizedHeightProperty);
016.    }
017. 
018.    /// <summary>
019.    /// Sets the virtualized height value.
020.    /// </summary>
021.    public static void SetVirtualizedHeight(DependencyObject obj, double value)
022.    {
023.        obj.SetValue(VirtualizedHeightProperty, value);
024.    }
025. 
026.    // Using a DependencyProperty as the backing store for VirtualizedHeight.  This enables animation, styling, binding, etc...
027.    public static readonly DependencyProperty VirtualizedHeightProperty =
028.        DependencyProperty.RegisterAttached("VirtualizedHeight", typeof(double), typeof(MyVirtualizingPanel), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.Inherits, OnVirtualizedHeightChanged));
029. 
030.    /// <summary>
031.    /// Notify underneath panels about offset
032.    /// </summary>  ...        ...

099.    /// <summary>
100.    /// Removes all those containers that are not visible to user.
101.    /// </summary>
102.    private void CleanUp(int startPos, int endPos)
103.    {
104.        IItemContainerGenerator generator = this.ItemContainerGenerator;
105.        for (int i = 0; i < this.InternalChildren.Count; i++)
106.        {
107.            UIElement element =  this.InternalChildren[i];
108.            int index = ((ItemContainerGenerator)generator).IndexFromContainer(element);
109.            if (index < startPos || index > endPos)
110.            {
111.                this.RemoveInternalChildRange(i, 1);
112.                generator.Remove(generator.GeneratorPositionFromIndex(index), 1);
113.                i--;
114.            }
115.        }
116.    }

 ...       ... 


139.    /// <summary>
140.    /// Provides only the visible containers to the users.

141.    /// </summary>
142.    protected override Size MeasureOverride(Size availableSize)
143.    {
144.        UIElementCollection children = this.InternalChildren;
145.        IItemContainerGenerator generator = this.ItemContainerGenerator;
146.            ...
150.        this.CleanUp(firstVisibleIndex, lastVisibleIndex);
151.            ...
152.            GeneratorPosition startGenPos = generator.GeneratorPositionFromIndex(firstVisibleIndex);
154.        using (generator.StartAt(startGenPos, GeneratorDirection.Forward, true))
155.        {
156.            for (int i = firstVisibleIndex; i < lastVisibleIndex; i++)
157.            {
158.                bool newlyRealized;
159.                UIElement child = (UIElement)generator.GenerateNext(out newlyRealized);
160. 
161.                if (!children.Contains(child))
162.                {
163.                    if (i - firstVisibleIndex >= children.Count)
164.                    {
165.                        this.AddInternalChild(child);
166.                    }
167.                    else if (children[i - firstVisibleIndex] != child)
168.                    {
169.                        this.InsertInternalChild(i - firstVisibleIndex, child);
170.                    }
171.                }
172. 
173.                generator.PrepareItemContainer(child);
174. 
175.                if (i == firstVisibleIndex)
176.                {
177.                    SetVirtualizedHeight(child, this.indent - 16.0 > 0.0 ? this.indent - 16.0 : 0.0);
178.                }
179. 
180.                child.Measure(new Size(double.PositiveInfinity, availableSize.Height - y > 0 ? availableSize.Height - y : 0.0));
181.                       ...
183.                x = Math.Max(x, child.DesiredSize.Width);
184.                y += child.DesiredSize.Height;
185.                if (y >= availableSize.Height)
186.                {

187.                    lastVisibleIndex = i;
188.                }
189.            }
190.        }
191. 
192.        this.CleanUp(firstVisibleIndex, lastVisibleIndex);
193.           ...
194.        if (this.IsScrolling)
195.        {
196.            Size computatedSize = new Size(x, estimatedHeight);
197.            this.viewport = new Size(x, availableSize.Height);
198.            this.extent.Width = this.viewport.Width;
199.            this.extent.Height = computatedSize.Height > this.viewport.Height ? computatedSize.Height : this.viewport.Height;
200.            this.ScrollOwner.InvalidateScrollInfo();
201.        }
202. 
203.        return new Size(x, estimatedHeight);
204.    }
205. 
206.    /// <summary>
207.    /// Arranges the visible children to user.
208.    /// </summary>
209.    protected override Size ArrangeOverride(Size finalSize)
210.    {
211.        UIElementCollection children = this.InternalChildren;
212.            ...
213.        for (int i = 0; i < children.Count; i++)
214.        {
215.            UIElement child = children[i];
216.            child.Arrange(new Rect(-this.HorizontalOffset, y, finalSize.Width + this.HorizontalOffset, child.DesiredSize.Height));
217.            y += child.DesiredSize.Height;
218.        }
219. 
220.        return finalSize;
221.    }
 ...      ...

 
I wanted to focus on MeasureOverride and ArrangeOverride methods therefore I didn't post the full code of IScrollInfo. Implementation of IScrollInfo interface is usually always the same anyway.

If you need to look up on IScrollInfo interface here is the link for you:

http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.iscrollinfo%28v=vs.110%29.aspx

I will skip explaining each step. Instead let me sum it up for you. 

  • MeasureOverride measures and provides visible containers. Those of you who don't know, it is common for controls which enable UI virtualization in WPF to do all the calculations of how many items and which items are gonna be displayed inside the MeasureOverride. As you can see in code when a container becomes invisible to user it will be removed by generator. 
  • ArrangeOverride serves for arranging the containers.

By the way to provide the scroll offset to inner children of nested MyVirtulizingPanels I used the tick with property value inheritance.

Feel free to contact me or leave comments in case you are writing such a panel for your own custom control or if you would like to share your opinion about this article. 

The code examples provided in this article are meant to be used for non commercial purposes. All rights are reserved. :)