Thursday, 18 April 2013

MVVM - Binding to TreeView.SelectedItem

At first, binding to the TreeView's SelectedItem property sounds like a simple task, but if you give it a go you will soon realise it's not so straight forward. That's because the WPF TreeView's SelectedItem property is read-only so we can't set the binding in XAML like normal.

So how can we access the selected item from our ViewModel? The answer is with an attached property. Defining custom attached properties is a handy technique for MVVM that allows us to bind properties of a ViewModel to otherwise inaccessible parts of our View such as read-only properties and events. I'm sure you will see more of these in my future posts.

The basic principle behind this technique is we define a custom property that we can attach to our TreeView in XAML. We can then bind to this property from our ViewModel. The clever part happens within our property implementation, here is where we subscribe to the TreeView.SelectedItemChanged event. When this event is fired we set the value of our attached property to the value of the SelectedItem and this automatically updates our ViewModel. Here's the code for our attached property;

  1. using System.Windows;
  2. using System.Windows.Controls;
  3.  
  4. namespace JourneyIntoCode.Behaviours
  5. {
  6.     public class TreeViewSelectedItemBehaviour
  7.     {
  8.         // Declare our attached property, it needs to be a DependencyProperty so
  9.         // we can bind to it from oout ViewMode.
  10.         public static readonly DependencyProperty TreeViewSelectedItemProperty =
  11.             DependencyProperty.RegisterAttached(
  12.             "TreeViewSelectedItem",
  13.             typeof(object),
  14.             typeof(TreeViewSelectedItemBehaviour),
  15.             new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
  16.                 new PropertyChangedCallback(TreeViewSelectedItemChanged)));
  17.  
  18.         // We need a Get method for our new property
  19.         public static object GetTreeViewSelectedItem(DependencyObject dependencyObject)
  20.         {
  21.             return (object)dependencyObject.GetValue(TreeViewSelectedItemProperty);
  22.         }
  23.  
  24.         // As well as a Set method for our new property
  25.         public static void SetTreeViewSelectedItem(
  26.           DependencyObject dependencyObject, object value)
  27.         {
  28.             dependencyObject.SetValue(TreeViewSelectedItemProperty, value);
  29.         }
  30.  
  31.         // This is the handler for when our new property's value changes
  32.         // When our property is set to a non null value we need to add an event handler
  33.         // for the TreeView's SelectedItemChanged event
  34.         private static void TreeViewSelectedItemChanged(DependencyObject dependencyObject,
  35.             DependencyPropertyChangedEventArgs e)
  36.         {
  37.             TreeView tv = dependencyObject as TreeView;
  38.  
  39.             if (e.NewValue == null && e.OldValue != null)
  40.             {
  41.                 tv.SelectedItemChanged -=
  42.                     new RoutedPropertyChangedEventHandler<object>(tv_SelectedItemChanged);
  43.             }
  44.             else if (e.NewValue != null && e.OldValue == null)
  45.             {
  46.                 tv.SelectedItemChanged +=
  47.                     new RoutedPropertyChangedEventHandler<object>(tv_SelectedItemChanged);
  48.             }
  49.         }
  50.  
  51.         // When TreeView.SelectedItemChanged fires, set our new property to the value
  52.         static void tv_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
  53.         {
  54.             SetTreeViewSelectedItem((DependencyObject)sender, e.NewValue);
  55.         }
  56.     }
  57. }

In our XAML (building on from my last post on TreeViews) we can attach and bind to our property like so;

  1. <TreeView ItemsSource="{Binding Children}"
  2.           behaviours:TreeViewSelectedItemBehaviour.TreeViewSelectedItem="{Binding SelectedItem}">
  3.     <TreeView.Resources>
  4.         <HierarchicalDataTemplate DataType="{x:Type local:DirectoryViewModel}"
  5.                                   ItemsSource="{Binding Children}">
  6.             <TextBlock Text="{Binding Name}"/>
  7.         </HierarchicalDataTemplate>
  8.         <DataTemplate DataType="{x:Type local:FileViewModel}">
  9.             <TextBlock Text="{Binding Name}"/>
  10.         </DataTemplate>
  11.     </TreeView.Resources>
  12. </TreeView>

No comments:

Post a comment