在应用了 HierarchicalDataTemplate 的 WPF TreeView 中绑定 SelectedItem

Posted

技术标签:

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

我有一个数据绑定TreeView,我想绑定SelectedItem。 This 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,但我不知道如何到达那里。有谁知道如何以及它是正确的方式吗?

【问题讨论】:

【参考方案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 中,并且我还在下面添加了它作为附加答案 (***.com/a/27447702/208817)。

以上是关于在应用了 HierarchicalDataTemplate 的 WPF TreeView 中绑定 SelectedItem的主要内容,如果未能解决你的问题,请参考以下文章

应用程序调试 - 是啥决定了我的应用程序在后台停留多长时间?

在拒绝 iOS 应用程序后,Apple 为重新提交应用程序提供了多长时间?

如何知道在 django 中使用了哪个版本的应用程序 [重复]

在应用程序被杀死并重新启动后,Android 应用程序黑暗主题消失了

在两个不同的应用程序之间共享非消耗性应用内购买是不是违反了 App Store 指南?

Apple在应用程序中拒绝了facebook登录应用程序