Виртуализация для пользовательского дерева

Я сделал собственный TreeView с несколькими столбцами. Все работало хорошо, пока на дереве не появилось много предметов.

Я попытался включить виртуализацию, выполнив VirtualizingPanel.IsVirtualizing="True" (Будет VirtualizingStackPanel.IsVirtualizing если вы <.NET 4.5), но это не только не ускоряет его, но даже ухудшает время загрузки.
В обычном TreeView это свойство работает, но я не могу найти способ заставить его работать на моем собственном дереве

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 и это тоже не помогло.

Я раздеваю часть детандера, так как она не актуальна. Вы можете дважды щелкнуть по родительскому узлу, чтобы развернуть дерево, и загрузка дочерних элементов займет много времени.

1 ответ

Решение

В стиле TreeListView, в родительском ScrollViewer ItemsPresenter, установите CanContentScroll="True":

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

В стиле TreeListViewItem у вас должно быть что-то с именем "Expander" (по какой-то неизвестной мне причине - возможно, какой-то стиль / код ищет это?). Просто поместите ToggleButton в StackPanel стиля:

<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 и добавил свойство Data2 в класс Node, чтобы убедиться, что он работает (и работает). Вот изменения в коде:

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, когда он равен True. Что-то вроде этого:

<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>
Другие вопросы по тегам