If it walks like a duck and talks like a duck, it must be a ... TreeGrid! [A simple, XAML-only TreeGrid UI for WPF]
If you've done much work with WPF or Silverlight, chances are you already know what a TreeView is and what a DataGrid is. You know that a TreeView
is good for showing hierarchical data and a DataGrid
is good for showing tabular data. But you may not know about their hybrid love child, the TreeGrid
- and that's what this post is about.
Sometimes you've got data that's basically tabular in nature, yet also has a hierarchical aspect, and you'd like to leverage that to give people control over the level of detail they're seeing. Most commonly, you'll see a TreeGrid
used when the tabular data can be nicely summarized (or "rolled up") into hierarchical groupings. For example, a list of people's name and address would not make a good TreeGrid
, because there's no natural grouping that makes sense (you can't combine addresses). However, a list of people's company and salary might make a good TreeGrid
because it's natural to group by job and the aggregated salary information could be informative (either as an average or as a sum).
Aside: You might wonder if DataGrid's native support for grouping would be useful here. In my experience, DataGrid
s don't tend to summarize the grouped data like we want - but if you have examples to the contrary, I'd love to see them.
So with all this talk about TreeGrid
, you might expect to find one in the Silverlight or WPF framework, or perhaps as part of the Silverlight Toolkit or WPF Toolkit. But you won't - it's just not used frequently enough to have made it to the big leagues yet. The good news is that a bit of web searching will turn up some third-party TreeGrid
options that definitely seem worth evaluating. But because I'm cheap and a show-off - and occasionally fall victim to a little NIH - I decided to craft a TreeGrid
-like experience using only the WPF TreeView
control, a couple of Grid
s, and some XAML.
That's right - no code, just XAML!
Here's how my SimpleTreeGridUX sample looks with some data I made up about the schedule of a fictional developer:
And here's the complete XAML:
<!-- TreeGrid "Control" --> <Border BorderBrush="Black" BorderThickness="1"> <!-- Resources --> <Border.Resources> <Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}"> <Setter Property="Margin" Value="3 0 3 0"/> </Style> <Style x:Key="TextBlockBoldStyle" TargetType="{x:Type TextBlock}" BasedOn="{StaticResource TextBlockStyle}"> <Setter Property="FontWeight" Value="Bold"/> </Style> </Border.Resources> <!-- Content --> <Grid Grid.IsSharedSizeScope="True"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition/> </Grid.RowDefinitions> <!-- Column headers --> <TreeViewItem Grid.Row="0" BorderThickness="1"> <TreeViewItem.Header> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition SharedSizeGroup="Task"/> <!-- Placeholders for two columns of ToggleButton --> <ColumnDefinition SharedSizeGroup="Toggle"/> <ColumnDefinition SharedSizeGroup="Toggle"/> <ColumnDefinition SharedSizeGroup="Duration"/> <ColumnDefinition SharedSizeGroup="Notes"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="Task" Style="{StaticResource TextBlockBoldStyle}"/> <!-- Empty TreeViewItem to measure the size of its ToggleButton into the "Toggle" group--> <TreeViewItem Grid.Column="1" Padding="0"/> <TextBlock Grid.Column="3" Text="Duration" Style="{StaticResource TextBlockBoldStyle}"/> <TextBlock Grid.Column="4" Text="Notes" Style="{StaticResource TextBlockBoldStyle}"/> </Grid> </TreeViewItem.Header> </TreeViewItem> <!-- Data rows --> <TreeView Grid.Row="1" ItemsSource="{Binding SubItems}" BorderBrush="Gray" BorderThickness="0 1 0 0"> <TreeView.ItemTemplate> <!-- Level 0 template leaves space for 2 child "Toggle" levels --> <HierarchicalDataTemplate ItemsSource="{Binding SubItems}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition SharedSizeGroup="Task"/> <ColumnDefinition SharedSizeGroup="Toggle"/> <ColumnDefinition SharedSizeGroup="Toggle"/> <ColumnDefinition SharedSizeGroup="Duration"/> <ColumnDefinition SharedSizeGroup="Notes"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="{Binding Task}" Style="{StaticResource TextBlockStyle}"/> <TextBlock Grid.Column="3" Text="{Binding Duration}" Style="{StaticResource TextBlockStyle}"/> <TextBlock Grid.Column="4" Text="{Binding Notes}" Style="{StaticResource TextBlockStyle}"/> </Grid> <!-- Level 1 template leaves space for 1 child "Toggle" level --> <HierarchicalDataTemplate.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding SubItems}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition SharedSizeGroup="Task"/> <ColumnDefinition/> <ColumnDefinition SharedSizeGroup="Toggle"/> <ColumnDefinition SharedSizeGroup="Duration"/> <ColumnDefinition SharedSizeGroup="Notes"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="{Binding Task}" Style="{StaticResource TextBlockStyle}"/> <TextBlock Grid.Column="3" Text="{Binding Duration}" Style="{StaticResource TextBlockStyle}"/> <TextBlock Grid.Column="4" Text="{Binding Notes}" Style="{StaticResource TextBlockStyle}"/> </Grid> <!-- Level 2 template has no children --> <HierarchicalDataTemplate.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding SubItems}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition SharedSizeGroup="Task"/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition SharedSizeGroup="Duration"/> <ColumnDefinition SharedSizeGroup="Notes"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="{Binding Task}" Style="{StaticResource TextBlockStyle}"/> <TextBlock Grid.Column="3" Text="{Binding Duration}" Style="{StaticResource TextBlockStyle}"/> <TextBlock Grid.Column="4" Text="{Binding Notes}" Style="{StaticResource TextBlockStyle}"/> </Grid> </HierarchicalDataTemplate> </HierarchicalDataTemplate.ItemTemplate> </HierarchicalDataTemplate> </HierarchicalDataTemplate.ItemTemplate> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView> </Grid> </Border>
Notes:
- There are two "tricks" I use to get
DataGrid
-like behavior from aTreeView
. The first is the Grid.IsSharedSizeScope attachedDependencyProperty
and its partner-in-crime DefinitionBase.SharedSizeGroup. (Both available only on WPF for now.) By settingIsSharedSizeScope
on a parent element andSharedSizeGroup
on some of the column/row definitions ofGrid
s within it, it's possible to "link" the sizes of cells across differentGrid
s. In the scenario above, that sharing takes place across the separateGrid
s of the column headers and eachTreeViewItem
row of data. In this manner, same-width columns are created for the "Task", "Duration", and "Notes" fields so they all line up properly. Except that they wouldn't actually line up if it weren't for... - The "Toggle" shared size group which is used to offset
TreeViewItem
children to take into account the indent thatTreeViewItem
parents automatically impose on them. The following diagram of the defaultTreeViewItem
layout should help explain what I mean:◊ Header Children TreeViewItem
's toggle element. So if all we did was make each column's cells the same width, things wouldn't actually line up because of this offset showing up in different amounts everywhere:◊ Header ◊ Header Children TreeViewItem
in the header section of the XAML is for). Having done that, the "Toggle" size group can be used to simulate the width of the actual toggle anywhere it's needed. Therefore, I'm able to insert the appropriate counter-offset for each level of the tree - and everything lines up beautifully! - You've probably noticed that there's an unfortunate amount of XAML duplication above - each of the three HierarchicalDataTemplates are very nearly identical. In fact, the only difference among them is whether the second and third
ColumnDefinition
have theSharedSizeGroup
property or not. Now, I strive to stay as DRY as the next guy, and I tried to come up with a way to collapse the three templates into one. But while I could do so quite easily using a bit of code, I couldn't come up with a nice way that was pure XAML.Aside: Converting the template contents to a
UserControl
gets close, but there's still the problem of toggling that property based on external input. And while it would definitely be possible to decorate the (view) model with information about each element's level, I considered that to be "cheating" for the purposes of this exercise.:)
[Click here to download the complete source code for the SimpleTreeGridUX sample.]
Just in case it's not really obvious by now, what I describe here is not a true TreeGrid
control! While I'll suggest that it looks like a real TreeGrid
, behaves mostly like one, and is probably good enough for many scenarios, I'm also the first to acknowledge it's not a true TreeGrid
. A true TreeGrid
would probably have an extensive API for managing columns and rows, allow sorting and arbitrary nesting, and end up with an object model pretty similar to DataGrid
's. So if you came here looking for a proper TreeGrid
control, I'm sorry to disappoint you - but if you came here hoping to learn more about solving real-world problems with WPF, I hope this has been educational!