使用WPF在虚拟化TreeView中选择节点

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用WPF在虚拟化TreeView中选择节点相关的知识,希望对你有一定的参考价值。

有没有办法在虚拟化TreeView中手动选择节点然后将其带入视图?

我在TreeView中使用的数据模型是基于VM-M-V模型实现的。每个TreeViewItem的IsSelected属性绑定到ViewModel中的对应属性。我还为TreeView的ItemSelected事件创建了一个监听器,我为所选的TreeViewItem调用了BringIntoView()。

这种方法的问题似乎是在创建实际的TreeViewItem之前不会引发ItemSelected事件。因此,启用虚拟化后,节点选择将无法执行任何操作,直到TreeView足够滚动,然后在最终引发事件时“神奇地”跳转到所选节点。

我真的很喜欢使用虚拟化,因为我的树中有数千个节点,并且在启用虚拟化时我已经看到了相当令人印象深刻的性能改进。

答案

Estifanos Kidane给出的链接破了。他可能意味着the "Changing selection in a virtualized TreeView" MSDN sample。但是,此示例显示如何在树中选择节点,但使用代码隐藏而不是MVVM和绑定,因此当更改绑定的SelectedItem时,它也不会处理缺少的SelectedItemChanged event

我能想到的唯一解决方案是打破MVVM模式,当绑定到SelectedItem属性的ViewModel属性发生更改时,获取View并调用代码隐藏方法(类似于MSDN示例),以确保新值实际上是在树中选择的。

这是我编写的代码来处理它。假设您的数据项是Node类型,它具有Parent属性:

public class Node
{
    public Node Parent { get; set; }
}

我写了以下行为类:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

public class NodeTreeSelectionBehavior : Behavior<TreeView>
{
    public Node SelectedItem
    {
        get { return (Node)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(Node), typeof(NodeTreeSelectionBehavior),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));

    private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var newNode = e.NewValue as Node;
        if (newNode == null) return;
        var behavior = (NodeTreeSelectionBehavior)d;
        var tree = behavior.AssociatedObject;

        var nodeDynasty = new List<Node> { newNode };
        var parent = newNode.Parent;
        while (parent != null)
        {
            nodeDynasty.Insert(0, parent);
            parent = parent.Parent;
        }

        var currentParent = tree as ItemsControl;
        foreach (var node in nodeDynasty)
        {
            // first try the easy way
            var newParent = currentParent.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem;
            if (newParent == null)
            {
                // if this failed, it's probably because of virtualization, and we will have to do it the hard way.
                // this code is influenced by TreeViewItem.ExpandRecursive decompiled code, and the MSDN sample at http://code.msdn.microsoft.com/Changing-selection-in-a-6a6242c8/sourcecode?fileId=18862&pathId=753647475
                // see also the question at http://stackoverflow.com/q/183636/46635
                currentParent.ApplyTemplate();
                var itemsPresenter = (ItemsPresenter)currentParent.Template.FindName("ItemsHost", currentParent);
                if (itemsPresenter != null)
                {
                    itemsPresenter.ApplyTemplate();
                }
                else
                {
                    currentParent.UpdateLayout();
                }

                var virtualizingPanel = GetItemsHost(currentParent) as VirtualizingPanel;
                CallEnsureGenerator(virtualizingPanel);
                var index = currentParent.Items.IndexOf(node);
                if (index < 0)
                {
                    throw new InvalidOperationException("Node '" + node + "' cannot be fount in container");
                }
                CallBringIndexIntoView(virtualizingPanel, index);
                newParent = currentParent.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
            }

            if (newParent == null)
            {
                throw new InvalidOperationException("Tree view item cannot be found or created for node '" + node + "'");
            }

            if (node == newNode)
            {
                newParent.IsSelected = true;
                newParent.BringIntoView();
                break;
            }

            newParent.IsExpanded = true;
            currentParent = newParent;
        }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        SelectedItem = e.NewValue as Node;
    }

    #region Functions to get internal members using reflection

    // Some functionality we need is hidden in internal members, so we use reflection to get them

    #region ItemsControl.ItemsHost

    static readonly PropertyInfo ItemsHostPropertyInfo = typeof(ItemsControl).GetProperty("ItemsHost", BindingFlags.Instance | BindingFlags.NonPublic);

    private static Panel GetItemsHost(ItemsControl itemsControl)
    {
        Debug.Assert(itemsControl != null);
        return ItemsHostPropertyInfo.GetValue(itemsControl, null) as Panel;
    }

    #endregion ItemsControl.ItemsHost

    #region Panel.EnsureGenerator

    private static readonly MethodInfo EnsureGeneratorMethodInfo = typeof(Panel).GetMethod("EnsureGenerator", BindingFlags.Instance | BindingFlags.NonPublic);

    private static void CallEnsureGenerator(Panel panel)
    {
        Debug.Assert(panel != null);
        EnsureGeneratorMethodInfo.Invoke(panel, null);
    }

    #endregion Panel.EnsureGenerator

    #region VirtualizingPanel.BringIndexIntoView

    private static readonly MethodInfo BringIndexIntoViewMethodInfo = typeof(VirtualizingPanel).GetMethod("BringIndexIntoView", BindingFlags.Instance | BindingFlags.NonPublic);

    private static void CallBringIndexIntoView(VirtualizingPanel virtualizingPanel, int index)
    {
        Debug.Assert(virtualizingPanel != null);
        BringIndexIntoViewMethodInfo.Invoke(virtualizingPanel, new object[] { index });
    }

    #endregion VirtualizingPanel.BringIndexIntoView

    #endregion Functions to get internal members using reflection
}

使用此类,您可以编写如下的XAML:

<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             xmlns:local="clr-namespace:MyProject">
    <Grid>
        <TreeView ItemsSource="{Binding MyItems}"
                  ScrollViewer.CanContentScroll="True"
                  VirtualizingStackPanel.IsVirtualizing="True"
                  VirtualizingStackPanel.VirtualizationMode="Recycling">
            <i:Interaction.Behaviors>
                <local:NodeTreeSelectionBehavior SelectedItem="{Binding MySelectedItem}" />
            </i:Interaction.Behaviors>
        </TreeView>
    <Grid>
<UserControl>
另一答案

我通过为TreeViewTreeViewItemVirtualizingStackPanel创建自定义控件来解决这个问题。解决方案的一部分来自http://code.msdn.microsoft.com/Changing-selection-in-a-6a6242c8

每个TreeItem(绑定项)都需要知道它的父级(由ITreeItem强制执行)。

public interface ITreeItem {
    ITreeItem Parent { get; }
    IList<ITreeItem> Children { get; }
    bool IsSelected { get; set; }
    bool IsExpanded { get; set; }
}

当在任何TreeItem上设置IsSelected时,将通知视图模型并引发事件。视图中相应的事件侦听器在BringItemIntoView上调用TreeView

TreeView在所选项目的路径上找到所有TreeViewItems并将其带入视图。

在这里剩下的代码:

public class SelectableVirtualizingTreeView : TreeView {
    public SelectableVirtualizingTreeV

以上是关于使用WPF在虚拟化TreeView中选择节点的主要内容,如果未能解决你的问题,请参考以下文章

WPF中treeview模版问题

wpf 自定义treeview 如何获得树节点集合

wpf中的treeview如何增加2级节点?在C#中如何添加?

wpf中选中treeview的某个子节点后获取子节点所在的所有父节点的内容用于数据库查询

wpf如何根据输入信息动态生成treeview

wpf treeview 怎么获取节点的值