This is the next sample in the WPF Sample Applications Series. The purpose of the Sample Series is to provide concise code solutions for specific programming tasks. This sample provides a brief description of the problem, the solution and full source code.
Introduction
These past few months I’ve been privileged to be a part of the WPF for Line of Business Training Tour. What I really love is to be around developers excited about the WPF platform. At each city I got requests from attendees to show them how to accomplish a task in WPF. During a break I would sit down and write the code and usually I add the code to the session downloads.
This sample is the result of the question, “how can I do multi-level grouping?”
Application
All regions have been collapsed. Count of Account Mangers is displayed along with the sales in dollars.
The West region has been expanded along with the child states. Notice the state grouping level has totals for its state. Account Manages have their name and sales figure displayed.
Application Requirements
- Grouping levels must be collapsible
- Display total sales for Account Managers in the level
- Display count of Account Managers in the level
- Display the familiar “+” and “-” icon for expanding and collapsing levels
- Sort data by region, state and sales descending
Grouping and Sorting
<CollectionViewSource Source="{x:Static local:Data.AccountManagers}" x:Key="cvs">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Region" />
<scm:SortDescription PropertyName="State" />
<scm:SortDescription PropertyName="Sales" Direction="Descending" />
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Region" />
<PropertyGroupDescription PropertyName="State" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
WPF provides the CollectionViewSource for codeless sorting and grouping of data. The SortDescriptions and GroupDescriptions collections can be modified at runtime if desired. The SortDescription provides the ability to set the sort direction as I’ve done for the Sales.
The ListBox consumes the CollectionViewSource data by assigning the ListBox.ItemsSource property to the CollectionViewSource resource.
Note: I’ve assigned the Source to a static property so that I can have design time data and rending of the ListBox during development.
ListBox.GroupStyle
The ListBox.GroupStyle does all the heaving lifting for rendering the group level headers. You have several options when working with GroupStyles.
- Define a GroupStyle.HeaderTemplate
- Define a GroupStyle.ContainerStyle
- Use a HeaderTemplateSelector to select the HeaderTemplate at runtime
- Use a ContainerStyleSelector to select the ContainerStyle at runtime
For this application, I’ve chosen to define a ContainerStyle in XAML.
When defining a ContainerStyle, we will re-template the GroupItem that gets created for each level of grouping.
The purpose of re-templating the GroupItem is so that we have full control over how level is rendered.
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsBottomLevel}" Value="True">
<Setter TargetName="gridTemplate" Property="Grid.Background"
Value="#FF965F00" />
</DataTrigger>
</ControlTemplate.Triggers>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Background="Black" x:Name="gridTemplate" Height="26"
VerticalAlignment="Center">
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="14" />
<Setter Property="Foreground" Value="White" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="45" />
<ColumnDefinition Width="30" />
<ColumnDefinition Width="110" />
</Grid.ColumnDefinitions>
<ToggleButton x:Name="btnShowHide" IsChecked="True" Margin="3.5" />
<TextBlock Grid.Column="1" Text="{Binding Path=Name}" Margin="3.5,0" />
<TextBlock Grid.Column="2" Text="Count:" />
<TextBlock Grid.Column="3" Text="{Binding Path=ItemCount}"
TextAlignment="Right" Margin="0,0,11,0" />
<TextBlock Grid.Column="4"
Text="{Binding StringFormat=\{0:C\},
Converter={StaticResource groupItemSalesSubtotalConverter}}"
TextAlignment="Right" />
</Grid>
<ItemsPresenter
Visibility="{Binding ElementName=btnShowHide,
Path=IsChecked,
Converter={StaticResource booleanToVisibilityConverter}}"
Grid.Row="1" Margin="11,0,0,3.5" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
The DataContext for a GroupItem is a CollectionViewGroup.
In this ControlTemplate we will bind to the following properties of the CollectionViewGroup:
- IsBottom – used by DataTrigger to set level heading Background
- Name – used to display the level’s heading text
- ItemCount – used to display the number of members in this level
Collapsible Group Levels
The ToggleButton has been re-templated to display an image that indicates the state of the Button. (see download for template)
The ItemsPresenter at the bottom of the template is where the data items will be rendered.
Using the built-in BooleanToVisibilityConvert and binding the ItemsPresenter.Visibility property to the ToggleButton.IsChecked property, we get simple, codeless collapse and expand behavior of the data members.
You really have to admire the sheer power of WPF here.
Subtotals
The last TextBlock Text property has a strange Binding. There is no path. When the converter is called, the DataContext will be passed as the value to the converter. In this code the CollectionViewGroup will be passed to the converter.
Imports System.Windows.Data
<ValueConversion(GetType(CollectionViewGroup), GetType(Double))> _
Public Class GroupItemSalesSubtotalConverter
Implements IValueConverter
Public Function Convert( _
ByVal value As Object, ByVal targetType As System.Type, _
ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) _
As Object Implements System.Windows.Data.IValueConverter.Convert
If value IsNot Nothing AndAlso TypeOf value Is CollectionViewGroup Then
Return GetSubTotal(DirectCast(value, CollectionViewGroup))
Else
Return Nothing
End If
End Function
Private Function GetSubTotal(ByVal obj As CollectionViewGroup) As Double
Dim dbl As Double
For Each objItem As Object In obj.Items
If TypeOf objItem Is AccountManager Then
dbl += DirectCast(objItem, AccountManager).Sales
Else
dbl += GetSubTotal(objItem)
End If
Next
Return dbl
End Function
Public Function ConvertBack( _
ByVal value As Object, ByVal targetType As System.Type, _
ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) _
As Object Implements System.Windows.Data.IValueConverter.ConvertBack
Throw New NotImplementedException
End Function
End Class
This converter uses recursion to drill down to each member of the level and calculate a sales subtotal.
ListBox.ItemTemplate
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel>
<TextBlock DockPanel.Dock="Right" HorizontalAlignment="Right"
Text="{Binding Path=Sales, StringFormat=C}" />
<TextBlock Text="{Binding Path=FullName}" />
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
The ListBox.ItemTemplate is used when rendering the AccountManager data items. A DockPanel provides the required layout. The TextBlocks are data bound to properties on the AccountManager class.
Download
After downloading you must change the file extension from .doc to .zip. This is a requirement of WordPress.com
Source Code (32KB)
Hope you can learn a little more about WPF from this article and the Sample Series.
Have a great day,
Just a grain of sand on the worlds beaches.