Monday, 25 October 2010

Silverlight Toolkit's TreeView SelectedItem problem.

For those who are trying to bind the TreeView's SelectedItem, you will quickly realise that it is a readonly property, which means you cannot bind that property as you would with a ComboBox for example.

One solution to that problem is to wrap each TreeView object in a class that exposes two properties, IsSelected and IsExpanded, which is subsequently binded to the TreeViewItem's IsSelected and IsExpanded properties respectively, and is easily implemented in WPF. Because in Silverlight, you cannot bind the property via the Setter property like WPF, one workaround is to extend the TreeView control and override the GetContainerForItemOverride() method to hardcode the IsSelected and IsExpanded bindings manually as seen in this blog here, which also provides good examples on how to expand all nodes, collapse all nodes etc...

The other solution is to create a simple attached property that you can bind to, which in my view, is a cleaner approach. This way you can bind it to a single object as the selected item without wrapping the view models with extra properties (IsSelected, IsExpanded) and managing them.
This idea is gotten from this blog here, which shows you how you can bind the selectedItem property from the ViewModel to the view, with some performance hit. I simply extended it further to include the binding from the View to the ViewModel so that I do not need to use any code behind, by adding a behavior to attach the selected item changed event to it. Use it like so (Omit AllowSelectedItemBinding if you only require one way binding aka. ViewModel to View) :-

local:TreeViewSelectedItem.SelectedItem="{Binding SelectedObjectToBind}" 
local:TreeViewSelectedItem.AllowSelectedItemBinding="True">
 using  System;
 using  System.Net;
 using  System.Windows;
 using  System.Windows.Controls;
 using  System.Windows.Documents;
 using  System.Windows.Ink;
 using  System.Windows.Input;
 using  System.Windows.Media;
 using  System.Windows.Media.Animation;
 using  System.Windows.Shapes;
 using  System.Collections;
 using  System.Collections.Generic;
 using  System.Collections.ObjectModel;
 using  System.Windows.Controls.Primitives;
 using  System.Collections.Specialized;
 
 namespace  Shared
 {
     public static class TreeViewSelectedItem 
     {
         /// <summary> 
         /// Enable selected item binding 
         /// </summary> 
         public static readonly DependencyProperty SelectedItemProperty =
                DependencyProperty.RegisterAttached("SelectedItem" , 
                 typeof(object), typeof(TreeViewSelectedItem),
                new PropertyMetadata(null, OnSelectedItemChanged));
 
         /// <summary> 
         /// If this value is not set, it will only allow one way binding from view model to ui 
         /// on the selected item 
         /// </summary> 
         public  static  DependencyProperty  AllowSelectedItemBindingProperty =
             DependencyProperty.RegisterAttached("AllowSelectedItemBinding" , 
                 typeof(bool), typeof(TreeViewSelectedItem ),
             new  PropertyMetadata (false, OnAllowSelectedItemBindingChanged));
 
         public static bool GetAllowSelectedItemBinding
             (FrameworkElement e)
         {
             return (bool)e.GetValue(AllowSelectedItemBindingProperty);
         }
 
         public static void SetAllowSelectedItemBinding(
             FrameworkElement e, bool value)
         {
             e.SetValue(AllowSelectedItemBindingProperty, value);
         }
 
         public static void SetSelectedItem(DependencyObject o, object propertyValue)
         {
             o.SetValue(SelectedItemProperty, propertyValue);
         }
 
         public static object  GetSelectedItem(DependencyObject o)
         {
             return o.GetValue(SelectedItemProperty);
         }
 
         private static void OnAllowSelectedItemBindingChanged(DependencyObject d, 
             DependencyPropertyChangedEventArgs e)
         {
             TreeView treeView = d as TreeView;
             if  (treeView == null)
             {
                 return;
             }
 
             CreateBehavior(treeView, (bool)e.NewValue);
         }
 
         private  static  void  OnSelectedItemChanged(DependencyObject d, 
             DependencyPropertyChangedEventArgs e)
         {
             TreeView treeView = d as TreeView;
             if  (treeView == null )
             {
                 return;
             }
 
             TreeViewItem item = 
                 treeView.ItemContainerGenerator.ContainerFromItem(e.NewValue) 
                     as TreeViewItem;
             if  (item != null)
             {
                 item.IsSelected = true;
                 item.UpdateLayout();
                 return;
             }
 
             for  (int  i = 0; i< treeView.Items.Count; i++)
             {
                 SelectItem(e.NewValue, 
                     treeView.ItemContainerGenerator.ContainerFromIndex(i) 
                         as  TreeViewItem);
             }
         }
 
         private static void SelectItem(object o, TreeViewItem parent)
         {
             if(parent == null)
             {
                 return ;
             }
 
             bool isExpanded = parent.IsExpanded;
             if(!isExpanded)
             {
                 parent.IsExpanded = true;
                 parent.UpdateLayout();
             }
 
             TreeViewItem item = 
                 parent.ItemContainerGenerator.ContainerFromItem(o) 
                     as TreeViewItem;
             if(item != null)
             {
                 item.IsSelected = true;
                 return;
             }
 
             for  (int i = 0; i< parent.Items.Count; i++)
             {
                 SelectItem(o, 
                     parent.ItemContainerGenerator.ContainerFromIndex(i) 
                         as TreeViewItem);
             }
 
             if(parent.IsExpanded != isExpanded)
             {
                 parent.IsExpanded = isExpanded;
             }
         }
 
         private static void CreateBehavior(TreeView target, bool attach)
         {
             if(attach)
             {
                 if(target != null)
                 {
                     target.SelectedItemChanged -= _treeView_SelectedItemChanged;
                     target.SelectedItemChanged += _treeView_SelectedItemChanged;
                 }
             }
             else  // detach 
             {
                 if(target != null)
                     target.SelectedItemChanged -= _treeView_SelectedItemChanged;
             }
         }
 
         private static void  _treeView_SelectedItemChanged(object d, 
             RoutedPropertyChangedEventArgs <object > e)
         {
             TreeView treeView = d as TreeView;
             treeView.SetValue(TreeViewSelectedItem.SelectedItemProperty, e.NewValue);
         }
     }
 }

3 comments:

Anonymous said...

Good post. I solved a problem I was having by seeing your code. I was setting .IsSelected before expanding all the parent nodes, causing the selection to fail.

Anonymous said...

It helps me a lot.. as i had wasted to much time to select desired Item and on the way to waste another time..but u save my time....

Anonymous said...

building selection for TreeView nodes