2008-10-08 51 views
17

Có cách nào để chọn thủ công một nút trong ảo hóa TreeView và sau đó đưa nó vào xem?Chọn một nút trong TreeView ảo hóa với WPF

Mô hình dữ liệu tôi đang sử dụng với TreeView được triển khai dựa trên mô hình VM-M-V. Mỗi thuộc tính IsSelected của TreeViewItem được liên kết với một thuộc tính corresponing trong ViewModel. Tôi cũng đã tạo một trình lắng nghe cho sự kiện ItemSelected của TreeView nơi tôi gọi BringIntoView() cho TreeViewItem đã chọn.

Vấn đề với cách tiếp cận này dường như là sự kiện ItemSelected sẽ không được nâng lên cho đến khi TreeViewItem thực tế được tạo ra. Vì vậy, với sự lựa chọn nút kích hoạt ảo hóa sẽ không làm bất cứ điều gì cho đến khi TreeView được cuộn đủ và sau đó nó nhảy "kỳ diệu" đến nút đã chọn khi sự kiện cuối cùng được nâng lên.

Tôi thực sự muốn sử dụng ảo hóa vì tôi có hàng nghìn nút trong cây và tôi đã thấy các cải tiến hiệu suất khá ấn tượng khi bật ảo hóa.

+0

Hi, tôi cũng đang mắc kẹt với cùng một vấn đề. Bạn có giải pháp cho vấn đề này? – akjoshi

Trả lời

0

Dưới đây là một ví dụ lấy từ một MSDN Question public void ScrollToItem (chỉ số 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); 

    } 
+0

như Mark cho biết, điều này chỉ hoạt động cho một mục ListBox dẫn xuất – arolson101

1

tôi đã sử dụng một tài sản gắn liền với giải quyết vấn đề này.

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 

} 

Và trong phong cách XAML của tôi cho một TreeViewItem, tôi chỉ thiết lập thuộc tính là true

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

HTH

+4

Khi bạn sử dụng TreeView ảo hóa thì TreeViewItem bạn muốn chọn nhiều khả năng chưa được tạo - vì vậy hoàn toàn vô dụng khi áp dụng bất kỳ kiểu nào cho TreeViewItem. – springy76

12

Liên kết Estifanos Kidane đã bị phá vỡ. Ông có thể có nghĩa là the "Changing selection in a virtualized TreeView" MSDN sample. tuy nhiên, mẫu này cho thấy cách chọn một nút trong cây, nhưng sử dụng mã-đằng sau chứ không phải MVVM và ràng buộc, vì vậy nó cũng không xử lý thiếu SelectedItemChanged event khi SelectedItem bị ràng buộc bị thay đổi. Giải pháp duy nhất tôi có thể nghĩ là phá vỡ mô hình MVVM, và khi thuộc tính ViewModel được liên kết với các thay đổi thuộc tính SelectedItem, hãy lấy View và gọi một phương thức code-behind (tương tự như mẫu MSDN) mà làm cho chắc chắn giá trị mới thực sự được chọn trong cây.

Đây là mã tôi đã viết để xử lý. Giả sử mục dữ liệu của bạn là loại Node trong đó có một tài sản Parent:

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

tôi đã viết lớp hành vi sau đây:

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 
} 

Với lớp này, bạn có thể viết XAML như sau:

<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: Tính năng này hoạt động tốt với tôi trong Windows 7/dotNet 4.0. Có ai biết nếu các phương pháp được gọi thông qua sự phản ánh có sẵn trong dotNet 4.5 hoặc 4.5.1. (Tôi sẽ tự kiểm tra nhưng muốn cung cấp cơ hội cho một nhà phát triển khác chia sẻ sự khôn ngoan của họ :) –

+2

@CameronPeters .Net 4.5 đã thêm BringIndexIntoViewPublic (http://msdn.microsoft.com/en-us/library/system.windows.controls .virtualizingpanel.bringindexintoviewpublic.aspx), vì vậy nếu bạn đang sử dụng .Net 4.5 hoặc mới hơn, bạn có thể gọi nó trực tiếp thay vì nhận được nó thông qua sự phản ánh. – splintor

+0

Các bạn thật tuyệt vời - Tôi đã thực hiện giải pháp này - nó hoạt động trong Windows 10 và nó cũng hoạt động với phương thức BringIndexIntoViewPublic trong .Net 4.5 - cool :-) Tôi đã sử dụng giao diện làm tham chiếu thay vì nút thực lớp học - vì vậy, tôi đoán mô hình không hoàn toàn bị phá vỡ. Tôi tự hỏi nếu có thay thế cho phương thức khác: EnsureGenerator và Property: ItemsHost trong phiên bản 4.5 trở lên? Tại sao bạn phải sử dụng các mục ẩn để làm điều gì đó phổ biến? – user3313608

1

Tôi đã giải quyết sự cố này bằng cách tạo các điều khiển tùy chỉnh cho TreeView, TreeViewItemVirtualizingStackPanel. Một phần của giải pháp là từ http://code.msdn.microsoft.com/Changing-selection-in-a-6a6242c8.

Mỗi mục TreeItem (mục ràng buộc) yêu cầu phải biết cha mẹ của nó (được thực thi bởi ITreeItem).

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

Khi IsSelected được đặt trên bất kỳ TreeItem, kiểu xem được thông báo và tăng sự kiện. Trình xử lý sự kiện tương ứng trong chế độ xem gọi BringItemIntoView trên TreeView.

TreeView tìm tất cả TreeViewItems trên đường dẫn đến mục đã chọn và đưa chúng vào chế độ xem.

Và đây phần còn lại của các mã:

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

Và trong XAML:

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