简体   繁体   English

带虚拟化的WPF TreeView - 选择项目并将其置于视图中

[英]WPF TreeView with virtualization - select and bring item into view

I've been working with WPF treeview for a bit recently and I'm having a really awful time trying to get the selected item to show up on the screen when the user uses a search function that sets the IsSelected property on the backing object. 我最近一直在使用WPF树视图,当用户使用在后备对象上设置IsSelected属性的搜索功能时,我正试图让所选项目显示在屏幕上非常糟糕。

Currently my approach is using the method in this answer: https://stackoverflow.com/a/34620549/800318 目前我的方法是使用这个答案中的方法: 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 is my backing data type, which has a reference to its parent node. TreeViewEntry是我的后备数据类型,它具有对其父节点的引用。 Leftside_TreeView is the virtualized TreeView that is bound to the list of my objects. Leftside_TreeView是绑定到我的对象列表的虚拟化TreeView。 Turning off virtualization is not an option as performance is really bad with it off. 关闭虚拟化不是一种选择,因为性能非常糟糕。

When I search for an object and the backing data object is found, I call this FocusTreeViewNode() method with the object as its parameter. 当我搜索一个对象并找到后备数据对象时,我将该对象作为其参数调用此FocusTreeViewNode()方法。 It will typically work on the first call, selecting the object and bringing it into view. 它通常在第一次调用时工作,选择对象并将其带入视图。

Upon doing the search a second time, the node to select is passed in, however the ContainerFromItem() call when the stack is emptied (so it is trying to generate the container for the object itself) returns null. 在第二次执行搜索时,将传入要选择的节点,但是当清空堆栈时,ContainerFromItem()调用(因此它尝试为对象本身生成容器)返回null。 When I debug this I can see the object I am searching for in the ContainerGenerator's items list, but for some reason it is not being returned. 当我调试这个时,我可以在ContainerGenerator的项目列表中看到我正在搜索的对象,但由于某种原因它没有被返回。 I looked up all the things to do with UpdateLayout() and other things, but I can't figure this out. 我查看了所有与UpdateLayout()和其他事情有关的事情,但我无法弄清楚这一点。

Some of the objects in the container may be off the page even after the parent node is brought into view - eg an expander has 250 items under it and only 60 are rendered at time. 即使在父节点进入视图之后,容器中的一些对象也可能在页面外 - 例如,扩展器在其下面有250个项目,并且在时间上仅渲染60个。 Could this be an issue? 这可能是个问题吗?

Update 更新

Here is a sample project that makes a virtualized treeview that shows this issue. 这是一个示例项目,它使虚拟化树视图显示此问题。 https://github.com/Mgamerz/TreeViewVirtualizingErrorDemo https://github.com/Mgamerz/TreeViewVirtualizingErrorDemo

Build it in VS, then in the search box enter something like 4. Press search several times and it will throw an exception saying the container was null, even though if you open the generator object you can clearly see it's in the generator. 在VS中构建它,然后在搜索框中输入类似于4.按几次搜索,它会抛出一个异常,说容器为空,即使你打开generator对象,你也可以清楚地看到它在生成器中。

Like many other aspects of WPF development, this operation can be handled by using the MVVM design pattern. 与WPF开发的许多其他方面一样,可以使用MVVM设计模式来处理此操作。

Create a ViewModel class, including an IsSelected property, which holds the data for each tree item. 创建一个ViewModel类,包括一个IsSelected属性,该属性保存每个树项的数据。

Bringing the selected item into view can then be handled by an attached property 然后,可以通过附加属性处理将所选项目置于视图中

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;
    }
} 

This can then be used as part of a style for TreeViewItems 然后,这可以用作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>

More details and a full example of usage on my recent blog post . 更多详细信息以及我最近的博客文章中的完整使用示例。

Update 13 Oct 10月13日更新

The blog post has been amended for when running in standard (non-lazy loading mode). 在标准(非延迟加载模式)下运行时,博客文章已经过修改。 The associated demo project shows a nested data structure of over 400,000 elements being displayed in a TreeView, and yet the response for selecting any random node is instantaneous. 关联的演示项目显示了在TreeView中显示的超过400,000个元素的嵌套数据结构,但是选择任何随机节点的响应是瞬时的。

It's quite difficult to get the TreeViewItem for a given data item, in all cases, especially the virtualized ones. 在所有情况下,特别是虚拟化的项目,很难获得给定数据项的TreeViewItem

Fortunately, Microsoft has provided a helper function for us here How to: Find a TreeViewItem in a TreeView that I have adapted so it doesn't need a custom VirtualizingStackPanel class (requires .NET Framework 4.5 or higher, for older versions, consult the link above). 幸运的是,Microsoft为我们提供了一个帮助函数如何:在我已经调整的TreeView中查找TreeViewItem ,因此它不需要自定义的VirtualizingStackPanel类(需要.NET Framework 4.5或更高版本,对于旧版本,请参阅链接以上)。

Here is how you can replace your FocusTreeViewNode method: 以下是如何替换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;
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM