12

我有一個數據綁定TreeView,我想綁定SelectedItemThis attached behavior完美地工作,沒有HierarchicalDataTemplate,但與它相關的行爲只能用一種方式(UI到數據)而不是另一種,因爲現在e.NewValueMyViewModel而不是TreeViewItem在HierarchicalDataTemplate應用的WPF中綁定SelectedItem TreeView

這是從附加的行爲的代碼片段:

private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) 
{ 
    var item = e.NewValue as TreeViewItem; 
    if (item != null) 
    { 
     item.SetValue(TreeViewItem.IsSelectedProperty, true); 
    } 
} 

這是我TreeView定義:

<Window xmlns:interactivity="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"> 
    <TreeView ItemsSource="{Binding MyItems}" VirtualizingStackPanel.IsVirtualizing="True"> 
     <interactivity:Interaction.Behaviors> 
      <behaviors:TreeViewSelectedItemBindingBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}" /> 
     </interactivity:Interaction.Behaviors> 
     <TreeView.Resources> 
      <HierarchicalDataTemplate DataType="{x:Type local:MyViewModel}" ItemsSource="{Binding Children}"> 
       <TextBlock Text="{Binding Name}"/> 
      </HierarchicalDataTemplate> 
     </TreeView.Resources> 
    </TreeView> 
</Window> 

如果我能在附加的行爲方法OnSelectedItemChangedTreeView參考,也許我可以使用this question中的答案獲取TreeViewItem,但我不知道如何到達那裏。有誰知道如何,是否正確的路要走?

回答

18

這裏是上述附加行爲的改進版本。它完全支持雙向綁定,並且可以與HeriarchicalDataTemplateTreeView s一起使用,其中的項目被虛擬化。請注意,雖然要找到需要選擇的'TreeViewItem',但它會實現(即創建)虛擬化,直到找到合適的虛擬化對象。這可能是大型虛擬化樹的性能問題。

/// <summary> 
///  Behavior that makes the <see cref="System.Windows.Controls.TreeView.SelectedItem" /> bindable. 
/// </summary> 
public class BindableSelectedItemBehavior : Behavior<TreeView> 
{ 
    /// <summary> 
    ///  Identifies the <see cref="SelectedItem" /> dependency property. 
    /// </summary> 
    public static readonly DependencyProperty SelectedItemProperty = 
     DependencyProperty.Register(
      "SelectedItem", 
      typeof(object), 
      typeof(BindableSelectedItemBehavior), 
      new UIPropertyMetadata(null, OnSelectedItemChanged)); 

    /// <summary> 
    ///  Gets or sets the selected item of the <see cref="TreeView" /> that this behavior is attached 
    ///  to. 
    /// </summary> 
    public object SelectedItem 
    { 
     get 
     { 
      return this.GetValue(SelectedItemProperty); 
     } 

     set 
     { 
      this.SetValue(SelectedItemProperty, value); 
     } 
    } 

    /// <summary> 
    ///  Called after the behavior is attached to an AssociatedObject. 
    /// </summary> 
    /// <remarks> 
    ///  Override this to hook up functionality to the AssociatedObject. 
    /// </remarks> 
    protected override void OnAttached() 
    { 
     base.OnAttached(); 
     this.AssociatedObject.SelectedItemChanged += this.OnTreeViewSelectedItemChanged; 
    } 

    /// <summary> 
    ///  Called when the behavior is being detached from its AssociatedObject, but before it has 
    ///  actually occurred. 
    /// </summary> 
    /// <remarks> 
    ///  Override this to unhook functionality from the AssociatedObject. 
    /// </remarks> 
    protected override void OnDetaching() 
    { 
     base.OnDetaching(); 
     if (this.AssociatedObject != null) 
     { 
      this.AssociatedObject.SelectedItemChanged -= this.OnTreeViewSelectedItemChanged; 
     } 
    } 

    private static Action<int> GetBringIndexIntoView(Panel itemsHostPanel) 
    { 
     var virtualizingPanel = itemsHostPanel as VirtualizingStackPanel; 
     if (virtualizingPanel == null) 
     { 
      return null; 
     } 

     var method = virtualizingPanel.GetType().GetMethod(
      "BringIndexIntoView", 
      BindingFlags.Instance | BindingFlags.NonPublic, 
      Type.DefaultBinder, 
      new[] { typeof(int) }, 
      null); 
     if (method == null) 
     { 
      return null; 
     } 

     return i => method.Invoke(virtualizingPanel, new object[] { i }); 
    } 

    /// <summary> 
    /// Recursively search for an item in this subtree. 
    /// </summary> 
    /// <param name="container"> 
    /// The parent ItemsControl. This can be a TreeView or a TreeViewItem. 
    /// </param> 
    /// <param name="item"> 
    /// The item to search for. 
    /// </param> 
    /// <returns> 
    /// The TreeViewItem that contains the specified item. 
    /// </returns> 
    private static TreeViewItem GetTreeViewItem(ItemsControl container, object item) 
    { 
     if (container != null) 
     { 
      if (container.DataContext == item) 
      { 
       return container as TreeViewItem; 
      } 

      // Expand the current container 
      if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded) 
      { 
       container.SetValue(TreeViewItem.IsExpandedProperty, true); 
      } 

      // Try to generate the ItemsPresenter and the ItemsPanel. 
      // by calling ApplyTemplate. Note that in the 
      // virtualizing case even if the item is marked 
      // expanded we still need to do this step in order to 
      // regenerate the visuals because they may have been virtualized away. 
      container.ApplyTemplate(); 
      var itemsPresenter = 
       (ItemsPresenter)container.Template.FindName("ItemsHost", container); 
      if (itemsPresenter != null) 
      { 
       itemsPresenter.ApplyTemplate(); 
      } 
      else 
      { 
       // The Tree template has not named the ItemsPresenter, 
       // so walk the descendents and find the child. 
       itemsPresenter = container.GetVisualDescendant<ItemsPresenter>(); 
       if (itemsPresenter == null) 
       { 
        container.UpdateLayout(); 
        itemsPresenter = container.GetVisualDescendant<ItemsPresenter>(); 
       } 
      } 

      var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0); 

      // Ensure that the generator for this panel has been created. 
#pragma warning disable 168 
      var children = itemsHostPanel.Children; 
#pragma warning restore 168 

      var bringIndexIntoView = GetBringIndexIntoView(itemsHostPanel); 
      for (int i = 0, count = container.Items.Count; i < count; i++) 
      { 
       TreeViewItem subContainer; 
       if (bringIndexIntoView != null) 
       { 
        // Bring the item into view so 
        // that the container will be generated. 
        bringIndexIntoView(i); 
        subContainer = 
         (TreeViewItem)container.ItemContainerGenerator. 
               ContainerFromIndex(i); 
       } 
       else 
       { 
        subContainer = 
         (TreeViewItem)container.ItemContainerGenerator. 
               ContainerFromIndex(i); 

        // Bring the item into view to maintain the 
        // same behavior as with a virtualizing panel. 
        subContainer.BringIntoView(); 
       } 

       if (subContainer == null) 
       { 
        continue; 
       } 

       // Search the next level for the object. 
       var resultContainer = GetTreeViewItem(subContainer, item); 
       if (resultContainer != null) 
       { 
        return resultContainer; 
       } 

       // The object is not under this TreeViewItem 
       // so collapse it. 
       subContainer.IsExpanded = false; 
      } 
     } 

     return null; 
    } 

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) 
    { 
     var item = e.NewValue as TreeViewItem; 
     if (item != null) 
     { 
      item.SetValue(TreeViewItem.IsSelectedProperty, true); 
      return; 
     } 

     var behavior = (BindableSelectedItemBehavior)sender; 
     var treeView = behavior.AssociatedObject; 
     if (treeView == null) 
     { 
      // at designtime the AssociatedObject sometimes seems to be null 
      return; 
     } 

     item = GetTreeViewItem(treeView, e.NewValue); 
     if (item != null) 
     { 
      item.IsSelected = true; 
     } 
    } 

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) 
    { 
     this.SelectedItem = e.NewValue; 
    } 
} 

而對於完整性票數的目的是GetVisualDescentants實現:

/// <summary> 
///  Extension methods for the <see cref="DependencyObject" /> type. 
/// </summary> 
public static class DependencyObjectExtensions 
{ 
    /// <summary> 
    ///  Gets the first child of the specified visual that is of tyoe <typeparamref name="T" /> 
    ///  in the visual tree recursively. 
    /// </summary> 
    /// <param name="visual">The visual to get the visual children for.</param> 
    /// <returns> 
    ///  The first child of the specified visual that is of tyoe <typeparamref name="T" /> of the 
    ///  specified visual in the visual tree recursively or <c>null</c> if none was found. 
    /// </returns> 
    public static T GetVisualDescendant<T>(this DependencyObject visual) where T : DependencyObject 
    { 
     return (T)visual.GetVisualDescendants().FirstOrDefault(d => d is T); 
    } 

    /// <summary> 
    ///  Gets all children of the specified visual in the visual tree recursively. 
    /// </summary> 
    /// <param name="visual">The visual to get the visual children for.</param> 
    /// <returns>All children of the specified visual in the visual tree recursively.</returns> 
    public static IEnumerable<DependencyObject> GetVisualDescendants(this DependencyObject visual) 
    { 
     if (visual == null) 
     { 
      yield break; 
     } 

     for (var i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++) 
     { 
      var child = VisualTreeHelper.GetChild(visual, i); 
      yield return child; 
      foreach (var subChild in GetVisualDescendants(child)) 
      { 
       yield return subChild; 
      } 
     } 
    } 
} 
+2

我如何使用GetVisualDescendant方法?我添加了對PresentationFramework的引用,但仍然無法使用?我錯過了什麼? – Lukas

+4

GetVisualDescendant方法是一個拖放實用程序中使用的擴展方法[實現](https://gong-wpf-dragdrop.googlecode.com/svn-history/r29/branches/jon/GongSolutions.Wpf.DragDrop /Utilities/VisualTreeExtensions.cs),這就是我找到它的地方。 – Xtr

+0

工程就像一個魅力。非常好的解決方案來擴展TreeView控件的糟糕的mvvm功能。 –

3

我知道這是老問題,但也許這將是對他人有幫助的。我結合從Link

一個代碼,它現在看起來:

using System.Windows; 
using System.Windows.Controls; 
using System.Windows.Interactivity; 
using System.Windows.Media; 

namespace Behaviors 
{ 
    public class BindableSelectedItemBehavior : Behavior<TreeView> 
    { 
     #region SelectedItem Property 

     public object SelectedItem 
     { 
      get { return (object)GetValue(SelectedItemProperty); } 
      set { SetValue(SelectedItemProperty, value); } 
     } 

     public static readonly DependencyProperty SelectedItemProperty = 
      DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged)); 

     private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) 
     { 
      // if binded to vm collection than this way is not working 
      //var item = e.NewValue as TreeViewItem; 
      //if (item != null) 
      //{ 
      // item.SetValue(TreeViewItem.IsSelectedProperty, true); 
      //} 

      var tvi = e.NewValue as TreeViewItem; 
      if (tvi == null) 
      { 
       var tree = ((BindableSelectedItemBehavior)sender).AssociatedObject; 
       tvi = GetTreeViewItem(tree, e.NewValue); 
      } 
      if (tvi != null) 
      { 
       tvi.IsSelected = true; 
       tvi.Focus(); 
      } 
     } 

     #endregion 

     #region Private 

     private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) 
     { 
      SelectedItem = e.NewValue; 
     } 

     private static TreeViewItem GetTreeViewItem(ItemsControl container, object item) 
     { 
      if (container != null) 
      { 
       if (container.DataContext == item) 
       { 
        return container as TreeViewItem; 
       } 

       // Expand the current container 
       if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded) 
       { 
        container.SetValue(TreeViewItem.IsExpandedProperty, true); 
       } 

       // Try to generate the ItemsPresenter and the ItemsPanel. 
       // by calling ApplyTemplate. Note that in the 
       // virtualizing case even if the item is marked 
       // expanded we still need to do this step in order to 
       // regenerate the visuals because they may have been virtualized away. 

       container.ApplyTemplate(); 
       var itemsPresenter = 
        (ItemsPresenter)container.Template.FindName("ItemsHost", container); 
       if (itemsPresenter != null) 
       { 
        itemsPresenter.ApplyTemplate(); 
       } 
       else 
       { 
        // The Tree template has not named the ItemsPresenter, 
        // so walk the descendents and find the child. 
        itemsPresenter = FindVisualChild<ItemsPresenter>(container); 
        if (itemsPresenter == null) 
        { 
         container.UpdateLayout(); 
         itemsPresenter = FindVisualChild<ItemsPresenter>(container); 
        } 
       } 

       var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0); 

       // Ensure that the generator for this panel has been created. 
#pragma warning disable 168 
       var children = itemsHostPanel.Children; 
#pragma warning restore 168 

       for (int i = 0, count = container.Items.Count; i < count; i++) 
       { 
        var subContainer = (TreeViewItem)container.ItemContainerGenerator. 
                  ContainerFromIndex(i); 
        if (subContainer == null) 
        { 
         continue; 
        } 

        subContainer.BringIntoView(); 

        // Search the next level for the object. 
        var resultContainer = GetTreeViewItem(subContainer, item); 
        if (resultContainer != null) 
        { 
         return resultContainer; 
        } 
        else 
        { 
         // The object is not under this TreeViewItem 
         // so collapse it. 
         //subContainer.IsExpanded = false; 
        } 
       } 
      } 

      return null; 
     } 

     /// <summary> 
     /// Search for an element of a certain type in the visual tree. 
     /// </summary> 
     /// <typeparam name="T">The type of element to find.</typeparam> 
     /// <param name="visual">The parent element.</param> 
     /// <returns></returns> 
     private static T FindVisualChild<T>(Visual visual) where T : Visual 
     { 
      for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++) 
      { 
       Visual child = (Visual)VisualTreeHelper.GetChild(visual, i); 
       if (child != null) 
       { 
        T correctlyTyped = child as T; 
        if (correctlyTyped != null) 
        { 
         return correctlyTyped; 
        } 

        T descendent = FindVisualChild<T>(child); 
        if (descendent != null) 
        { 
         return descendent; 
        } 
       } 
      } 

      return null; 
     } 

     #endregion 

     #region Protected 

     protected override void OnAttached() 
     { 
      base.OnAttached(); 

      AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged; 
     } 

     protected override void OnDetaching() 
     { 
      base.OnDetaching(); 

      if (AssociatedObject != null) 
      { 
       AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged; 
      } 
     } 

     #endregion 
    } 
} 
+3

噢,有人認爲我的代碼很有用:-) –

+0

有時候我們到'var itemsHostPanel =(Panel)VisualTreeHelper.GetChild(itemsPresenter,0);'和'itemPresenter'爲null。有什麼想法嗎? – MoonKnight

+0

@Killercam你打我吧:)我猜你發現雙子座的同樣的錯誤,因爲我現在正在調查... –

2

如果你發現,像我一樣,是this answer有時會崩潰,因爲itemPresenter爲null,則此修改該解決方案可以爲你工作。

變化OnSelectedItemChanged本(如果樹尚未加載,然後等待,直到樹加載並再次嘗試):

private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) 
{ 
    Action<TreeViewItem> selectTreeViewItem = tvi2 => 
    { 
     if (tvi2 != null) 
     { 
      tvi2.IsSelected = true; 
      tvi2.Focus(); 
     } 
    }; 

    var tvi = e.NewValue as TreeViewItem; 

    if (tvi == null) 
    { 
     var tree = ((BindableTreeViewSelectedItemBehavior) sender).AssociatedObject; 
     if (!tree.IsLoaded) 
     { 
      RoutedEventHandler handler = null; 
      handler = (sender2, e2) => 
      { 
       tvi = GetTreeViewItem(tree, e.NewValue); 
       selectTreeViewItem(tvi); 
       tree.Loaded -= handler; 
      }; 
      tree.Loaded += handler; 

      return; 
     } 
     tvi = GetTreeViewItem(tree, e.NewValue); 
    } 

    selectTreeViewItem(tvi); 
}