【问题标题】:Binding SelectedItem in a HierarchicalDataTemplate-applied WPF TreeView在应用了 HierarchicalDataTemplate 的 WPF TreeView 中绑定 SelectedItem
【发布时间】:2012-06-19 10:16:22
【问题描述】:

我有一个数据绑定TreeView,我想绑定SelectedItemThis attached behavior 在没有 HierarchicalDataTemplate 的情况下可以完美运行,但附加的行为只能以一种方式(UI 到数据)起作用,因为现在 e.NewValueMyViewModel 而不是 TreeViewItem

这是来自附加行为的代码 sn-p:

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>

如果我可以在附加的行为方法OnSelectedItemChanged 中获得对TreeView 的引用,也许我可以使用this question 中的答案来获得TreeViewItem,但我不知道如何到达那里。有谁知道如何以及它是正确的方式吗?

【问题讨论】:

    标签: wpf data-binding treeview treeviewitem hierarchicaldatatemplate


    【解决方案1】:

    这是上述附加行为的改进版本。它完全支持双向绑定,还可以与HeriarchicalDataTemplateTreeViews 一起使用,其中它的项目是虚拟化的。请注意,虽然要找到需要选择的“TreeViewItem”,但它会实现(即创建)虚拟化的TreeViewItems,直到找到正确的。这可能是大型虚拟树的性能问题。

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

    【讨论】:

    • 如何使用 GetVisualDescendant 方法?我添加了对 PresentationFramework 的引用,但仍然无法使用?我错过了什么?
    • GetVisualDescendant 方法是在拖放工具中使用的扩展方法implementation,反正我就是在那儿找到的。
    • 像魅力一样工作。扩展 TreeView 控件的不良 mvvm 功能的非常好的解决方案。
    • @peter 请查看 Xtr 的 avove 评论中的链接
    • @bitbonk,请原谅我的愚蠢。我已经看到了那个链接,但是把那个代码放在哪里?
    【解决方案2】:

    如果您像我一样发现 this answer 有时会因为 itemPresenter 为空而崩溃,那么对该解决方案的修改可能对您有用。

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

    【讨论】:

      【解决方案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
          }
      }
      

      【讨论】:

      • 有时我们到达var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);,而itemPresenter 为空。有什么想法吗?
      • @Killercam 你打败了我 :) 我猜你在 Gemini 中发现了与我现在正在调查的相同的错误......
      • 问题似乎是 GetVisualChild 并不总是有效,因为可视化树在调用时并不总是完全加载。
      • 嗨@TimJones,我想我已经通过稍微重写这个行为来解决这个问题。我很想看看你的解决方案,你合并到 Gemini 了吗?
      • 我已将它合并到 Gemini 中,并且我还在下面添加了它作为附加答案 (stackoverflow.com/a/27447702/208817)。
      猜你喜欢
      • 1970-01-01
      • 2011-09-12
      • 1970-01-01
      • 2013-02-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-01
      相关资源
      最近更新 更多