自定义树视图的虚拟化

virtualization for custom treeview

我制作了一个包含多列的自定义 TreeView。一切都很好,直到树中有很多项目。

我尝试通过 VirtualizingPanel.IsVirtualizing="True" 启用虚拟化(如果您是 < .NET 4.5,将是 VirtualizingStackPanel.IsVirtualizing)但它不仅没有加速,它实际上使加载时间均匀更糟。
在普通的 TreeView 上,这个 属性 可以解决问题,但我找不到让它在我的自定义 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>

我尝试将 ItemPanel 模板化为 VirtualizingStackPanel,但也无济于事。

我删除了扩展器部分,因为它不相关。您可以双击父节点展开树,加载子节点需要很长时间。

在 TreeListView 样式上,在 ItemsPresenter 的父 ScrollViewer 上,设置 CanContentScroll="True":

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

在 TreeListViewItem 样式中,您需要有一个名为 "Expander" 的东西(出于某种我不知道的原因 - 也许某些 style/code 正在寻找它?)。 只需在样式的 StackPanel 中放置一个 ToggleButton:

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

这是完整的 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>

我添加了一个 Test2 DataViewColum 并向节点 class 添加了一个 Data2 属性 以确保它有效(确实有效)。以下是对代码的更改:

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>();
    }
}

仅供参考 - VS 为我制作的模板(当我正在弄清楚这一点时),将 CanContentScroll="True" 放入 VirtualizingPanel.IsVirtualizing 触发器的 setter 中是真的。像这样:

<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>