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.

Comments

Anonymous said…
Nice work. I needed something like this to complete a "search" feature. This worked great... as soon as I remembered to go back to the Xaml and add the Mode=TwoWay. In fact, it stuck out when I read the code, but I dismissed it until, through debugging, I noticed the One Way behavior.
Anonymous said…
> It's important to add "Mode=TwoWay" to the binding, otherwise it won't work.

Try

new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
SelectedObjectChangedCallback)

Popular posts from this blog

Restart the Windows File Sharing Service to fix weird problems

WPF, ImageSource and Embedded Resources

SharpDevelop dark color scheme