Andrej Tozon's blog

In the Attic

NAVIGATION - SEARCH

Making the Silverlight TreeView bindable two-way

One of the most common scenarios in LOB applications is a list control, displaying some sort of items, and clicking on an item provides the user with some details about selected item. This is called a Master-detail scenario. Take Microsoft Outlook, as a typical three level example [Folder-Mail-Content]. I’m going to implement this scenario with Silverlight Toolkit’s TreeView using the MVVM pattern.

I’ll use the same PageViewModel, used in the first post of my TreeView Editing series and begin working on the user interface, first using a ListBox, not the TreeView. The PageViewModel is, again, set as the DataContext of the main page.

ListBox

Selecting a help topic from the list will get its description shown in a TextBlock below the ListBox. How this works is that when an item is selected, the ViewModel is notified. The ViewModel then gets the selected item’s details and notifies the TextBlock when the details are available. Sounds complicated? It’s not, really.

Let’s do this the easy way – I’m going to use the HelpTopic class as a list item and as a detail. That means both the ListBox and the TextBlock will be bound to the new SelectedTopic property on the ViewModel:

private HelpTopic selectedTopic;
public HelpTopic SelectedTopic
{
    get { return selectedTopic; }
    set
    {
        if (selectedTopic == value)
        {
            return;
        }
        selectedTopic = value;
        OnPropertyChanged("SelectedTopic");
    }
}

with ListBox and TextBlock bound as displayed in this parts of Xaml:

<ListBox ItemsSource="{Binding HelpTopics}" DisplayMemberPath="Name"
SelectedItem="{Binding SelectedTopic, Mode=TwoWay}" />

<TextBlock Text="{Binding SelectedTopic.Name}" />

Now let’s add a Tree and bind it the same way as the ListBox. Here’s the complete Xaml:

<StackPanel>
    <StackPanel Orientation="Horizontal">
        <ListBox ItemsSource="{Binding HelpTopics}" DisplayMemberPath="Name" 
                 SelectedItem="{Binding SelectedTopic, Mode=TwoWay}" Width="300"
                 HorizontalAlignment="Stretch" /> <slt:TreeView VerticalAlignment="Stretch" ItemsSource="{Binding HelpTopics}"
                      SelectedItem="{Binding SelectedTopic, Mode=TwoWay}" Width="300"> <slt:TreeView.ItemTemplate> <slt:HierarchicalDataTemplate ItemsSource="{Binding SubTopics}"> <TextBlock Text="{Binding Name}" VerticalAlignment="Center" /> </slt:HierarchicalDataTemplate> </slt:TreeView.ItemTemplate> </slt:TreeView> </StackPanel> <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Stretch"> <StackPanel Orientation="Horizontal"> <TextBlock Text="You selected: " Margin="4" /> <TextBlock Text="{Binding SelectedTopic.Name}" VerticalAlignment="Center" Margin="4" /> </StackPanel> </Border> </StackPanel>

We have two controllers now (ListBox and TreeView). But let’s observe how they like being controlled.

image

image

TreeView differs from the ListBox in having a private SelectedItem property setter, which makes two-way binding impossible. Almost impossible anyway, there is a way around it.

Let’s extend the TreeView by creating a new attached property which will provide us with “the-missing-way binding”, needed to update the TreeView from the ViewModel:

public class SelectionService
{
    public static readonly DependencyProperty SelectedItemProperty = 
           DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(SelectionService),
           new PropertyMetadata(null, OnSelectedItemChanged)); 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 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) { return; } item.IsSelected = true; } }

There’s really just two lines of code that actually do anything. In the OnSelectedItemChangedMethod, I’m getting the container TreeViewItem of the selected HelpTopic and set its IsSelected property to true.

To attach this property to the TreeView, add the following to the above-defined TreeView:

<slt:TreeView ... local:SelectionService.SelectedItem="{Binding SelectedTopic}">

There’s however two minor issues to this approach… TreeView’s native SelectedItem property is still two-way bound so when ViewModel tries to call its private setter, an exception is still thrown, which may affect performance. What we would need here is a OneWayToSource type binding, which exists in WPF, but unfortunately not in Silverlight.

The other issue is that the above code only works for the root level. If you want to select any node in the hierarchy, you would traverse the tree unit you find the one that should be selected. But… the TreeView creates TreeViewItems only when needed (when their parent node is expanded). In order to fix this, each item has to be expanded before inspecting their children and then collapsed, if that was its original state. Additionally, this approach can be even more time consuming. Let’s look at the quick and dirty implementation:

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;
        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;
    }
}

OK, now we have a two-way bindable TreeView, playing nice with the ViewModel, but with some performance hit for that “other-way binding”. I’m sure the Silverlight Toolkit guys would make this much more performant, so if you would like to see TreeView’s SelectedItem property to be made public, you can vote here.