19

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.

4

7 回答 7

18

Estifanos Kidane 提供的链接已损坏。他可能指的是“在虚拟化 TreeView 中更改选择”MSDN 示例。但是,此示例显示了如何选择树中的节点,但使用代码隐藏而不是 MVVM 和绑定,因此当绑定的 SelectedItem 更改时,它也不会处理丢失的SelectedItemChanged 事件。

我能想到的唯一解决方案是打破 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>
于 2012-02-09T07:30:52.863 回答
2

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

IsSelected在任何 TreeItem 上设置时,会通知视图模型并引发事件。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>
于 2013-07-26T14:09:36.647 回答
1

我使用附加属性来解决这个问题。

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

高温高压

于 2009-11-12T15:00:03.217 回答
1

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

如果 newParent 为空,我会更新 currentParent 布局并再次尝试获取 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>

于 2018-09-07T08:28:03.623 回答
0

Here is an example taken from an 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);

    }
于 2008-10-08T16:34:25.573 回答
0

看到最近发布了这个问题的新答案,我将把我的 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>

我在最近的一篇博文中对此进行了更详细的介绍。

于 2018-09-07T08:55:31.747 回答
0

@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>
于 2020-10-29T23:06:28.347 回答