使用 WPF 在虚拟化树视图中选择一个节点



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

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

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

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

Estifanos Kidane给出的链接已断开。他可能指的是"更改虚拟化树视图中的选择"MSDN 示例。但是,此示例演示如何在树中选择节点,但使用代码隐藏而不是 MVVM 和绑定,因此在绑定的 SelectedItem 更改时,它也不会处理缺少的 SelectedItemChanged 事件。

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

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

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。

每个树项(绑定项(都需要知道其父项(由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
}

在我的 XAML 样式中,我只是将属性设置为 true

<Setter Property="Behaviours:TreeViewItemBehaviour.IsBroughtIntoViewWhenSelected" Value="True" />

如果你使用了这个(https://stackoverflow.com/a/9206992/8559138(决策,有时得到InvalidOperationException,你可以使用我的固定决策:

如果 newParent 为空并尝试再次获取 ContainerFromIndex,我会更新当前父级布局。

 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 问题的示例 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>

我在最近的一篇博客文章中更详细地介绍了这一点。

@splintor的出色答案的更新,使用一些现代 C# 功能,没有任何反思。

public class Node
{
    public Node Parent { get; set; }
}
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)
    {
        if (!(e.NewValue is Node newNode)) return;
        var treeView = ((NodeTreeSelectionBehavior)d).AssociatedObject;
        var ancestors = new List<Node> { newNode };
        var parent = newNode;
        while ((parent = parent.Parent) != null)
        {
            ancestors.Insert(0, parent);
        }
        var currentParent = treeView as ItemsControl;
        foreach (var node in ancestors)
        {
            // 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.
                // see also the question at http://stackoverflow.com/q/183636/46635
                var itemsPresenter = (ItemsPresenter)currentParent.Template.FindName("ItemsHost", currentParent);
                var virtualizingPanel = (VirtualizingPanel)VisualTreeHelper.GetChild(itemsPresenter, 0);
                var index = currentParent.Items.IndexOf(node);
                if (index < 0)
                {
                    throw new InvalidOperationException("Node '" + node + "' cannot be fount in container");
                }
                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 Node;
    }
}

以相同的方式使用:

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

最新更新