【问题标题】:WPF TreeView with virtualization - select and bring item into view具有虚拟化的 WPF TreeView - 选择并将项目带入视图
【发布时间】:2018-09-18 02:27:18
【问题描述】:

我最近一直在使用 WPF 树视图,当用户使用在支持对象。

目前我的方法是使用这个答案中的方法: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 对象你可以清楚地看到它在发电机。

【问题讨论】:

  • 你有一个小的复制项目吗?
  • 是的,我相信我可以创建一个,给我几个小时,因为我正在做的那个太复杂了,无法缩小。
  • 完成 - 添加了一个 github 项目的链接来展示该问题

标签: c# wpf treeview


【解决方案1】:

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

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

然后可以通过附加属性处理将所选项目显示在视图中

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 中显示了超过 400,000 个元素,但选择任何随机节点的响应都是即时的。

【讨论】:

  • 虽然不像我希望的那么容易(我不知道新手怎么能弄明白)你的博客文章非常方便且非常详细,谢谢。
  • 我花了很多年几乎全职接触 WPF / MVVM 来达到我对平台感到满意的地步,并建立了一个像这样的有用结构库。要记住的关键是 WPF 在设计范式中与 winforms 完全相反。诸如事件处理程序和无休止代码之类的技术可能在其他 UI 平台中运行良好,但在 WPF 中运行不佳。
  • 当两本最畅销的 WPF 书籍(Adam Nathan 和 Matthew MacDonald)仍然只是顺便提及 MVVM 设计模式时,学习情况无济于事。
  • @Mgamerz 我刚刚更新了我的博文。新的演示项目演示了在 TreeView 中显示包含超过 400,000 个元素的嵌套数据结构,但选择任何随机节点的响应都是即时的。
  • 实现了这一点,不幸的是,当虚拟化打开时它也不能可靠地工作。对 containerfromitem() 的调用不会返回任何内容,因此无法将此类项目带入视野。一旦第一个 BringIntoView() 失败,所有后续调用通常也会失败。搜索继续……
【解决方案2】:

获取给定数据项的TreeViewItem 非常困难,在所有情况下,尤其是虚拟化数据项。

幸运的是,微软在这里为我们提供了一个帮助函数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;
}

【讨论】:

  • 我已经对此进行了测试,虽然它确实有效,但在大型树上的性能非常糟糕 - 我们在 20 或 30,000 个节点的树上谈论超过 60 秒。既然我们知道我们试图关注的项目的父母,也许有一种优化它的方法或什么的?
  • @Mgamerz - mmhhh ...真的。问题似乎来自于 BringIndexIntoViewPublic 上的 20000 循环。不知道如何在没有问题的情况下做得更好。问题是虚拟化面板优化了 UI,而不是搜索。也许你应该改变你做事的方式。例如,您可以使用自定义 TreeView 从 Items 构建 TreeViewItems 字典:social.msdn.microsoft.com/Forums/vstudio/en-US/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-08-12
  • 2013-03-04
  • 2010-11-12
  • 2023-03-24
  • 2010-11-05
  • 2015-01-28
  • 1970-01-01
相关资源
最近更新 更多