c# wpf mvvm treeview

c# - Seleccionar un nodo en TreeView virtualizado con WPF



mvvm (4)

Aquí hay un ejemplo tomado de un MSDN Question public void ScrollToItem (índice int)

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

¿Hay alguna manera de seleccionar manualmente un nodo en la virtualización de TreeView y luego ponerlo a la vista?

El modelo de datos que estoy usando con mi TreeView se implementa en base al modelo VM-MV. La propiedad IsSelected de cada TreeViewItem está vinculada a una propiedad correspondiente en ViewModel. También he creado un oyente para el evento ItemSelected de TreeView donde llamo a BringIntoView () para el TreeViewItem seleccionado.

El problema con este enfoque parece ser que el evento ItemSelected no se levantará hasta que se cree el TreeViewItem real. Por lo tanto, con la virtualización habilitada, la selección de nodos no hará nada hasta que TreeView se desplace lo suficiente y salte "mágicamente" al nodo seleccionado cuando el evento finalmente se levante.

Realmente me gustaría usar la virtualización porque tengo miles de nodos en mi árbol y ya he visto mejoras de rendimiento bastante impresionantes cuando la virtualización se ha habilitado.


Usé una propiedad adjunta para resolver este problema.

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 }

Y en mi estilo XAML para TreeViewItem, simplemente establezco la propiedad en true

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

HTH


El enlace que dio Estifanos Kidane está roto. Probablemente se refería a la muestra de MSDN "Cambio de selección en un TreeView virtualizado" . sin embargo, este ejemplo muestra cómo seleccionar un nodo en un árbol, pero usando código subyacente y no MVVM y enlace, por lo que tampoco maneja el evento SelectedItemChanged que falta cuando se cambia el SelectedItem vinculado.

La única solución que puedo pensar es romper el patrón MVVM y cuando la propiedad ViewModel que está vinculada a la propiedad SelectedItem cambia, obtener la vista y llamar a un método de código subyacente (similar al ejemplo de MSDN) que asegura que el nuevo valor en realidad está seleccionado en el árbol.

Aquí está el código que escribí para manejarlo. Suponga que sus elementos de datos son del tipo Node que tiene una propiedad Parent :

public class Node { public Node Parent { get; set; } }

Escribí la siguiente clase de comportamiento:

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://.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 }

Con esta clase, puede escribir XAML de la siguiente manera:

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


Resolví este problema creando controles personalizados para TreeView , TreeViewItem y VirtualizingStackPanel . Una parte de la solución es de http://code.msdn.microsoft.com/Changing-selection-in-a-6a6242c8 .

Cada elemento TreeItem (elemento vinculado) requiere conocer su elemento principal (impuesta por ITreeItem ).

public interface ITreeItem { ITreeItem Parent { get; } IList<ITreeItem> Children { get; } bool IsSelected { get; set; } bool IsExpanded { get; set; } }

Cuando se selecciona IsSelected en cualquier TreeItem, se notifica al modelo de vista y se genera un evento. El detector de eventos correspondiente en la vista llama a BringItemIntoView en TreeView .

TreeView encuentra todos los elementos de TreeViewItems en la ruta al elemento seleccionado y los pone a la vista.

Y aquí el resto del código:

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

Y en XAML:

<controls:SelectableVirtualizingTreeView x:Name="_treeView" ItemsSource="{Binding TreeItemViewModels}" Margin="8" SelectedItemChanged="OnTreeViewSelectedItemChanged"> <controls:SelectableVirtualizingTreeView.ItemTemplate> <HierarchicalDataTemplate ... /> </controls:SelectableVirtualizingTreeView.ItemTemplate> </controls:SelectableVirtualizingTreeView>