Виртуализация для пользовательского дерева
Я сделал собственный 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>