共用方式為


Hierarchical Databinding in WPF

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: wpf,databinding,code

Comments

  • Anonymous
    May 24, 2008
    Nice post, Chris.  It would be much easier if you bound the TreeViewItems, and the elements in their ItemTemplate, to a ViewModel.  That would remove the need to walk the visual tree, etc. http://www.codeproject.com/KB/WPF/TreeViewWithViewModel.aspx Thanks, Josh

  • Anonymous
    May 27, 2008
    ASP.NET Ending a Response without Response.End() Exceptions? [Via: Rick Strahl ] UFrame: div meets...

  • Anonymous
    September 03, 2008
    Hi, I have a DB table [1] which is self referenced. DataID field is the FK of ParentDataID field. For multilevel hierarchical data binding to a WPF tree view control, I have a stored procedure that returns result set in the below mentioned order[1]. The below result set contains all the top level data with NULL ParentDataID data & all the child records has the ParentDataID field data as the pointer to its parent data. Based on the below dataset structure, we have a requirement to bind the resultset with the multi level hierarical data to the TreeView control. How can we achive this funcionality of binding the dynamic hierarical multi level dataset to TreeView? After binding the above result set, the Tree View data needs to be displayed in the below format. How do we implement this in the WPF Tree View?

  • Test 1         * Test 2                  * Test 5        * Test 3                  * Test 6        * Test 4 [1] DataID ParentDataID  Description 1          NULL               Test 1 2          1                     Test 2 3          1                     Test 3 4          1                     Test 4 5          2                     Test 5 6          3                     Test 6  Regards, Vipul Mehta

  • Anonymous
    September 15, 2008
    Hi Vipul i need the same stuff as you. If you find it, please send me a link. I will do the same to you. bye, Jon

  • Anonymous
    December 04, 2008
    Nice example! Since you seem to know your way around TreeViews very well, I have a hopefully simple question for you. I have two DataTables, Group and Member, how could I create a tree like this: Group 1    Group 1a         Member A         Member B    Group 1b         Member C    Member D That is, a Group can have other (sub)Groups as well as Members as children.

  • Anonymous
    May 20, 2009
    The comment has been removed