2008-10-08 22 views
17

Gibt es eine Möglichkeit, manuell einen Knoten in der Virtualisierung von TreeView auszuwählen und ihn dann anzuzeigen?Auswählen eines Knotens in virtualisiertem TreeView mit WPF

Das Datenmodell, das ich mit meinem TreeView verwende, ist basierend auf dem VM-M-V-Modell implementiert. Die IsSelected-Eigenschaft jedes TreeViewItems wird an eine entsprechende Eigenschaft in ViewModel gebunden. Ich habe auch einen Listener für das TreeView ItemSelected-Ereignis erstellt, wo ich BringIntoView() für das ausgewählte TreeViewItem aufruft.

Das Problem mit diesem Ansatz scheint zu sein, dass das ItemSelected-Ereignis nicht ausgelöst wird, bis das eigentliche TreeViewItem erstellt wird. Bei der Virtualisierung wird die Auswahl der Knoten so lange nicht funktionieren, bis die TreeView genug gescrollt ist und dann "magisch" zum ausgewählten Knoten springt, wenn das Event schließlich ausgelöst wird.

Ich würde wirklich gerne Virtualisierung verwenden, da ich Tausende von Knoten in meinem Baum habe und ich bereits beeindruckende Leistungsverbesserungen gesehen habe, wenn die Virtualisierung aktiviert wurde.

+0

Hallo, ich auch mit dem gleichen Problem steckte. Sie haben eine Lösung für dieses Problem? – akjoshi

Antwort

0

Hier ist ein Beispiel von einem MSDN Question public void ScrollToItem (int index) genommen

{ 

     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

wie Mark sagte, dies funktioniert nur für eine ListBox abgeleitetes Element – arolson101

1

ich eine angefügte Eigenschaft verwendet, um dieses Problem zu lösen.

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 

} 

Und in meiner XAML Stil für eine TreeViewItem, habe ich nur die Eigenschaft auf true

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

HTH

+4

Wenn Sie eine virtualisierte TreeView verwenden, dann das TreeViewItem, das Sie am wahrscheinlichsten auswählen möchten, ist noch nicht erstellt - also ist es absolut nutzlos, einen Style für TreeViewItem anzuwenden. – springy76

12

Der Link gab Estifanos Kidane gebrochen ist. Er meinte wahrscheinlich the "Changing selection in a virtualized TreeView" MSDN sample. jedoch zeigt diese Probe, wie ein Knoten in einem Baum wählen, aber unter Verwendung von Code-Behind und nicht mvvm und verbindlich, so dass es auch nicht die fehlenden SelectedItemChanged event behandeln, wenn das gebundene SelectedItem geändert wird. Die einzige Lösung, die ich mir vorstellen kann, ist, das MVVM-Muster aufzubrechen, und wenn die ViewModel-Eigenschaft, die an die SelectedItem-Eigenschaft gebunden ist, geändert wird, ruft die View auf und ruft eine Code-Behind-Methode (ähnlich dem MSDN-Beispiel) auf sicher, dass der neue Wert tatsächlich in der Baumstruktur ausgewählt ist.

Hier ist der Code, den ich geschrieben habe, um damit umzugehen. Angenommen, Ihre Datenelemente vom Typ sind Node, die eine Parent Eigenschaft hat:

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

ich das folgende Verhalten Klasse schrieb:

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 
} 

Mit dieser Klasse können Sie schreiben XAML wie folgt aus:

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

+1: Das funktioniert gut für mich in Windows 7/dotNet 4.0. Weiß jemand, ob die durch Reflexion aufgerufenen Methoden in dotNet 4.5 oder 4.5.1 verfügbar sind? (Ich werde mich selbst überprüfen, aber wollte eine Gelegenheit für einen anderen Entwickler geben, ihre Weisheit zu teilen :) –

+2

@CameronPeters .Net 4.5 hinzugefügt BringIndexIntoViewPublic (http://msdn.microsoft.com/en-us/library/system.windows.controls .virtualizingpanel.bringindixintoviewpublic.aspx). Wenn Sie also .Net 4.5 oder höher verwenden, können Sie es direkt aufrufen, anstatt es per Reflektion zu erhalten. – splintor

+0

Sie Jungs sind genial - ich habe es geschafft, diese Lösung zu implementieren - es funktioniert in Windows 10 und es funktioniert auch mit der Methode BringIndexIntoPublic in .Net 4.5 - cool :-) Ich habe eine Schnittstelle als Referenz anstelle des eigentlichen Knotens verwendet Klasse - also, ich denke, das Muster ist nicht vollständig gebrochen. Ich frage mich, ob es einen Ersatz für die andere Methode gibt: EnsureGenerator und Property: ItemsHost in 4.5 oder später? Wie kommt es, dass Sie versteckte Objekte verwenden müssen, um etwas so Alltägliches zu tun? – user3313608

1

Ich löste dieses Problem, indem ich benutzerdefinierte Steuerelemente für TreeView, TreeViewItem und VirtualizingStackPanel erstellte. Ein Teil der Lösung ist von http://code.msdn.microsoft.com/Changing-selection-in-a-6a6242c8.

Jedes TreeItem (gebundenes Element) muss sein übergeordnetes Element kennen (erzwungen durch ITreeItem).

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

Wenn IsSelected auf jedem TreeItem gesetzt ist die Ansicht Modell informiert wird und löst ein Ereignis. Der entsprechende Ereignis-Listener in der Sicht ruft BringItemIntoView auf TreeView auf.

Die TreeView findet alle TreeViewItems auf dem Pfad zum ausgewählten Element und bringt sie in den Blick.

Und hier der Rest des Codes:

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

Und in XAML:

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