0

I made a custom TreeView with multiple columns. Everything worked well until thee are lots of items in the tree.

I tried to enable Virtualization by doing VirtualizingPanel.IsVirtualizing="True" (Will be VirtualizingStackPanel.IsVirtualizing if you are < .NET 4.5) but not only it does not speed it up, it actually made the load time even worse.
On a normal TreeView this property does the trick but I can't find a way to get it to work on my custom Tree

TreeViewItem.cs

public class TreeListViewItem : TreeViewItem
{
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new TreeListViewItem();
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return item is TreeListViewItem;
    }
}

TreeListView.cs

public class TreeListView : TreeView
{
    public GridViewColumnCollection Columns { get; set; }
    public TreeListView()
    {
        Columns = new GridViewColumnCollection();
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new TreeListViewItem();
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return item is TreeListViewItem;
    }
} 

Node.cs

public class Node
{
    public string Data { get; set; }
    public List<Node> Children { get; set; }
    public Node(string data)
    {
        Data = data;
        Children = new List<Node>();
    }
}

ViewModel.cs

public class ViewModel
{
    public ObservableCollection<Node> Nodes { get; private set; }
    public ViewModel()
    {
        Nodes = new ObservableCollection<Node>();
        Node parent = new Node("Parent");

        for (int i = 0; i < 5000; i++)
            parent.Children.Add(new Node(i.ToString()));

        Nodes.Add(parent);
    }
}

MainWindow.xaml

<Window x:Class="WPFTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFTest"
        Title="Test"
        mc:Ignorable="d"
        Width="200">
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <Style TargetType="local:TreeListView">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:TreeListView">
                        <Border BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                            <ScrollViewer VerticalScrollBarVisibility="Disabled">
                                <DockPanel>
                                    <GridViewHeaderRowPresenter Columns="{Binding Path=Columns, RelativeSource={RelativeSource TemplatedParent}}"
                                                        DockPanel.Dock="Top"/>
                                    <ScrollViewer HorizontalScrollBarVisibility="Disabled" 
                                          VerticalScrollBarVisibility="Auto" 
                                          DockPanel.Dock="Bottom">
                                        <ItemsPresenter/>
                                    </ScrollViewer>
                                </DockPanel>
                            </ScrollViewer>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="local:TreeListViewItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:TreeListViewItem">
                        <StackPanel>
                            <Border Name="Bd"
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Padding="{TemplateBinding Padding}">
                                <GridViewRowPresenter x:Name="PART_Header" 
                                                  Content="{TemplateBinding Header}" 
                                                  Columns="{Binding Path=Columns, RelativeSource={RelativeSource AncestorType={x:Type local:TreeListView}}}" >
                                </GridViewRowPresenter>
                            </Border>
                            <ItemsPresenter x:Name="ItemsHost" />
                        </StackPanel>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsExpanded" Value="false">
                                <Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <local:TreeListView ItemsSource="{Binding Nodes}" VirtualizingPanel.IsVirtualizing="True">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}"/>
            </TreeView.ItemTemplate>
            <local:TreeListView.Columns>
                <GridViewColumn Header="Test" Width="150">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <DockPanel>
                                <TextBlock Text="{Binding Data}"/>
                            </DockPanel>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
            </local:TreeListView.Columns>
        </local:TreeListView>
    </Grid>
</Window>

I tried templating the ItemPanel to be VirtualizingStackPanel and it did not help either.

I strip out the expander part since it is not relevant. You can double click on the parent node to expand the tree and it will take a long time to load the children.

Steve
  • 10,544
  • 6
  • 29
  • 66

1 Answers1

1

On the TreeListView style, on the ItemsPresenter's parent ScrollViewer, set CanContentScroll="True":

<ScrollViewer CanContentScroll="True"
    HorizontalScrollBarVisibility="Disabled"
    VerticalScrollBarVisibility="Auto" 
    DockPanel.Dock="Bottom">
    <ItemsPresenter/>
</ScrollViewer>

On the TreeListViewItem style, you need to have something named "Expander" (for some reason unknown to me - perhaps some style/code is looking for it?). Just put a ToggleButton in the style's StackPanel:

<StackPanel>
    <ToggleButton x:Name="Expander" Width="0" />
    <Border Name="Bd" ... />
    ....
</StackPanel>

Here is the complete XAML:

<Window x:Class="WpfApplication88.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication88"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <Style TargetType="local:TreeListView">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:TreeListView">
                        <Border BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                            <ScrollViewer VerticalScrollBarVisibility="Disabled">
                                <DockPanel>
                                    <GridViewHeaderRowPresenter Columns="{Binding Path=Columns, RelativeSource={RelativeSource TemplatedParent}}"
                                                    DockPanel.Dock="Top"/>
                                    <ScrollViewer CanContentScroll="True"
                                        HorizontalScrollBarVisibility="Disabled"
                                        VerticalScrollBarVisibility="Auto" 
                                        DockPanel.Dock="Bottom">
                                        <ItemsPresenter/>
                                    </ScrollViewer>
                                </DockPanel>
                            </ScrollViewer>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

       <Style TargetType="local:TreeListViewItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:TreeListViewItem">
                        <StackPanel>
                            <ToggleButton x:Name="Expander" Width="0" />
                            <Border Name="Bd"
                                Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Padding="{TemplateBinding Padding}">
                                <GridViewRowPresenter x:Name="PART_Header" 
                                                  Content="{TemplateBinding Header}" 
                                                  Columns="{Binding Path=Columns, RelativeSource={RelativeSource AncestorType={x:Type local:TreeListView}}}" >
                                </GridViewRowPresenter>
                            </Border>
                            <ItemsPresenter x:Name="ItemsHost" />
                        </StackPanel>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsExpanded" Value="false">
                                <Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <local:TreeListView ItemsSource="{Binding Nodes}" VirtualizingPanel.IsVirtualizing="True">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}"/>
            </TreeView.ItemTemplate>
            <local:TreeListView.Columns>
                <GridViewColumn Header="Test" Width="150">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <DockPanel>
                                <TextBlock Text="{Binding Data}"/>
                            </DockPanel>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn Header="Test 2" Width="150">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <DockPanel>
                                <TextBlock Text="{Binding Data2}"/>
                            </DockPanel>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
            </local:TreeListView.Columns>
        </local:TreeListView>
    </Grid>
</Window>

I added a Test2 DataViewColum and added a Data2 property to the Node class to make sure it worked (and it does). Here are the changes to the code:

public class ViewModel
{
    public ObservableCollection<Node> Nodes { get; private set; }
    public ViewModel()
    {
        Nodes = new ObservableCollection<Node>();
        Node parent = new Node("Parent", "Parent2");

        for (int i = 0; i < 5000; i++)
            parent.Children.Add(new Node(i.ToString(), (i * i).ToString()));

        Nodes.Add(parent);
    }
}

public class Node
{
    public string Data { get; set; }
    public string Data2 { get; set; }

    public List<Node> Children { get; set; }
    public Node(string data, string data2)
    {
        Data = data;
        Data2 = data2;
        Children = new List<Node>();
    }
}

FYI - The template that VS made for me (while I was figuring this out), put the CanContentScroll="True" into a setter on a trigger for VirtualizingPanel.IsVirtualizing when it is True. Something like this:

<Style TargetType="local:TreeListView">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:TreeListView">
                <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                    <DockPanel>
                        <GridViewHeaderRowPresenter Columns="{Binding Path=Columns, RelativeSource={RelativeSource TemplatedParent}}"
                                            DockPanel.Dock="Top"/>
                        <ScrollViewer x:Name="_tv_scrollviewer_" HorizontalScrollBarVisibility="Disabled" 
                                VerticalScrollBarVisibility="Auto" 
                                DockPanel.Dock="Bottom">
                            <ItemsPresenter/>
                        </ScrollViewer>
                    </DockPanel>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="VirtualizingPanel.IsVirtualizing" Value="True">
                        <Setter Property="CanContentScroll" TargetName="_tv_scrollviewer_" Value="True"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
J.H.
  • 4,052
  • 1
  • 14
  • 14
  • would never thought of CanContentScroll could possibly have an effect on this. Life saver – Steve Feb 22 '17 at 17:17