Saturday, November 6, 2010

How to Bind to Silverlight TreeView control's SelectedItem

I have no idea why the the Silverlight TreeView.SelectedItem Property is a read only Property. I know many people like me find needs to set this Property either in code or through binding.  What I have to do is to create a custom TreeView control which inherits from the original TreeView control, but with a new SelectedItem Property:

public class TreeViewEx : TreeView
{
        public TreeViewEx()
        {
            this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(TreeViewEx_SelectedItemChanged);
        }

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

        #region SelectedItem
      
        /// <summary>
        /// Gets or Sets the SelectedItem possible Value of the TreeViewItem object.
        /// </summary>
        public new object SelectedItem
        {
            get { return this.GetValue(TreeViewEx.SelectedItemProperty); }
            set { this.SetValue(TreeViewEx.SelectedItemProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public new static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), typeof(TreeViewEx), new PropertyMetadata(SelectedItemProperty_Changed));

        static void SelectedItemProperty_Changed(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            TreeViewEx targetObject = dependencyObject as TreeViewEx;
            if (targetObject != null)
            {
                TreeViewItem tvi = targetObject.FindItemNode(targetObject.SelectedItem) as TreeViewItem;
                if (tvi != null)
                    tvi.IsSelected = true;
            }
        }                                              
        #endregion SelectedItem  
 
        public TreeViewItem FindItemNode(object item)
        {
            TreeViewItem node = null;
            foreach (object data in this.Items)
            {
                node = this.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItem;
                if (node != null)
                {
                    if (data == item)
                        break;
                    node = FindItemNodeInChildren(node, item);
                    if (node != null)
                        break;
                }
            }
            return node;
        }

        protected TreeViewItem FindItemNodeInChildren(TreeViewItem parent, object item)
        {
            TreeViewItem node = null;
            bool isExpanded = parent.IsExpanded;
            if (!isExpanded) //Can't find child container unless the parent node is Expanded once
            {
                parent.IsExpanded = true;
                parent.UpdateLayout();
            }
            foreach (object data in parent.Items)
            {
                node = parent.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItem;
                if (data == item && node != null)
                    break;
                node = FindItemNodeInChildren(node, item);
                if (node != null)
                    break;
            }
            if (node == null && parent.IsExpanded != isExpanded)
                parent.IsExpanded = isExpanded;
            if (node != null)
                parent.IsExpanded = true;
            return node;
        }
}

Now the TreeViewEx.SelectedItem can be set either in code or through data binding.

Demo code can downloaded from here.

12 comments:

  1. Hi Sally,

    Your work is a true gem. I'm glad I searched the web before I implemented my own. One thing I notice though. The control does not behave accordingly if you manipulate the object source in the Loaded event of a page or any UI control that houses it. I found out that the ContainerFromItem(..) always return null in this case. It seems like the TreeViewItems are not created yet during these events. In a navigation or business app, I am able to rectify the issue by setting the DataContext in the OnNavigated and then pointing to the right item in the Loaded. I need to do this in our application because we have a feature loads the state of the UI when the app was last used.
    This is just a very small issue. Thank you very much for sharing your work!

    ReplyDelete
  2. Thnx.You save my time.

    ReplyDelete
  3. Really really nice. Thanks this is going to help me a ton.

    --tolga

    ReplyDelete
  4. Great work!

    However, one problem is that the background color changes to white and ignores whatever theme is applied.

    ReplyDelete
  5. This worked great! Thank you much!

    ReplyDelete
  6. great work, solved my issue which was not working from long time.

    ReplyDelete
  7. Finally I got the solution. Thanks a lot....

    ReplyDelete
  8. Hi, the sample link is broken. Can you please update it?

    ReplyDelete
  9. Here is the new link to the sample solution file:

    https://skydrive.live.com/?cid=9CFFD385FD75195B&id=9CFFD385FD75195B%21115

    Then you need to select SilverlightTreeViewEx to download.

    ReplyDelete
  10. none of the links are working to download????

    ReplyDelete
  11. Hello --
    I just found this, it looks very useful.
    I have (perhaps) a related problem I'm hoping you might have some ideas about...

    We are using a HierchicalDataTemplate for our TreeView.ItemTemplate, and it works very well.
    However, we need to handle the Expanded event for every TreeViewItem generated by the HDT.

    Any ideas on how to accomplish that?
    (The use case is a very tall and deep TreeView; for performance reasons, we only want to populate TreeViewItems when a node is expanded.)

    Thanks for any hints...

    ReplyDelete
  12. can any body help me to get the selected treeviewitem in MVVM, as Treeview does not have SelectedItem property?

    ReplyDelete