Is there a way to select manually a node in virtualizing TreeView and then bring it into view?
The data model I'm using with my TreeView is implemented based on the VM-M-V model. Each TreeViewItem's IsSelected property is binded to a corresponing property in ViewModel. I've also created a listener for TreeView's ItemSelected event where I call BringIntoView() for the selected TreeViewItem.
The problem with this approach seems to be that the ItemSelected event won't be raised until the actual TreeViewItem is created. So with the virtualization enabled node selection won't do anything until the TreeView is scrolled enough and then it jumps "magically" to the selected node when the event is finally raised.
I'd really like to use virtualization because I have thousands of nodes in my tree and I've already seen quite impressive performance improvements when the virtualization has been enabled.
The link Estifanos Kidane gave is broken. He probably meant the "Changing selection in a virtualized TreeView" MSDN sample. however, this sample shows how to select a node in a tree, but using code-behind and not MVVM and binding, so it also doesn't handle the missing SelectedItemChanged event when the bound SelectedItem is changed.
The only solution I can think of is to break the MVVM pattern, and when the ViewModel property that is bound to SelectedItem property changes, get the View and call a code-behind method (similar to the MSDN sample) that makes sure the new value is actually selected in the tree.
Here is the code I wrote to handle it. Suppose your data items are of type Node
which has a Parent
property:
public class Node
{
public Node Parent { get; set; }
}
I wrote the following behavior class:
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
}
With this class, you can write XAML like the following:
<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>