Elentok's Blog

About me

Data-binding the SelectedItem property of the WPF TreeView

In a new WPF application I'm writing I needed to bind the "SelectedItem" property of the TreeView control to the "SelectedItem" property of its ViewModel; but, alas, the TreeView's "SelectedItem" property is read-only...

After some googling I found the Versatile TreeView by Philip Sumi which adds the property I wanted, but since I'm learning WPF and want a deeper understanding I decided to write my own (and use Philip's TreeView as reference).

In the code I noticed a lot of dependency properties, and I figured its time to learn what are dependency properties and how do they work; again, some googling brought me to this post which I think gives a very good explanation of dependency properties.

Now that I knew what I had to do, I started writing the EnhancedTreeView class (which is based on the TreeView class).

At first I thought about overriding the SelectedItem property but it didn't go very smooth so I added the SelectedObject property.

I added a callback to the dependency-property-changed event (see SelectedObjectChangedCallback) and when the property was changed I needed to change the selected item; but, alas again!, the selected item is read-only...

Some more googling brought me to this blog post which uses the ItemsContainerGenerator to find the matching TreeViewItem for the object I want to select.

And here's the final code:

/// <summary>
/// This treeview class allows databinding the
/// "SelectedObject" property
/// </summary>
public class EnhancedTreeView : TreeView
{
  #region SelectedObject

  /// <summary>
  /// The dependency property that allows use to bind the
  /// "SelectedObject" property
  /// </summary>
  public static readonly DependencyProperty
    SelectedObjectProperty =
      DependencyProperty.Register(
        "SelectedObject",
        typeof(object),
        typeof(EnhancedTreeView),
        new PropertyMetadata(SelectedObjectChangedCallback));

  /// <summary>
  /// Gets or sets the select object (a writable version of
  /// the "SelectedItem" property)
  /// </summary>
  [Bindable(true)]
  public object SelectedObject
  {
    get { return GetValue(SelectedObjectProperty); }
    set { SetValue(SelectedObjectProperty, value); }
  }

  /// <summary>
  /// This method is called whenever ever the selected
  /// object is changed, and if it was changed from the
  /// outside, this method will set the selected item.
  /// </summary>
  /// <param name="obj"></param>
  /// <param name="eventArgs"></param>
  private static void SelectedObjectChangedCallback
    (DependencyObject obj,
     DependencyPropertyChangedEventArgs eventArgs)
  {
    EnhancedTreeView treeView = (EnhancedTreeView)obj;
    if (!ReferenceEquals(treeView.SelectedItem,
          eventArgs.NewValue))
      SelectItem(treeView, eventArgs.NewValue);
  }

  #endregion

  /// <summary>
  /// Searches the given item in the parent (recursively)
  /// and selects it, returns true if the item was found
  /// and selected, false otherwise.
  /// </summary>
  /// <param name="parent"></param>
  /// <param name="itemToSelect"></param>
  /// <returns></returns>
  private static bool SelectItem
    (ItemsControl parent, object itemToSelect)
  {
    var childTreeNode =
      parent.ItemContainerGenerator
        .ContainerFromItem(itemToSelect)
      as TreeViewItem;

    // if the item to select is directly under "parent",
    // just select it
    if (childTreeNode != null)
    {
      childTreeNode.Focus();
      return childTreeNode.IsSelected = true;
    }

    // if the item to select is not directly under "parent",
    // search the child nodes of "parent"
    if (parent.Items.Count > 0)
    {
      foreach (object childItem in parent.Items)
      {
        var childItemsControl =
          parent.ItemContainerGenerator
            .ContainerFromItem(childItem)
          as ItemsControl;

        if (SelectItem(childItemsControl, itemToSelect))
          return true;
      }
    }

    // if the given item wasn't found here:
    return false;
  }

  /// <summary>
  /// When the selected item is updated from inside the tree,
  /// this method will update the "SelectedObject" property.
  /// </summary>
  /// <param name="e"></param>
  protected override void OnSelectedItemChanged
    (RoutedPropertyChangedEventArgs<object> e)
  {
    this.SelectedObject = e.NewValue;

    base.OnSelectedItemChanged(e);
  }
}

To use the EnhancedTreeView all you need is to bind the SelectedObject property:

<controls:EnhancedTreeView
  SelectedObject="{Binding SelectedObject, Mode=TwoWay}"
  ItemsSource="..." />

It's important to add Mode=TwoWay to the binding, otherwise it won't work.

Next:Making the items in a WPF ItemsControl stretch