Introduction
I was going to title this blog "What's in a name?" because William Shakespeare's famous question smacked me on the head recently after what seemed like several hours of frustration. The answer in this particular case is "Quite a lot!". As you'll see when I cover the syntax used to group items, you can very easily fall into a trap when it comes to names.
But first, I'll need to set the scene. This small application:
- will create a collection of Person objects and databind them to a ListBox.
- will use very simple DataTemplates to format two properties of the Person class - FullName and Status.
- will use a GroupStyle HeaderTemplate to display a third property of the Person class - Category.
- groups the Person objects by their Category property.
- sorts the Categories and displays them in alphabetical order.
- sorts the Persons by name inside the Category groups and displays them in alphabetical order.
The Person Class
First, the Person Class - which I have chosen to implement INotifyPropertyChanged, although I don't actually take advantage of the change notification in this simple example:
Imports System.ComponentModel
Imports System.Collections.ObjectModel
Public Class Person
Implements INotifyPropertyChanged
Sub New(ByVal personname As String, ByVal personstatus As String, ByVal personsgroup As String)
Me.FullName = personname
Me.Status = personstatus
Me.Category = personsgroup
End Sub
Private _name As String
Public Property FullName() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
OnPropertyChanged(New PropertyChangedEventArgs("FullName"))
End Set
End Property
Private _status As String
Public Property Status() As String
Get
Return _status
End Get
Set(ByVal value As String)
_status = value
OnPropertyChanged(New PropertyChangedEventArgs("Status"))
End Set
End Property
Private _Category As String
Public Property Category() As String
Get
Return _Category
End Get
Set(ByVal value As String)
_Category = value
OnPropertyChanged(New PropertyChangedEventArgs("Category"))
End Set
End Property
Public Sub OnPropertyChanged(ByVal e As PropertyChangedEventArgs)
If Not PropertyChangedEvent Is Nothing Then
RaiseEvent PropertyChanged(Me, e)
End If
End Sub
Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
Public Shared Function GetPersons() As List(Of Person)
Dim GP As New List(Of Person)
GP.Add(New Person("Neil Birch", "Available", "My Friends"))
GP.Add(New Person("Joe Brown", "On Site", "Work Colleagues"))
GP.Add(New Person("Larry Blake", "Available", "VB City"))
GP.Add(New Person("Fran Mead", "At Work", "Family"))
GP.Add(New Person("Elaine Javan", "On Vacation", "Work Colleagues"))
GP.Add(New Person("Matt Higginbotham", "On Line", "VB City"))
GP.Add(New Person("Zoe Flint", "On Site", "Work Colleagues"))
Return GP
End Function
End Class
Essentially, all you need to note for the purpose of this article is that the GetPersons function creates a List (of Person) and each Person instance has values assigned to all three of the properties of the class - FullName, Status, and Category.
The WPF Window
The WPF Application contains just one Window. A List of Persons is created by using the GetPersons function and this List is used as the DataContext for the Window. This will allow the ListBox to access that List. The initial code-behind is as follows:
Class Window1
Dim Contacts As New List(Of Person)
Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
Contacts = Person.GetPersons
Me.DataContext = Contacts
End Sub
End Class
The ListBox
In the XAML for Window1, there is a ListBox. To begin with, this ListBox simply shows the Person's FullName, followed by their Status. There is a minimal amount of formatting in the two TextBlocks that are used for this.
<ListBox Name="lstContacts"
ItemsSource="{Binding}"
Margin="6,6,3,3" >
<ListBox.ItemTemplate>
<DataTemplate >
<StackPanel >
<TextBlock Text="{Binding Path=FullName}"
Margin="0,2,0,0"/>
<TextBlock Text="{Binding Path=Status}"
Margin="6,0,0,0" FontSize="11" Foreground="Navy" />
< span>StackPanel>
< span>DataTemplate>
< span>ListBox.ItemTemplate>
< span>ListBox>
The result so far is this:

Obviously, there is no sorting or grouping going on there yet.
CollectionView
When you set up the Binding between the data source (the List of Persons) and the target control (the ListBox), a CollectionView is created automatically. This is a wrapper for the binding and allows you to sort, filter, group or navigate through the collection without affecting the underlying collection itself. Think of it as an editable snapshot of the data and you won't be far off the mark
You can access the current view by using the GetDefaultView method and passing in the name of the data source - in this case, the Contacts List created in the code-behind of Window1. In our example, we will access the CollectionView and then group and sort the items as they are displayed in the ListBox. To try and keep things as straightforward as possible I'll tackle each of these one at a time.
Creation of an instance of the view by means of the GetDefaultView method is simple.
Imports System.ComponentModel
Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
Contacts = Person.GetPersons
Me.DataContext = Contacts
Dim currentView As ICollectionView
currentView = CollectionViewSource.GetDefaultView(Contacts)
End Sub
End Class
Note the inclusion of the Imports statement for System.ComponentModel, the class which houses ICollectionView.
It's the final two lines of the Window_Loaded event that create the View:

Grouping
The following line of code, placed in the Window_Loaded event, will group the individual items based on the Category property:
currentView.GroupDescriptions.Add(New PropertyGroupDescription("Category"))
The result is shown below:

So I don't have to be much of a mind reader to know that you're not too impressed at this point. In fact, you probably don't even believe that the items are now grouped. And even if they are, it's not very obvious to the user.
Let's start with the question of whether they are really grouped. If you look carefully at the order of the names you will see that they have changed from the way they were originally listed - as seen in the earlier screenshot. And if you take a peek at the code which created the Person instances you will be able to see the value of the Category property for each instance.
Public Shared Function GetPersons() As List(Of Person)
Dim GP As New List(Of Person)
GP.Add(New Person("Neil Birch", "Available", "My Friends"))
GP.Add(New Person("Joe Brown", "On Site", "Work Colleagues"))
GP.Add(New Person("Larry Blake", "Available", "VB City"))
GP.Add(New Person("Fran Mead", "At Work", "Family"))
GP.Add(New Person("Elaine Javan", "On Vacation", "Work Colleagues"))
GP.Add(New Person("Matt Higginbotham", "On Line", "VB City"))
GP.Add(New Person("Zoe Flint", "On Site", "Work Colleagues"))
Return GP
End Function
Now, you can see that the first Category is "My Friends" and Neil Birch is the sole member of that Category. More usefully, the next three instances - Joe Brown, Elaine Javan and Zoe Flint all have "Work Colleagues" as their Category. They are now all listed consecutively - and both Elaine Javan and Zoe Flint have been moved from their original positions.
The next two names - Larry Blake and Matt Higginbotham are both in the "VB City" Category. Fran Mead is the sole member of the "Family" Category.
So, the grouping has actually taken place. The order of the Categories is based on the order in which they first appear in the GetPersons function.
Clearly, we need something to make the grouping of Categories more obvious. And that 'something' is a GroupStyle. GroupStyle has a HeaderTemplate property which can be used to format the text and/or graphics that are displayed at the start of each group - in this case at the start of each Category.
The XAML is a little bit verbose, but - apart from one potential Gotcha - is straightforward.
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>
< span>DataTemplate>
< span>GroupStyle.HeaderTemplate>
< span>GroupStyle>
< span>ListBox.GroupStyle>
The GroupStyle has a HeaderTemplate. The HeaderTemplate contains a DataTemplate. In this case I have chosen to include only a basic TextBlock in the DataTemplate. The Text property of the TextBlock needs to show the Category name.
This is where I managed to get myself quite confused.
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>
I initially had a property in the Person class called Name. (I've since changed it to FullName for clarity.) I couldn't understand why the Path of a TextBlock that showed the Category value would be pointing to the Name property of Person class. And of course it doesn't. But, while I was getting to grips with this I set the path to what I thought was the most logical item - the Category property. In other words, I had it like this:
<TextBlock Text="{Binding Path=Category}" FontWeight="Bold"/>
Now, WPF is so forgiving when it comes to this kind of data binding that it doesn't throw an exception (quite rightly, because it is a valid path to an existing field). Sadly it doesn't show the Categories either. The result at this point is:

You can see that they are grouped, but there's no header. The key lesson to take away from this is that the Path in that particular binding points to the name that is assigned to the PropertyGroupDescription in the code-behind:
currentView.GroupDescriptions.Add(New PropertyGroupDescription("Category"))
Confused yet? Put simply, you always use the syntax of:
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>
regardless of what the actual name of the grouping property is. You're probably thinking that we're well into the "Too much information" stage now, but - apart from wanting to share my pain - I really think that this is Gotcha that is just waiting to bite the unwary and so it was worth spending a couple of extra minutes looking at it.
OK, so getting back on track, the correct version of the GroupStyle markup will bring you the result you want. I have included all the ListBox XAML so you can see the finished product:
<ListBox Name="lstContacts"
ItemsSource="{Binding}"
Margin="6,6,3,3" >
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>
< span>DataTemplate>
< span>GroupStyle.HeaderTemplate>
< span>GroupStyle>
< span>ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate >
<StackPanel >
<TextBlock Text="{Binding Path=FullName}"
Margin="0,2,0,0"/>
<TextBlock Text="{Binding Path=Status}"
Margin="6,0,0,0" FontSize="11" Foreground="Navy" />
< span>StackPanel>
< span>DataTemplate>
< span>ListBox.ItemTemplate>
< span>ListBox>
Here it is:

The grouping is clear, has a useful header and - more to the point - is accurate. The Categories are not yet in alphabetical order. The individual Person instances are also not sorted within their Categories, as you can see from the order in the Work Colleagues group.
As this blog has become a bit longer than I expected, I will continue with a Part 2, which will cover the Sorting methods.
I have posted a copy of this project which you can download from here.