XTab's Blog

Ged Mead's Blog at vbCity

vbCity Blogs moved to:
http://cs.vbcity.com/blogs
  Home :: Syndication  :: Login

AprMay 2013Jun
SMTWTFS
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

Archives

Topics

Ramblings

VB.NET

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" />

 

          StackPanel>

        DataTemplate>

      ListBox.ItemTemplate>

    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"/>

            DataTemplate>

          GroupStyle.HeaderTemplate>

        GroupStyle>

      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"/>

            DataTemplate>

          GroupStyle.HeaderTemplate>

        GroupStyle>

      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" />

 

          StackPanel>

        DataTemplate>

      ListBox.ItemTemplate>

    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.

posted on Sunday, October 25, 2009 1:25 PM