WPF Sample Series – ListBox Grouping, Sorting, Subtotals and Collapsible Regions

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.

ListBoxOne

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.

ListBoxTwoJPG 

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.

43 Responses to WPF Sample Series – ListBox Grouping, Sorting, Subtotals and Collapsible Regions

  1. ursri says:

    How to filter list items bound to collection of items? Item has image, text and number as members.

  2. wazikhan says:

    Karl first of all thnx for such a good article.
    can u plz give me any idea about how should i animate each item of the listbox one by one when the listbox is loaded just like tweeter blu load tweets. I create style for listboxItem in which i give animation to the listboxItem on load but it don’t work for me.

    • I’m not sure how to do this.

      Can you post your project somewhere and leave the link here? Maybe I can figure it out for you.

      Cheers,

      Karl

      • wazikhan says:

        Karl
        sorry i am replying late but i got the solution.

        I need to create custom panel which animate its Child Items on load and then use that panel in ListboxItemPanelTemplate.

    • Not sure how to do this or exactly what you are looking for.

      I did a Google search on, wpf animate listboxitem and got a lot of hits.

      I suggest checking these.

      Cheers,

      Karl

  3. grakenmol says:

    Hi Karl,

    Thanks for the good article..
    I’m having a problem where my grouped items aren’t showing.. I’m just getting the headings..
    When I remove the group by from the CollectionViewSource I get all the items showing correctly.

    I’ve pretty much mimicked your XAML..
    My CVS is bound to a property in my ViewModel.. The ViewModel updates this collection dynamically on a background thread, and notifies the UI dispatcher that it’s been modified, and it updates the UI fine..

    I changed the GRID in the ControlTemplate for GroupItem to an expander, and am using the Header and Content areas of the expander to show the data.. When the grid renders, it shows the right COUNT in the header, but no content in the Expander content area..

    Do you have any ideas, off the top of your head, what might be causing this issue ??

    Thanks
    Grant.

    • Grant,

      Without seeing the code I would have no way to grasp the issue clearly.

      Grouping is a strange creature and changing the data at runtime could be the problem. You may need to force the control to reload/redraw itself. Does you ViewModel pro;perty have change notification enabled?

      Not sure about the expander issue. When the expander expands, does it expand down? You can add a background color verify it is expanding. If you take this expander code out of the listbox does it work?

      Cheers,

      Karl

      • grakenmol says:

        Hi Karl,
        Thanks for the quick reply.
        I found the issue. I’m using some skins, and the control template for the ListBox uses a StackPanel as an items host, with the property IsItemsHost = True.. I’ve removed the stack panel and have replaced with a ItemsControl which works well now.

        Thanks for your help and your articles..

        Cheers,
        Grant.

  4. oskarhermansson says:

    Hi Karl,

    I would like to display the name of the property used to group in the header. In your example I would like the header to be something like “Region: North East … Count: 9″ and “State: HI … Count: 3″.

    How can I access the property names from the GroupItem template? I’ve investigated and come to the conclusion that the abstract CollectionViewGroup class does not contain this information, but the internal CollectionViewGroupInternal does – It has a property called GroupBy which would give the GroupDescription. Unfortunately the property is internal as well so my binding fails…

    System.Windows.Data Error: 39 : BindingExpression path error: ‘GroupBy’ property not found on ‘object’ ”CollectionViewGroupInternal’ (HashCode=37104753)’. BindingExpression:Path=GroupBy.PropertyName; DataItem=’CollectionViewGroupInternal’ (HashCode=37104753); target element is ‘TextBlock’ (Name=”); target property is ‘Text’ (type ‘String’)

    Any clues on how to workaround this?

    Thanks,
    Oskar

    • Oskar,

      The way I figured out what was available in the GroupItem was to look at the GroupItem in the Converter by setting a break point. At the break point you’ll be able to see all available data and how the GroupItem works.

      Give this a try, you’ll be amazed at the data.

      Cheers,

      Karl

  5. ubsch says:

    Karl, thanks for the good article. It works fine.
    But what can I do if the sales change – for example by user-input. Your GroupItemSalesSubtotalConverter
    doesn’t notify changes because the binding has no Path. Is there a way to notify changes in sales?

    Cheers
    ubsch

    • ubsch,

      Just add change notification (INotifyPropertyChanged) to the source class, Sales property and the WPF binding system will automatically pick it up.

      Cheers,

      Karl

  6. bobriddle says:

    I enjoyed reading your entry on collapsible regions with a WPF Listbox. But I cannot get your sample code file as a valid file. When I download it and rename the .DOC to .ZIP and attempt to open the archive, all I get is “listboxcollapsiblegroupszip.zip is not a valid archive”. Could you please consider checking the source code link to see what might have happened? Thanks!

    • vegeta4ss says:

      Same issue as bobriddle posted about.

      7zip, windows zip tool, and winrar all complain to me about it not being a valid archive.

      Can you check your link? This demo is exactly what I need to complete a project I am working on.

    • Bob,

      Sorry you had a problem.

      I just downloaded the file, renamed to listboxcollapsiblegroups.zip and used all three of my unzip tools without a problem.

      Windows Explorer
      WinZip 10
      WinRAR

      Please give it another try, I’ve never had a problem with this before for any file.

      I’ve also uploaded the same zip file to my Sky Drive.

      http://cid-51de981e071f222b.skydrive.live.com/self.aspx/Public/ListBox%20with%20Collapsible%20Regions/listboxcollapsiblegroups.zip

      Let me know,

      Cheers,

      Karl

    • bobriddle says:

      Thank you very much for reposting the sample to Sky Drive. I was able to download and unZIP it without issues from there. I have no idea what was going on with the WordPress download. I tried with both IE8 and FireFox 36.3; then tried unZIPing it with two different versions of WinZip and with the native archive support in Win7-64bit. No joy with either.

      After seeing what you accomplished here, I’m going to go hunt up a good read on XAML Attached Properties. It’s interesting how radically you altered the ListBox behavior.

      After using Win32 since the 80’s, I’m finding writing real-world WPF a bit more of a learning curve than I had expected. The early VS drag-and-drop WPF XAML design and basic XAML tool palette support is welcome, but still has a ways to go.

  7. vegeta4ss says:

    Thanks Karl! I think noscript was blocking quantserve or something yesterday because I tried again today and it worked. DL’d from skydrive just as a backup incase it corrupted. Thanks for your help. Hopefully I can take the content of the demo and make it work like the drilldown behavior on one of my sql reports.

  8. vegeta4ss says:

    Karl,

    I’m working with the sample code in one of my own projects and I have run into an error

    A panel with IsItemsHost=”true” is not nested in an ItemsControl

    I have tried setting this property on the grid objects in the group’s container style but I get different exceptions and can’t figure out what’s going on. Any idea what might be giving me this error?

    • IsItemsHost is only applied to panels that are the ItemsControl.ItemsPanel. Outside of this content you’ll get the error. So, just remove the IsItemsHost and the error will go away.

  9. macdonaldkeith34 says:

    Karl, I tried to download the code, but the page is always stuck on loading…

    • macdonaldkeith34 says:

      Nevermind. I saw your updated link in the responses…. Thanx

    • I just tried it and its working. However, I did have to click it twice. The first time I got a 406 error. The second time it worked.

      Sorry you had a problem.

      Karl

  10. HI,
    i ve implemented the above mentioned code and it went great. now im facing a problem i.e. let i the sale field is editable by user, then how to change the corresponding subtotal calculated?

    thanks in advance

    • You have to make sure that your classes implement INotifyPropertyChanged and that the source collection is an ObservableCollection(Of T) or C# ObservableCollection.

      This way when the data gets changed, change notification will flow to the UI.

      Karl

      • eficazcs says:

        Hi.

        Thanks so much for this article. I’ve implemented on my solution and tried to use ObservableCollection(Of T) as DataContext and it’s Type implementing INotifyPropertyChanged. But the subtotals don’t update.

        I have a ListView to show data. The ListView’ ItemsSource is a CollectionViewSource (using Grouping and Sortig) like your example.

        The datatemplate has 3 buttons: Pay, Edit and Delete. When user clicks on Pay ou Edit, a hidden UserControl do edit de selectedItem turns to Visible and the user Pay or modify the Paid item. When he clicks on Save the UserControl turns do Hide again, but the Group subtotal doesn’t update.

        Could you help me please? I don’t see what I’m doind wrong.

        Sorry for my bad english. Waiting for your reply.

        • Karl says:

          Place a break point on the property in the class you are editing and follow the call to see what is wrong. Make sure you are editing the “same instance” of the data class.

          You can also try and tell the CollectionViewSource to refresh itself and see if that causes the correct data values to be displayed.

          Best,

          Karl

  11. mikem63 says:

    Hi,
    I found the sample will be very useful to me. But I can’t down load the download from the link. Seems the file is not there. Please check and update the link.

    Thanks lot.

  12. aslan427 says:

    This sample has helped get started with grouping listboxes in WPF. One thing that I’d need to add is a checkbox next to each region/state. This checkbox when clicked selects/deselects all items that are under it. I added a checkbox control right after the region total and now I’m stuck on how to make it invoke an event to perform the selection/deslection. Any ideas on how this can be done or if it’s at all possible?

    Thanks.

    • Karl says:

      I can think of two ways.

      One is to add a relative source data binding on the listboxitem and look for a checkbox, then bind the isselected property to ischecked.

      Second is to change the code over to use MVVM and handle the selection in code. Doing this would require the view model to expose a collection of data, adding an isselected property to the data model, then when the checkbox is checked or unchecked, change the isselected property on the data model for each item with matching states.

      Cheers,

      Karl

  13. mycollections2 says:

    Hello Karl,

    Thanks for sharing this sample. Just a quick question, is it possible to have the first group open by default and the others closed ?

    Thanks for your time.

    Regards

    • Karl says:

      Yes, you can do this. Currently, there is no data model property you can bind to, but your could add one.

      Search for a button named, btnShowHide. This button has the IsChecked property set to True in the XAML. If you set this to False, all the sections will start off collapsed.

      You could do this programatically, after the form is loaded, just walk the visual tree of the ListBox and set the first button’s IsChecked property to True, this will cause it to open, assuming you have all the others set to false.

      You could also add a property to data bind the IsChecked property to.

      Best,

      Karl

  14. mycollections2 says:

    Hello Karl,

    Thanks a lot for your answer.
    My issue is that i add groupeStyle at runtime, so the first time there is no grouping, but the listbox was loaded.
    So when i click on my groupbutton, the visual tree of the listbox does not contain my togglebutton.
    I get it in my visual tree only when the style is displayed and that I interact with the listbox.

    Any clue?

    One more time thanks a lot your help.

    Jeff

    • Karl says:

      Jeff,

      Not sure I understand. Are you loading your listbox data on-demand?

      At any rate, when and how the listbox loads is orthogonal. Like I was saying before, you can data bind the btnShowHide.IsChecked property (my preference) or programmatically set it. This will control which groups display contracted or expanded.

      Best,

      Karl

      • mycollections2 says:

        Yes i load it on demand. And i apply style on demand to (in case of the latest build of my soft is available here : http://www.mediafire.com/?su8udz606p8oe61).
        I try to set the first group item to true (all others are false) using code, but the visualtreehelper never find the button.
        Using btnShowHide.IsChecked property databind will be perfect, but dunno how to let know that it’s the first one. Using a converter?
        Hope I was clear enough this time  sorry for my poor English.

        Jeff

        • Karl says:

          Not sure why you can’t locate the button. Remember you have to use the visualtreehelper within a recursive function that you need to write. You can’t call it on a top level element and expect it to travel the visual tree for you.

          You only need to data bind a Boollean property on your data model to the btnShowHide.IsChecked property. You don’t need a converter.

          Karl

  15. hi Guys I am working on a wpf app as part of a school assignment I am having hells trying to Group Data coming from a linq Query , do you guys have any tips ?

Follow

Get every new post delivered to your Inbox.

Join 248 other followers