具有虚拟化功能的WPF TreeView-选择并显示项目

时间:2018-09-18 02:27:18

标签: c# wpf treeview

最近我一直在使用WPF树视图,并且当用户使用在菜单上设置IsSelected属性的搜索功能时,试图在屏幕上显示所选项目真是太糟糕了。支持对象。

目前,我的方法是在此答案中使用该方法:https://stackoverflow.com/a/34620549/800318

    private void FocusTreeViewNode(TreeViewEntry node)
    {
        if (node == null) return;
        var nodes = (IEnumerable<TreeViewEntry>)LeftSide_TreeView.ItemsSource;
        if (nodes == null) return;

        var stack = new Stack<TreeViewEntry>();
        stack.Push(node);
        var parent = node.Parent;
        while (parent != null)
        {
            stack.Push(parent);
            parent = parent.Parent;
        }

        var generator = LeftSide_TreeView.ItemContainerGenerator;
        while (stack.Count > 0)
        {
            var dequeue = stack.Pop();
            LeftSide_TreeView.UpdateLayout();

            var treeViewItem = (TreeViewItem)generator.ContainerFromItem(dequeue);
            if (stack.Count > 0)
            {
                treeViewItem.IsExpanded = true;
            }
            else
            {
                if (treeViewItem == null)
                {
                    //This is being triggered when it shouldn't be
                    Debugger.Break();
                }
                treeViewItem.IsSelected = true;
            }
            treeViewItem.BringIntoView();
            generator = treeViewItem.ItemContainerGenerator;
        }
    }

TreeViewEntry是我的后备数据类型,它引用了其父节点。 Leftside_TreeView是绑定到我的对象列表的虚拟TreeView。关闭虚拟化不是一个选择,因为关闭它会导致性能下降。

当我搜索一个对象并找到支持数据的对象时,我以对象为参数调用此FocusTreeViewNode()方法。通常,它将在第一次调用时工作,选择对象并将其显示。

第二次执行搜索时,将传入要选择的节点,但是,当清空堆栈(因此它将尝试为对象本身生成容器)时,ContainerFromItem()调用将返回null。当我调试它时,我可以在ContainerGenerator的项目列表中看到要搜索的对象,但是由于某种原因它没有被返回。我查找了所有与UpdateLayout()有关的内容以及其他内容,但我无法弄清楚。

即使在父节点进入视图后,容器中的某些对象也可能不在页面上-例如扩展器下面有250个项目,一次只能渲染60个项目。这可能是个问题吗?

更新

这里是一个示例项目,该示例项目制作了一个虚拟的树状视图来显示此问题。 https://github.com/Mgamerz/TreeViewVirtualizingErrorDemo

在VS中进行构建,然后在搜索框中输入类似4的内容。按几次搜索,即使您打开generator对象,您也可以清楚地看到容器将为空,这将引发异常。在发电机中。

2 个答案:

答案 0 :(得分:3)

与WPF开发的许多其他方面一样,可以使用MVVM设计模式来处理此操作。

创建一个包含IsSelected属性的ViewModel类,该类保存每个树项的数据。

然后可以通过附加属性处理将所选项目带入视图

public static class perTreeViewItemHelper
{
    public static bool GetBringSelectedItemIntoView(TreeViewItem treeViewItem)
    {
        return (bool)treeViewItem.GetValue(BringSelectedItemIntoViewProperty);
    }

    public static void SetBringSelectedItemIntoView(TreeViewItem treeViewItem, bool value)
    {
        treeViewItem.SetValue(BringSelectedItemIntoViewProperty, value);
    }

    public static readonly DependencyProperty BringSelectedItemIntoViewProperty =
        DependencyProperty.RegisterAttached(
            "BringSelectedItemIntoView",
            typeof(bool),
            typeof(perTreeViewItemHelper),
            new UIPropertyMetadata(false, BringSelectedItemIntoViewChanged));

    private static void BringSelectedItemIntoViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        if (!(args.NewValue is bool))
            return;

        var item = obj as TreeViewItem;

        if (item == null)
            return;

        if ((bool)args.NewValue)
            item.Selected += OnTreeViewItemSelected;
        else
            item.Selected -= OnTreeViewItemSelected;
    }

    private static void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
    {
        var item = e.OriginalSource as TreeViewItem;
        item?.BringIntoView();

        // prevent this event bubbling up to any parent nodes
        e.Handled = true;
    }
} 

然后可以将其用作TreeViewItems样式的一部分

<Style x:Key="perTreeViewItemContainerStyle"
       TargetType="{x:Type TreeViewItem}">

    <!-- Link the properties of perTreeViewItemViewModelBase to the corresponding ones on the TreeViewItem -->
    <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
    <Setter Property="IsEnabled" Value="{Binding IsEnabled}" />

    <!-- Include the two "Scroll into View" behaviors -->
    <Setter Property="vhelp:perTreeViewItemHelper.BringSelectedItemIntoView" Value="True" />
    <Setter Property="vhelp:perTreeViewItemHelper.BringExpandedChildrenIntoView" Value="True" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TreeViewItem}">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"
                                          MinWidth="14" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>
                    <ToggleButton x:Name="Expander"
                                  Grid.Row="0"
                                  Grid.Column="0"
                                  ClickMode="Press"
                                  IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
                                  Style="{StaticResource perExpandCollapseToggleStyle}" />

                    <Border x:Name="PART_Border"
                            Grid.Row="0"
                            Grid.Column="1"
                            Padding="{TemplateBinding Padding}"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">

                        <ContentPresenter x:Name="PART_Header"
                                          Margin="0,2"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          ContentSource="Header" />

                    </Border>

                    <ItemsPresenter x:Name="ItemsHost"
                                    Grid.Row="1"
                                    Grid.Column="1" />
                </Grid>

                <ControlTemplate.Triggers>
                    <Trigger Property="IsExpanded" Value="false">
                        <Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed" />
                    </Trigger>

                    <Trigger Property="HasItems" Value="false">
                        <Setter TargetName="Expander" Property="Visibility" Value="Hidden" />
                    </Trigger>

                    <!--  Use the same colors for a selected item, whether the TreeView is focussed or not  -->
                    <Trigger Property="IsSelected" Value="true">
                        <Setter TargetName="PART_Border" Property="Background" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
                    </Trigger>

                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style TargetType="{x:Type TreeView}">
    <Setter Property="ItemContainerStyle" Value="{StaticResource perTreeViewItemContainerStyle}" />
</Style>

我最近的blog post上的更多详细信息和用法的完整示例。

更新10月13日

博客文章已在标准(非延迟加载模式)下运行时进行了修改。关联的演示项目显示了TreeView中显示的超过40万个元素的嵌套数据结构,但是选择任何随机节点的响应都是即时的。

答案 1 :(得分:1)

在所有情况下,尤其是虚拟化情况下,很难获得给定数据项的TreeViewItem

幸运的是,Microsoft在How to: Find a TreeViewItem in a TreeView处为我们提供了我们已经改编的帮助程序功能,因此它不需要自定义VirtualizingStackPanel类(对于较旧的版本,需要.NET Framework 4.5或更高版本,请参阅上面的链接。

以下是您可以替换FocusTreeViewNode方法的方法:

private void FocusTreeViewNode(MenuItem node)
{
    if (node == null)
        return;

    var treeViewItem = GetTreeViewItem(tView, node);
    treeViewItem?.BringIntoView();
}


public static TreeViewItem GetTreeViewItem(ItemsControl container, object item)
{
    if (container == null)
        throw new ArgumentNullException(nameof(container));

    if (item == null)
        throw new ArgumentNullException(nameof(item));

    if (container.DataContext == item)
        return container as TreeViewItem;

    if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
    {
        container.SetValue(TreeViewItem.IsExpandedProperty, true);
    }

    container.ApplyTemplate();
    if (container.Template.FindName("ItemsHost", container) is ItemsPresenter itemsPresenter)
    {
        itemsPresenter.ApplyTemplate();
    }
    else
    {
        itemsPresenter = FindVisualChild<ItemsPresenter>(container);
        if (itemsPresenter == null)
        {
            container.UpdateLayout();
            itemsPresenter = FindVisualChild<ItemsPresenter>(container);
        }
    }

    var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);
    var children = itemsHostPanel.Children;
    var virtualizingPanel = itemsHostPanel as VirtualizingPanel;
    for (int i = 0, count = container.Items.Count; i < count; i++)
    {
        TreeViewItem subContainer;
        if (virtualizingPanel != null)
        {
            // this is the part that requires .NET 4.5+
            virtualizingPanel.BringIndexIntoViewPublic(i);
            subContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromIndex(i);
        }
        else
        {
            subContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromIndex(i);
            subContainer.BringIntoView();
        }

        if (subContainer != null)
        {
            TreeViewItem resultContainer = GetTreeViewItem(subContainer, item);
            if (resultContainer != null)
                return resultContainer;

            subContainer.IsExpanded = false;
        }
    }
    return null;
}

private static T FindVisualChild<T>(Visual visual) where T : Visual
{
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
    {
        if (VisualTreeHelper.GetChild(visual, i) is Visual child)
        {
            if (child is T item)
                return item;

            item = FindVisualChild<T>(child);
            if (item != null)
                return item;
        }
    }
    return null;
}