带列的 TreeView 速度慢

TreeView with Columns slow

我想在我的 WPF 应用程序中显示按列拆分的大型层次结构。我的意图是使用带列的 TreeView 来实现。

对于少量数据,控件工作正常。滚动流畅,加载时间也可以接受(<1s)。但是随着大量数据滚动开始挂起并且不再流畅并且加载需要几秒钟。

这里是数据结构的代码:

public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public List<Contact> Contacts { get; set; }
}


public class Contact:Person
{

    public bool Test { get; set; }
}

这里是使用转换器的代码:

public class LevelToIndentConverter : IValueConverter
{
    private static readonly LevelToIndentConverter DefaultInstance = new LevelToIndentConverter();

    public static LevelToIndentConverter Default
    {
        get { return DefaultInstance; }
    }

    private const double IndentSize = 20.0;

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return new Thickness((int)value * IndentSize, 0, 0, 0);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

TreeListView代码:

public class TreeListView : TreeView
{
    public static readonly DependencyProperty ColumnsProperty =
        DependencyProperty.Register("Columns", typeof(GridViewColumnCollection), typeof(TreeListView), new FrameworkPropertyMetadata(new GridViewColumnCollection(), FrameworkPropertyMetadataOptions.None));

    public GridViewColumnCollection Columns
    {
        get { return (GridViewColumnCollection)GetValue(ColumnsProperty); }
        set { SetValue(ColumnsProperty, value); }
    }

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

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

    public TreeListView()
    {
        DefaultStyleKey = typeof(TreeListView);
        Columns = new GridViewColumnCollection();
    }
}

public class TreeListViewItem : TreeViewItem
{

    private int level = -1;

    public int Level
    {
        get
        {
            if (level != -1) { return level; }

            var parent = ItemsControlFromItemContainer(this) as TreeListViewItem;
            level = (parent != null) ? parent.Level + 1 : 0;
            return level;
        }
    }

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

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

    public TreeListViewItem()
    {
        DefaultStyleKey = typeof(TreeListViewItem);
    }

MainWindow.xaml

<Window x:Class="WpfApplication4.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="clr-namespace:WpfApplication4"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <Style x:Key="ToggleButton.TreeExpander" TargetType="{x:Type ToggleButton}">
        <Setter Property="Margin" Value="{Binding Level, RelativeSource={RelativeSource AncestorType={x:Type controls:TreeListViewItem}}, Converter={x:Static controls:LevelToIndentConverter.Default}}"/>
        <Setter Property="IsChecked" Value="{Binding Path=IsExpanded, RelativeSource={RelativeSource AncestorType= {x:Type controls:TreeListViewItem}}}"/>
        <Setter Property="Focusable" Value="False"/>
        <Setter Property="Padding" Value="0,2"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ToggleButton}">
                    <Border Padding="{TemplateBinding Padding}">
                        <Grid Background="Transparent" SnapsToDevicePixels="False">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>

                            <Grid x:Name="ArrowGrid" Grid.Column="0" Width="20">
                                <ContentControl x:Name="Up_Arrow" VerticalAlignment="Center" HorizontalAlignment="Center" Focusable="False">
                                    <TextBlock Text="-"/>
                                </ContentControl>
                                <ContentControl x:Name="Down_Arrow" VerticalAlignment="Center" HorizontalAlignment="Center" Visibility="Collapsed" Focusable="False">
                                    <TextBlock Text="+"></TextBlock>
                                </ContentControl>
                            </Grid>

                            <ContentPresenter Grid.Column="1" HorizontalAlignment="Stretch" RecognizesAccessKey="True" SnapsToDevicePixels="True" VerticalAlignment="Center" />
                        </Grid>
                    </Border>
                    <ControlTemplate.Triggers>
                        <DataTrigger Binding="{Binding Path=HasItems,RelativeSource={RelativeSource AncestorType={x:Type controls:TreeListViewItem}}}" Value="False">
                            <Setter TargetName="ArrowGrid" Property="Visibility" Value="Hidden"/>
                            <Setter Property="IsHitTestVisible" Value="False"/>
                        </DataTrigger>
                        <Trigger Property="IsChecked" Value="true">
                            <Setter TargetName="Down_Arrow" Property="Visibility" Value="Visible" />
                            <Setter TargetName="Up_Arrow" Property="Visibility" Value="Collapsed" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style x:Key="TreeListViewScrollViewerStyle" TargetType="{x:Type ScrollViewer}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ScrollViewer">
                    <Grid Background="{TemplateBinding Background}" >
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="Auto" />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>

                        <DockPanel Margin="{TemplateBinding Padding}">
                            <ScrollViewer DockPanel.Dock="Top" Focusable="false"
                                      HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">

                                <GridViewHeaderRowPresenter Columns="{Binding Path=TemplatedParent.Columns, RelativeSource={RelativeSource TemplatedParent}}"
                                                        ColumnHeaderContainerStyle="{Binding Path=TemplatedParent.ColumnHeaderContainerStyle, RelativeSource={RelativeSource TemplatedParent}}"
                                                        ColumnHeaderTemplate="{Binding Path=TemplatedParent.ColumnHeaderTemplate, RelativeSource={RelativeSource TemplatedParent}}"
                                                        ColumnHeaderTemplateSelector="{Binding Path=TemplatedParent.ColumnHeaderTemplateSelector, RelativeSource={RelativeSource TemplatedParent}}"
                                                        AllowsColumnReorder="{Binding Path=TemplatedParent.AllowsColumnReorder, RelativeSource={RelativeSource TemplatedParent}}"
                                                        ColumnHeaderContextMenu="{Binding Path=TemplatedParent.ColumnHeaderContextMenu, RelativeSource={RelativeSource TemplatedParent}}"
                                                        ColumnHeaderToolTip="{Binding Path=TemplatedParent.ColumnHeaderToolTip, RelativeSource={RelativeSource TemplatedParent}}"
                                                        SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                            </ScrollViewer>

                            <ScrollContentPresenter Name="PART_ScrollContentPresenter"
                                                KeyboardNavigation.DirectionalNavigation="Local"
                                                CanContentScroll="{TemplateBinding CanContentScroll}"
                                                CanHorizontallyScroll="False"
                                                CanVerticallyScroll="False" />
                        </DockPanel>


                        <ScrollBar Name="PART_HorizontalScrollBar"
                                Orientation="Horizontal"
                                BorderThickness="0,1,0,0"
                                Grid.Row="1" Grid.Column="0"
                                Maximum="{TemplateBinding ScrollableWidth}"
                                ViewportSize="{TemplateBinding ViewportWidth}"
                                Value="{TemplateBinding HorizontalOffset}"
                                Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" />

                        <ScrollBar Name="PART_VerticalScrollBar"
                                Grid.Column="1" Grid.Row="0"
                                BorderThickness="1,0,0,0"
                                Maximum="{TemplateBinding ScrollableHeight}"
                                ViewportSize="{TemplateBinding ViewportHeight}"
                                Value="{TemplateBinding VerticalOffset}"
                                Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" />

                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style x:Key="TreeListView.Base" TargetType="{x:Type controls:TreeListView}" >
        <Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
        <Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
        <Setter Property="VirtualizingPanel.ScrollUnit" Value="Item" />
        <Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
        <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
        <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel/>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
                        <ScrollViewer Style="{StaticResource TreeListViewScrollViewerStyle}">
                            <ItemsPresenter x:Name="ItemsPresenter"/>
                        </ScrollViewer>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style x:Key="TreeListViewItem.Base" TargetType="{x:Type controls:TreeListViewItem}" >
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="BorderThickness" Value="0,0,0,1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:TreeListViewItem}">
                    <VirtualizingStackPanel>
                        <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 Columns, RelativeSource={RelativeSource FindAncestor, AncestorType=controls:TreeListView}}" />
                        </Border>
                        <ItemsPresenter x:Name="ItemsHost" />
                    </VirtualizingStackPanel>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsExpanded" Value="false">
                            <Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed"/>
                        </Trigger>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter Property="Background" Value="Blue" />
                            <Setter Property="Foreground" Value="White" />
                        </Trigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="HasHeader" Value="false"/>
                                <Condition Property="Width" Value="Auto"/>
                            </MultiTrigger.Conditions>
                            <Setter TargetName="PART_Header" Property="MinWidth" Value="75"/>
                        </MultiTrigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsSelected" Value="True"/>
                                <Condition Property="IsSelectionActive" Value="false"/>
                            </MultiTrigger.Conditions>
                            <Setter Property="Background" Value="Gray"/>
                            <Setter Property="Foreground" Value="White"/>
                        </MultiTrigger>
                        <Trigger Property="IsEnabled" Value="false">
                            <Setter Property="Opacity" Value="0.5" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style BasedOn="{StaticResource TreeListView.Base}" TargetType="{x:Type controls:TreeListView}" />
    <Style BasedOn="{StaticResource TreeListViewItem.Base}" TargetType="{x:Type controls:TreeListViewItem}" />
</Window.Resources>
<Grid>
    <controls:TreeListView HorizontalAlignment="Stretch" ItemsSource="{Binding}" >
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate  ItemsSource="{Binding Contacts}" />
        </TreeView.ItemTemplate>

        <controls:TreeListView.Columns>
            <GridViewColumn Header="First Name" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <DataTemplate.Resources>

                            <DataTemplate DataType="{x:Type controls:Person}">
                                <TextBlock Text="{Binding FirstName}"/>
                            </DataTemplate>

                            <DataTemplate DataType="{x:Type controls:Contact}">
                                <TextBlock Text="{Binding FirstName}"/>
                            </DataTemplate>
                        </DataTemplate.Resources>

                        <ToggleButton Content="{Binding}" Style="{StaticResource ToggleButton.TreeExpander}"/>

                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>

            <GridViewColumn Header="Last Name" Width="300">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <DataTemplate.Resources>
                            <DataTemplate DataType="{x:Type controls:Person}">
                                <TextBlock Text="{Binding LastName}"/>
                            </DataTemplate>

                            <DataTemplate DataType="{x:Type controls:Contact}">
                                <TextBlock Text="{Binding LastName}"/>
                            </DataTemplate>
                        </DataTemplate.Resources>

                        <ContentPresenter Content="{Binding}" VerticalAlignment="Center"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
        </controls:TreeListView.Columns>
    </controls:TreeListView>
</Grid>

MainWindow.xaml.cs

    public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        var persons = new List<Person>();

        for (int i = 0; i < 1000; i++)
        {
            var person = new Person
            {
                FirstName = string.Format("{0} {1}", "Hans", i),
                LastName = "Wurst",
                Contacts = new List<Contact>()
            };

            for (int j = 0; j < 100; j++)
            {
                person.Contacts.Add(new Contact { FirstName = string.Format("{0} {1}", "Contact", j), LastName = "-" });
            }

            persons.Add(person);    
        }

        this.DataContext = persons;
    }
}

有人知道为什么这个观点滞后吗?我尝试启用虚拟化来提高性能,但它并没有解决问题。

到目前为止我发现它似乎与GridViewRowPresenter有关。当我删除它时一切正常。

欢迎任何帮助!

非常感谢, 斯特凡

更新

已为 TreeListView 激活虚拟化。参见样式 I TreeListView.Base。样式应用在 TreeListView by line

<Style BasedOn="{StaticResource TreeListView.Base}" TargetType="{x:Type controls:TreeListView}" />

。就我可以使用 WPF snoop 进行检查而言,虚拟化工作正常。

更新 2

我已经使用此处发布的代码在 GitHub 上创建了一个示例应用程序。你可以找到它here on GitHub

它还包含一个 TreeListView 样式的实现,没有 GridRows 来说明即使加载了所有子项,一切都可以正常工作。

VirtualizingStackPanel.VirtualizationModeVirtualizingStackPanel.IsVirtualizing 可以显着减少内存消耗并显着提高性能。请查看以下代码片段:

<TreeView x:Name=”myTreeView”

      VirtualizingStackPanel.IsVirtualizing="True"

      VirtualizingStackPanel.VirtualizationMode="Recycling" />

更新:

也许有帮助,但尝试不在 Style 中设置属性,而是在您的自定义控件中设置属性 TreeListView:

<controls:TreeListView HorizontalAlignment="Stretch" ItemsSource="{Binding}" 
          VirtualizingStackPanel.IsVirtualizing="True"
          VirtualizingStackPanel.VirtualizationMode="Recycling"   >
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate  ItemsSource="{Binding Contacts}" />
    </TreeView.ItemTemplate>
 </controls:TreeListView>

尝试在用户展开 TreeView 时加载数据。通常,用户不需要同时在屏幕上打开 50,000 个节点。仅加载用户需要查看的子项,以及您需要正确显示的任何子项信息。

更新 1:

看起来 GridViewRowPresenter 是您的性能问题的原因。我已经使用了 ContentPresenter 并且性能变得与您的 FastTreeView 中一样,但是 Header 对他们的列不正确。你应该更正它

<Style x:Key="TreeListViewItem.Base" TargetType="{x:Type controls:TreeListViewItem}" >
...
<VirtualizingStackPanel>
   <Border Name="Bd" Background="{TemplateBinding Background}" VirtualizingPanel.IsVirtualizing="True" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">                                  
       <!--<GridViewRowPresenter x:Name="PART_Header" Content="{TemplateBinding Header}" Columns="{Binding Columns, RelativeSource={RelativeSource FindAncestor, AncestorType=controls:TreeListView}}" />-->
        <ContentPresenter x:Name="PART_Header" Content="{Binding}" Grid.Column="1">
          <ContentPresenter.Resources>
             <DataTemplate DataType="{x:Type controls:Person}">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding FirstName}" Margin="0,0,10,0"/>
                    <TextBlock Text="{Binding LastName}" Margin="10,0,0,0"/>
                 </StackPanel>                                            
             </DataTemplate>

             <DataTemplate DataType="{x:Type controls:Contact}">
               <StackPanel Orientation="Horizontal">
                  <TextBlock Text="{Binding FirstName}"/>
                  <TextBlock Text="{Binding LastName}"/>
              </StackPanel>
             </DataTemplate>
            </ContentPresenter.Resources>
         </ContentPresenter>                               
        </Border>
       <ItemsPresenter x:Name="ItemsHost" />
    </VirtualizingStackPanel>

我已经解决了这个问题。

看来性能问题的根本原因是GridViewRowPresenter没有设置固定高度。

当我在 TreeListViewItem.Base 样式的 GridViewRowPresenter 上设置高度时,一切都非常有效:

<GridViewRowPresenter x:Name="PART_Header" Height="20" Content="{TemplateBinding Header}" Columns="{Binding Columns, RelativeSource={RelativeSource FindAncestor, AncestorType=controls:TreeListView}}" />

感谢您的帮助。