有没有办法在虚拟化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 SelectableVirtualizingTreeView() {
VirtualizingStackPanel.SetIsVirtualizing(this, true);
VirtualizingStackPanel.SetVirtualizationMode(this, VirtualizationMode.Recycling);
var panelfactory = new FrameworkElementFactory(typeof(SelectableVirtualizingStackPanel));
panelfactory.SetValue(Panel.IsItemsHostProperty, true);
var template = new ItemsPanelTemplate { VisualTree = panelfactory };
ItemsPanel = template;
}
public void BringItemIntoView(ITreeItem treeItemViewModel) {
if (treeItemViewModel == null) {
return;
}
var stack = new Stack<ITreeItem>();
stack.Push(treeItemViewModel);
while (treeItemViewModel.Parent != null) {
stack.Push(treeItemViewModel.Parent);
treeItemViewModel = treeItemViewModel.Parent;
}
ItemsControl containerControl = this;
while (stack.Count > 0) {
var viewModel = stack.Pop();
var treeViewItem = containerControl.ItemContainerGenerator.ContainerFromItem(viewModel);
var virtualizingPanel = FindVisualChild<SelectableVirtualizingStackPanel>(containerControl);
if (virtualizingPanel != null) {
var index = viewModel.Parent != null ? viewModel.Parent.Children.IndexOf(viewModel) : Items.IndexOf(treeViewItem);
virtualizingPanel.BringIntoView(index);
Focus();
}
containerControl = (ItemsControl)treeViewItem;
}
}
protected override DependencyObject GetContainerForItemOverride() {
return new SelectableVirtualizingTreeViewItem();
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item) {
base.PrepareContainerForItemOverride(element, item);
((TreeViewItem)element).IsExpanded = true;
}
private static T FindVisualChild<T>(Visual visual) where T : Visual {
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++) {
var child = (Visual)VisualTreeHelper.GetChild(visual, i);
if (child == null) {
continue;
}
var correctlyTyped = child as T;
if (correctlyTyped != null) {
return correctlyTyped;
}
var descendent = FindVisualChild<T>(child);
if (descendent != null) {
return descendent;
}
}
return null;
}
}
public class SelectableVirtualizingTreeViewItem : TreeViewItem {
public SelectableVirtualizingTreeViewItem() {
var panelfactory = new FrameworkElementFactory(typeof(SelectableVirtualizingStackPanel));
panelfactory.SetValue(Panel.IsItemsHostProperty, true);
var template = new ItemsPanelTemplate { VisualTree = panelfactory };
ItemsPanel = template;
SetBinding(IsSelectedProperty, new Binding("IsSelected"));
SetBinding(IsExpandedProperty, new Binding("IsExpanded"));
}
protected override DependencyObject GetContainerForItemOverride() {
return new SelectableVirtualizingTreeViewItem();
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item) {
base.PrepareContainerForItemOverride(element, item);
((TreeViewItem)element).IsExpanded = true;
}
}
public class SelectableVirtualizingStackPanel : VirtualizingStackPanel {
public void BringIntoView(int index) {
if (index < 0) {
return;
}
BringIndexIntoView(index);
}
}
public abstract class TreeItemBase : ITreeItem {
protected TreeItemBase() {
Children = new ObservableCollection<ITreeItem>();
}
public ITreeItem Parent { get; protected set; }
public IList<ITreeItem> Children { get; protected set; }
public abstract bool IsSelected { get; set; }
public abstract bool IsExpanded { get; set; }
public event EventHandler DescendantSelected;
protected void RaiseDescendantSelected(TreeItemViewModel newItem) {
if (Parent != null) {
((TreeItemViewModel)Parent).RaiseDescendantSelected(newItem);
} else {
var handler = DescendantSelected;
if (handler != null) {
handler.Invoke(newItem, EventArgs.Empty);
}
}
}
}
public class MainViewModel : INotifyPropertyChanged {
private TreeItemViewModel _selectedItem;
public MainViewModel() {
TreeItemViewModels = new List<TreeItemViewModel> { new TreeItemViewModel { Name = "Item" } };
for (var i = 0; i < 30; i++) {
TreeItemViewModels[0].AddChildInitial();
}
TreeItemViewModels[0].IsSelected = true;
TreeItemViewModels[0].DescendantSelected += OnDescendantSelected;
}
public event EventHandler DescendantSelected;
public event PropertyChangedEventHandler PropertyChanged;
public List<TreeItemViewModel> TreeItemViewModels { get; private set; }
public TreeItemViewModel SelectedItem {
get {
return _selectedItem;
}
set {
if (_selectedItem == value) {
return;
}
_selectedItem = value;
var handler = PropertyChanged;
if (handler != null) {
handler.Invoke(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
}
private void OnDescendantSelected(object sender, EventArgs eventArgs) {
var handler = DescendantSelected;
if (handler != null) {
handler.Invoke(sender, eventArgs);
}
}
}
public partial class MainWindow {
public MainWindow() {
InitializeComponent();
var mainViewModel = (MainViewModel)DataContext;
mainViewModel.DescendantSelected += OnMainViewModelDescendantSelected;
}
private void OnAddButtonClick(object sender, RoutedEventArgs e) {
var mainViewModel = (MainViewModel)DataContext;
var treeItemViewModel = mainViewModel.SelectedItem;
if (treeItemViewModel != null) {
treeItemViewModel.AddChild();
}
}
private void OnMainViewModelDescendantSelected(object sender, EventArgs eventArgs) {
_treeView.BringItemIntoView(sender as TreeItemViewModel);
}
private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) {
if (e.OldValue == e.NewValue) {
return;
}
var treeView = (TreeView)sender;
var treeItemviewModel = treeView.SelectedItem as TreeItemViewModel;
var mainViewModel = (MainViewModel)DataContext;
mainViewModel.SelectedItem = treeItemviewModel;
}
}
在XAML中:
<controls:SelectableVirtualizingTreeView x:Name="_treeView" ItemsSource="{Binding TreeItemViewModels}" Margin="8"
SelectedItemChanged="OnTreeViewSelectedItemChanged">
<controls:SelectableVirtualizingTreeView.ItemTemplate>
<HierarchicalDataTemplate ... />
</controls:SelectableVirtualizingTreeView.ItemTemplate>
</controls:SelectableVirtualizingTreeView>
我使用附加属性来解决这个问题。
public class TreeViewItemBehaviour
{
#region IsBroughtIntoViewWhenSelected
public static bool GetIsBroughtIntoViewWhenSelected(TreeViewItem treeViewItem)
{
return (bool)treeViewItem.GetValue(IsBroughtIntoViewWhenSelectedProperty);
}
public static void SetIsBroughtIntoViewWhenSelected(
TreeViewItem treeViewItem, bool value)
{
treeViewItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value);
}
public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty =
DependencyProperty.RegisterAttached(
"IsBroughtIntoViewWhenSelected",
typeof(bool),
typeof(TreeViewItemBehaviour),
new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged));
static void OnIsBroughtIntoViewWhenSelectedChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
TreeViewItem item = depObj as TreeViewItem;
if (item == null)
return;
if (e.NewValue is bool == false)
return;
if ((bool)e.NewValue)
{
item.Loaded += item_Loaded;
}
else
{
item.Loaded -= item_Loaded;
}
}
static void item_Loaded(object sender, RoutedEventArgs e)
{
TreeViewItem item = e.OriginalSource as TreeViewItem;
if (item != null)
item.BringIntoView();
}
#endregion // IsBroughtIntoViewWhenSelected
}
在我的TreeViewItem的XAML样式中,我只是将属性设置为true
<Setter Property="Behaviours:TreeViewItemBehaviour.IsBroughtIntoViewWhenSelected" Value="True" />
HTH
如果您使用此(https://stackoverflow.com/a/9206992/8559138)决策并且有时会获得InvalidOperationException,则可以使用我的固定决策:
如果newParent为null并尝试再次获取ContainerFromIndex,则更新currentParent布局。
newParent = currentParent.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
if (newParent == null)
{
currentParent.UpdateLayout();
virtualizingPanel.BringIndexIntoViewPublic(index);
newParent = currentParent.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
}
完整决定:
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 INode SelectedItem
{
get { return (INode)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 INode;
if (newNode == null) return;
var behavior = (NodeTreeSelectionBehavior)d;
var tree = behavior.AssociatedObject;
var nodeDynasty = new List<INode> { 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;
var index = 0;
VirtualizingPanel virtualizingPanel = null;
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();
}
virtualizingPanel = GetItemsHost(currentParent) as VirtualizingPanel;
CallEnsureGenerator(virtualizingPanel);
index = currentParent.Items.IndexOf(node);
if (index < 0)
{
throw new InvalidOperationException("Node '" + node + "' cannot be fount in container");
}
if (virtualizingPanel != null)
{
virtualizingPanel.BringIndexIntoViewPublic(index);
}
newParent = currentParent.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
if (newParent == null)
{
currentParent.UpdateLayout();
virtualizingPanel.BringIndexIntoViewPublic(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 INode;
}
#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
#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>
这是一个来自MSDN Question public void ScrollToItem(int index)的示例
{
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Background,
(System.Windows.Threading.DispatcherOperationCallback)delegate(object arg)
{
int N = fileList.Items.Count;
if (N == 0)
return null;
if (index < 0)
{
fileList.ScrollIntoView(fileList.Items[0]); // scroll to first
}
else
{
if (index < N)
{
fileList.ScrollIntoView(fileList.Items[index]); // scroll to item
}
else
{
fileList.ScrollIntoView(fileList.Items[N - 1]); // scroll to last
}
}
return null;
}, null);
}
看到最近在这个问题上发布了一个新的答案,我将把我的0.02美元添加到这个问题的MVVM纯解决方案中。
将perTreeViewItemViewModelBase作为树视图项数据的基类,您可以使用附加属性在TreeView上创建可绑定的选定项属性。
public class perTreeViewHelper : Behavior<TreeView>
{
public object BoundSelectedItem
{
get { return GetValue(BoundSelectedItemProperty); }
set { SetValue(BoundSelectedItemProperty, value); }
}
public static readonly DependencyProperty BoundSelectedItemProperty =
DependencyProperty.Register("BoundSelectedItem",
typeof(object),
typeof(perTreeViewHelper),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnBoundSelectedItemChanged));
private static void OnBoundSelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var item = args.NewValue as perTreeViewItemViewModelBase;
if (item != null)
item.IsSelected = true;
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
base.OnDetaching();
}
private void OnTreeViewSelectedItemChanged(object obj, RoutedPropertyChangedEventArgs<object> args)
{
BoundSelectedItem = args.NewValue;
}
}
第二个辅助类处理滚动TreeViewItems到视图。有两种不同的情况
请注意使用调度程序优先级,这可确保在我们尝试将其滚动到视图之前完全形成任何虚拟化项目。
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;
}
public static bool GetBringExpandedChildrenIntoView(TreeViewItem treeViewItem)
{
return (bool)treeViewItem.GetValue(BringExpandedChildrenIntoViewProperty);
}
public static void SetBringExpandedChildrenIntoView(TreeViewItem treeViewItem, bool value)
{
treeViewItem.SetValue(BringExpandedChildrenIntoViewProperty, value);
}
public static readonly DependencyProperty BringExpandedChildrenIntoViewProperty =
DependencyProperty.RegisterAttached(
"BringExpandedChildrenIntoView",
typeof(bool),
typeof(perTreeViewItemHelper),
new UIPropertyMetadata(false, BringExpandedChildrenIntoViewChanged));
private static void BringExpandedChildrenIntoViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (!(args.NewValue is bool))
return;
var item = obj as TreeViewItem;
if (item == null)
return;
if ((bool)args.NewValue)
item.Expanded += OnTreeViewItemExpanded;
else
item.Expanded -= OnTreeViewItemExpanded;
}
private static void OnTreeViewItemExpanded(object sender, RoutedEventArgs e)
{
var item = e.OriginalSource as TreeViewItem;
if (item == null)
return;
// use DispatcherPriority.ContextIdle, so that we wait for all of the UI elements for any newly visible children to be created
// first bring the last child into view
Action action = () =>
{
var lastChild = item.ItemContainerGenerator.ContainerFromIndex(item.Items.Count - 1) as TreeViewItem;
lastChild?.BringIntoView();
};
item.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
// then bring the expanded item (back) into view
action = () => { item.BringIntoView(); };
item.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
// prevent this event bubbling up to any parent nodes
e.Handled = true;
}
}
此助手类可以包含在TreeView控件的样式中。
<Style x:Key="perExpandCollapseToggleStyle" TargetType="ToggleButton">
<Setter Property="Focusable" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid Width="10"
Height="10"
Background="Transparent">
<Path x:Name="ExpanderGlyph"
Margin="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Data="M 0,3 L 0,5 L 3,5 L 3,8 L 5,8 L 5,5 L 8,5 L 8,3 L 5,3 L 5,0 L 3,0 L 3,3 z"
Fill="LightGreen"
Stretch="None" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="ExpanderGlyph" Property="Data" Value="M 0,0 M 8,8 M 0,3 L 0,5 L 8,5 L 8,3 z" />
<Setter TargetName="ExpanderGlyph" Property="Fill" Value="Red" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ExpanderGlyph" Property="Fill" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<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中更详细地介绍了这一点。