使用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>
我通过为TreeView
,TreeViewItem
和VirtualizingStackPanel
创建自定义控件来解决这个问题。解决方案的一部分来自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如何增加2级节点?在C#中如何添加?