Binding SelectedItem in a HierarchicalDataTemplate-applied WPF TreeView
Asked Answered
N

3

16

I have a data-bound TreeView and I want to bind SelectedItem. This attached behavior works perfectly without HierarchicalDataTemplate but with it the attached behavior only works one way (UI to data) not the other because now e.NewValue is MyViewModel not TreeViewItem.

This is a code snippet from the attached behavior:

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

This is my TreeView definition:

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

If I can get a reference to the TreeView in the attached behavior method OnSelectedItemChanged, maybe I can use the answers in this question to get the TreeViewItem but I don't know how to get there. Does anyone know how and is it the right way to go?

Noli answered 16/6, 2012 at 18:27 Comment(0)
F
23

Here is an improved version of the above mentioned attached behavior. It fully supports twoway binding and also works with HeriarchicalDataTemplate and TreeViews where its items are virtualized. Please note though that to find the 'TreeViewItem' that needs to be selected, it will realize (i.e. create) the virtualized TreeViewItems until it finds the right one. This could potentially be a performance problem with big virtualized trees.

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

And for the sake of completeness hier is the implementation of 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;
            }
        }
    }
}
Faso answered 9/9, 2013 at 14:4 Comment(7)
how can I use GetVisualDescendant method? I added reference to PresentationFramework, but still unable to use? What I am missing?Suave
The GetVisualDescendant method is an extension method used in the utils of a drag and drop implementation, that's where I found it anyway.Lioness
Works like a charm. Very good solution to extend poor mvvm capabilities of the TreeView Control.Weigel
@peter please see the link in the avove comment by XtrFaso
@bitbonk, please excuse my stupidity.. I have seen that link, but where to put that code?Dinesen
That implementation link is broken, could anyone post the implementation of GetVisualDescendant?Resolvent
My view model exposes both the treeview ItemsSource ObservableCollection and the SelectedItem source property. When changing the view's datacontext back and forth between several instances of my view model the selection is lost althought each vm instance keeps its own selected item reference. @Faso Am I missing something?Lehman
S
3

I know this is old question, but perhaps it will be helpful for others. I combined a code from Link

And it looks now:

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
    }
}
Suave answered 17/12, 2013 at 13:55 Comment(5)
Sometimes we get to var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0); and the itemPresenter is null. Any thoughts?Eileneeilis
@Killercam you beat me to it :) I'm guessing you found the same bug in Gemini as I'm now investigating...Cora
The problem seems to be that GetVisualChild doesn't always work, because the visual tree hasn't always completely loaded by the time it's called.Cora
Hi @TimJones, I think I have fixed this by slightly rewriting this behavior. I would be interested to see your solution, have you merged into Gemini yet?Eileneeilis
I have merged it into Gemini, and I've also added it as an additional answer below (https://mcmap.net/q/110976/-binding-selecteditem-in-a-hierarchicaldatatemplate-applied-wpf-treeview).Cora
C
3

If you find, like I did, that this answer sometimes crashes because itemPresenter is null, then this modification to that solution might work for you.

Change OnSelectedItemChanged to this (if the Tree isn't loaded yet, then it waits until the Tree is loaded and tries again):

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);
}
Cora answered 12/12, 2014 at 16:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.