I got an email the other day from a friend who was having some trouble getting the WPF TreeView to do what he wanted to do.  He had a master-detail relationship between Doctors and Patients, and wanted to add some additional information about each Doctor and Patient to he TreeView via the Expander control.  He was having problems getting these thee parts implemented, so I’d said I would help him out and take a look.  I’m documenting what I found here not only for him, but also for me (grin) as well as anyone else that encounters problems like these and wants to find a quick solution in the community.

Luckily for us, the first part – implementing the hierarchical data binding - was relatively straight forward.  The WPF TreeView component natively supports hierarchical data sources, such as our collection of Doctor objects that each had a collection of Patient objects.  This 2-level object hierarchy fits well into the typical tree control scenario.  To implement our binding, all you need to do is specify a HierarchicalDataTemplate for the TreeViewItems that have children, and a regluar old DataTemplate for those that do not.

<!--Patient Content Template-->
<DataTemplate x:Key="PatientTemplate">
    <Border BorderBrush="AliceBlue" BorderThickness="1" CornerRadius="10"
            Background="{StaticResource TreeViewItemBackground}" >
        <Expander HeaderTemplate="{DynamicResource PatientHeaderTemplate}" 
                  Header="{Binding}" IsTabStop="False" HorizontalAlignment="Left" 
                  IsEnabled="True" ExpandDirection="Down">
            <Grid Margin="5,5,5,5">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <TextBlock Text="{Binding Path=GestationalAge}" 
                           Grid.Column="0" Grid.Row="0" />
                <TextBlock Text="{Binding Path=BirthDate.Year}" 
                           Grid.Column="1" Grid.Row="0" />
                <TextBlock Text="{Binding Path=BirthHeadCircumference}" 
                           Grid.Column="0" Grid.Row="1" />
                <TextBlock Text="{Binding Path=BirthLength}" 
                           Grid.Column="1" Grid.Row="1" />
            </Grid>
        </Expander>
    </Border>
</DataTemplate>

<!-- Doctor Content Template -->
<HierarchicalDataTemplate x:Key="DoctorTemplate"
    ItemsSource="{Binding Patients}"
    ItemTemplate="{StaticResource PatientTemplate}">
    <Border x:Name="DoctorTemplateBorder" BorderBrush="AliceBlue" 
            BorderThickness="1" CornerRadius="10"
            Background="{StaticResource TreeViewItemBackground}" >
        <Expander x:Name="DoctorTemplateExpander" 
                  HeaderTemplate="{DynamicResource DoctorHeaderTemplate}" 
                  Header="{Binding}" IsTabStop="False"  
                  HorizontalAlignment="Left" IsEnabled="True" 
                  ExpandDirection="Down">
            <Grid Margin="5,5,5,5">
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <TextBlock Grid.Row="0" Text="{Binding Path=FullName}" />
                <TextBlock Grid.Row="1" Text="This is a doctor" />
            </Grid>
        </Expander>
    </Border>
</HierarchicalDataTemplate>

And of course the TreeView markup

<TreeView Name="_myTreeView"
          Margin="0,0,0,0" 
          ItemsSource="{Binding}" 
          ItemTemplate="{StaticResource DoctorTemplate}"
          TreeViewItem.Expanded="_myTreeView_Expanded"
          TreeViewItem.Collapsed="_myTreeView_Collapsed"
          TreeViewItem.Selected="_myTreeView_Selected"
          TreeViewItem.Unselected="_myTreeView_Unselected"
    />

In this example, the HierarchicalDataTemplate gets its source data from the default binding context (which happens to be a simple collection of Doctor objects).  For its children, HierarchicalDataTemplate defines an ItemsSource attribute that specifies which property on the associated Doctor object has the collection of (in this case) Patient objects.  When the TreeView’s DataContext property is set, the binding magic takes over and the tree fills up.  Once we add in the WPF expander controls with a gradient background we’ve got a nice looking tree:

image

Great!  The Doctor and Patient objects get filled in as expected.

Now, for part two – ensuring that that when a note was expanded in the TreeView, that item was also selected (highlighted). At first, I looked at the TreeView item to find an ItemExpanded event, or something similar, but there was none.  As it turns out, I forgot that what was actually getting expanded was a TreeViewItem – not the TreeView itself, and found TreeViewItem.Expanded just as I should have figured.  Once I figured this out, wiring up the selection of the expanded TreeViewItem was pretty simple:

TreeViewItem _item = e.OriginalSource as TreeViewItem;
_item.IsSelected = true;

Lastly, I need to identify how to automatically expand the Doctor item when its corresponding tree node was expanded.  After trying everything I could think of, and searching for a while online, I finally found this article that pointed to a sample generic method that recurses through the visual tree to find an object of the requested type.  It’s not foolproof for every situation (what if I had multiple Expanders in that TreeViewItem?) but it works for this purpose.  Just for fun, I turned this into an Extension Method that will add this method to every DependencyObject just in case I need it somewhere else:

Here’s the generic Extension method I tied to DependencyObject:

public static childItem FindVisualChild<childItem>(this DependencyObject obj) where childItem : DependencyObject
{
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(obj, i);
        if (child != null && child is childItem)
        {
            return (childItem)child;
        }
        else
        {
            childItem childOfChild = FindVisualChild<childItem>(child);
            if (childOfChild != null)
            {
                return childOfChild;
            }
        }
    }
    return null;
}

And here’s how you call it:

Expander _expander = _item.FindVisualChild<Expander>();
_expander.IsExpanded = true;

With this code wired into the TreeView object’s Expanded event handler, the expand/collapse behavior was complete and working as I’d hoped, and I’ve completed all of the tasks my friend asked me to look at. 

Here are some helpful links gathered from my travels around DataBinding fun

And here is a link to my sample project:

Technorati Tags: ,,