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.