首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >使用WPF在虚拟化TreeView中选择节点

使用WPF在虚拟化TreeView中选择节点
EN

Stack Overflow用户
提问于 2008-10-08 16:25:27
回答 6查看 13K关注 0票数 19

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

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

这种方法的问题似乎是在创建实际的TreeViewItem之前不会引发ItemSelected事件。因此,在启用虚拟化的情况下,节点选择不会做任何事情,直到TreeView滚动足够多,然后当事件最终被引发时,它会“神奇地”跳到所选的节点。

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

EN

回答 6

Stack Overflow用户

发布于 2012-02-09 15:30:53

Estifanos Kidane给出的链接被打破了。他可能指的是the "Changing selection in a virtualized TreeView" MSDN sample。但是,这个示例展示了如何选择树中的节点,但使用的是代码隐藏而不是MVVM和绑定,因此当绑定的SelectedItemChanged event发生更改时,它也不会处理丢失的SelectedItem。

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

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

代码语言:javascript
复制
public class Node
{
    public Node Parent { get; set; }
}

我写了下面的behavior类:

代码语言:javascript
复制
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:

代码语言:javascript
复制
<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>
票数 17
EN

Stack Overflow用户

发布于 2013-07-26 22:09:37

我通过为TreeViewTreeViewItemVirtualizingStackPanel创建自定义控件解决了这个问题。部分解决方案来自http://code.msdn.microsoft.com/Changing-selection-in-a-6a6242c8

每个TreeItem (绑定项)都需要知道它的父项(由ITreeItem强制执行)。

代码语言:javascript
复制
public interface ITreeItem {
    ITreeItem Parent { get; }
    IList<ITreeItem> Children { get; }
    bool IsSelected { get; set; }
    bool IsExpanded { get; set; }
}

当在任何TreeItem上设置了IsSelected时,视图模型都会收到通知并引发一个事件。视图中相应的事件侦听器在TreeView上调用BringItemIntoView

TreeView查找所选项目路径上的所有TreeViewItems,并将它们带到视图中。

下面是代码的其余部分:

代码语言:javascript
复制
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中:

代码语言:javascript
复制
<controls:SelectableVirtualizingTreeView x:Name="_treeView" ItemsSource="{Binding TreeItemViewModels}" Margin="8" 
        SelectedItemChanged="OnTreeViewSelectedItemChanged">
    <controls:SelectableVirtualizingTreeView.ItemTemplate>
        <HierarchicalDataTemplate ... />
    </controls:SelectableVirtualizingTreeView.ItemTemplate>
</controls:SelectableVirtualizingTreeView>
票数 2
EN

Stack Overflow用户

发布于 2009-11-12 23:00:03

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

代码语言:javascript
复制
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

代码语言:javascript
复制
<Setter Property="Behaviours:TreeViewItemBehaviour.IsBroughtIntoViewWhenSelected" Value="True" />

HTH

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/183636

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档