Delay's Blog is the blog of David Anson, a Microsoft developer who works with the Silverlight, WPF, Windows Phone, and web platforms.
http://dlaa.me/
@DavidAns
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.
TreeView
DataGrid
TreeGrid
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, DataGrids 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 Grids, and some XAML.
Grid
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:
DependencyProperty
IsSharedSizeScope
SharedSizeGroup
TreeViewItem
ColumnDefinition
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. :)
UserControl
[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! :)