Feb 13, 2006

How to implement custom grouping

16GroupByType

My previous post shows how to group items based on the value of a certain property. In a real-world scenario you may want to group your items based on some other logic. With this in mind, Avalon Data Binding provides a way for you to write custom code and specify how you want to group your items. This allows maximum flexibility; you can group your items pretty much any way you can think of.

Brett made a comment to my last blog post asking how to group items based on their type. I will use this scenario to show you how to do custom Grouping.

My data source in this sample is of type ObservableCollection<object>, and contains some objects of type GreekGod and others of type GreekHero. My goal is to group all the items of type GreekGod in a group called “Greek Gods” and group all GreekHero items under the group “Greek Heroes”. This is what the markup looks like:

    <Window.Resources>
        <local:GreekGodsAndHeroes x:Key="GodsAndHeroes" />
        <local:GroupByTypeConverter x:Key="GroupByTypeConverter"/>

        <CollectionViewSource x:Key="cvs" Source="{Binding Source={StaticResource GodsAndHeroes}}">
            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription Converter="{StaticResource GroupByTypeConverter}"/>
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>
    </Window.Resources>

Notice that this time, instead of setting PropertyName in PropertyGroupDescription, I set the Converter property. This Converter is defined in the code behind and contains the logic to divide the data items in groups.

    public class GroupByTypeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is GreekGod)
            {
                return "Greek Gods";
            }
            else if (value is GreekHero)
            {
                return "Greek Heroes";
            }
            return null;
        }
    }

All the items that return the same value in the Converter will be grouped together. In this scenario I am grouping the items based on their type and my groups are of type string. Remember that you can use a Converter to group your items some other way. Notice also that the groups don’t have to be a string, they can be any object you want.

Just like in the previous post, I want to display the groups and items in a TreeView.

    <TreeView ItemsSource="{Binding Source={StaticResource cvs}, Path=Groups}" Width="200">
    </TreeView>

In this case, however, templating the items is not as obvious. When the items are all of the same type this is really easy to achieve with a chain of HierarchicalDataTemplates and a DataTemplate for the leaf nodes. In this scenario we need a HierarchicalDataTemplate for the groups and one of two DataTemplates for the leaf nodes, depending on their type.

My first approach to this was to have those 3 templates in the resources and set their DataType property instead of giving them a key (with x:Key). This does not work because when you use a HierarchicalDataTemplate to template a group and do not set its ItemTemplate property, that same template is used for the lower levels of the hierarchy. This behavior is useful when all the levels have items of the same type (for example, when using a TreeView to display a hierarchy of directories in a computer).

My second approach was to set the ItemTemplateSelector property of the HierarchicalDataTemplate to a template selector that decides the correct template to use based on the type of the leaf item. Unfortunately there is a bug in the ItemTemplateSelector property of HierarchicalDataTemplate that prevents this from working. Once the bug is fixed, this will be the correct way to specify the templates.

My third and final approach was to move the template selector to the TreeView and add one more “if” branch to deal with deciding what type to return for the groups (which are of type CollectionViewGroup).

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        string templateKey;
        if (item is CollectionViewGroup)
        {
            templateKey = "GroupTemplate";
        }
        else if (item is GreekGod)
        {
            templateKey = "GreekGodTemplate";
        }
        else if (item is GreekHero)
        {
            templateKey = "GreekHeroTemplate";
        }
        else
        {
            return null;
        }
        return (DataTemplate)((FrameworkElement)container).FindResource(templateKey);
    }

    <Window.Resources>
        <local:GodHeroTemplateSelector x:Key="GodHeroTemplateSelector" />
        (…)
    </Window.Resources>

    <TreeView ItemsSource="{Binding Source={StaticResource cvs}, Path=Groups}" ItemTemplateSelector="{StaticResource GodHeroTemplateSelector}" Width="200">
    </TreeView>

For each of the items displayed in the TreeView, this template selector looks up the appropriate (Hierarchical)DataTemplate in the resources.

Here is a screenshot of the completed sample:

Here you can find the VS project with this sample code. This works with January CTP WPF bits.

19 Comments
  1. jason d

    Hi Beatriz, I’ve got a question that actually is in response to your posts from last year, about getting a handle on a ListBoxItem from a ListBox. I didn’t think you’d be checking comments from such an old post which is why I put this here…

    What if you’d like to specify things about the ListBoxItems before they are created? As an example, let’s say you want to be notified when the user mouses over a row in your ListBox. ListBoxItem (or ListViewItem) extends ContentControl, so it has MouseEnter and MouseLeave events. But as a WPF developer binding an ObservableCollection to a ListBox, we are not creating the ListBoxItems ourselves. They are created by some FrameworkElementFactory by the WPF framework I’d imagine, when the ListBox is having it’s visual tree built. So how can we get access to that FrameworkElementFactory, and register some methods to be invoked upon the MouseEnter/MouseLeave events being fired from the ListBoxItem?

    Thanks! Great stuff here. This blog is doing more to explain the internals of WPF databinding than anything I’ve seen yet!

    • Bea

      Hi Jason,

      You can set properties and register events on a generated ListBoxItem by defining a style for it. This can be done by setting the ItemContainerStyle property of ItemsControl to your style.

      There is a Setter element that can be used inside a Style to set properties on the element being styled. There is also an EventSetter element that can register event handlers for the events you specify.

      In the following markup, I am setting the foreground of all ListBoxItems to blue. I also have event handlers set up for MouseEnter/MouseLeave.

      <Style x:Key=”ContainerStyle” TargetType=”{x:Type ListBoxItem}”>
      <Style.Setters>
      <EventSetter Event=”Mouse.MouseEnter” Handler=”MouseEnterHandler”/>
      <EventSetter Event=”Mouse.MouseLeave” Handler=”MouseLeaveHandler”/>
      <Setter Property=”Foreground” Value=”DarkBlue” />
      </Style.Setters>
      </Style>

      In the complete sample (see link below), I have event handlers such that when I mouse over the item the background becomes light blue, and when I mouse out the background goes back to white.

      You can find a complete sample with this here.

      Let me know if this answers your question.

      Bea

  2. Brett

    Hi Bea,

    Thanks for posting this for us. Your blog is invaluable to me for validating the capabilities of the WPF so early in its lifecycle. By showing us the best solution and the workaround, you have saved me hours of futility.

    Thank you very much!

    Brett

  3. Bea

    I would just like to add that I do check comments from old posts. I receive email notifications for those. So feel free to add the comments where you think they make more sense.

    Thanks.

  4. Alex

    Hi Beatriz, your examples are really great!

    I havw one question regarding 1MouseEnterListBoxItem example. It works as it is in C#, however, when converted to VB.NET it cannot be compiled until last parameters of the event handlers are converted from RoutedEventArgs to MouseEventArgs.
    Why is the same XAML code requires different event handler signatures in different languages?

    Regards,
    Alex

    • Bea

      Hi Alex,

      I am no expert in VB, but I asked around here and it seems that this is a by design difference between the two languages. In C#, if your delegate takes a MouseEventArgs (which derives from RoutedEventArgs), the parameter in the signature of the method can be RoutedEventArgs. Apparently in VB the types in the signature of the delegate and the parameter types have to match exactly.

      If there are any VB experts reading this, please feel free to chime in and confirm or correct this.

  5. Ruurd Boeke

    Hi Beatriz,

    I think it’s weird that one can not use events within styles. I have a style that shows a button when ‘IsSelected’ (by a trigger). When the button is clicked, I want something to happen. But I can’t use the Click event from that button. Why is that?

    • Bea

      Ruurd,

      I’m not sure I understand your question. What element are you styling, the Button or something else? Buttons don’t have an IsSelected property. Or do you mean you have a Button within a template? If you post your xaml here or in the Avalon Forum I will take a look.

      Bea

  6. jane

    Hi Beatriz,your example is really great.But I meet some troubles when I make the tree.If the tree have many more levels,how can I to deal with it.
    Look forward to your help.

    • Bea

      Hi Jane,

      I uploaded to my server a sample that does custom grouping with two levels of groups: first it groups by the first letter of the Greek God or Hero’s name, and then it groups by type. You can find my sample here.

      Let me know if this is what you were looking for.

      Bea

  7. sergey

    Hi Beatriz! Blog is really wonderfull! I’ll apreciate, if you answer one question. You wrote that: “Notice also that the groups don’t have to be a string, they can be any object you want.”. The question is if return type of Convert() method is a custom class(for example it has two properties: Id and Name), how could id be displayed in the header of the group? Thanks.

    • Bea

      Hi Sergey,

      Thanks for your nice words.

      You can control how the group name is displayed and what information you show, in the template for the group. In this particular sample, I am grabbing the template I want from the resources, in the “SelectTemplate” method. For group headers, I am picking the template with key “GroupTemplate”, so this is the template you should change to display ID only. You can do this by changing the Path of the Binding. If you look at the “GroupTemplate” in this sample, you will see the following binding: {Binding Path=Name}. Most likely you will simply need to change the Path to Path=ID.

      Does this answer your question?

      Bea

  8. Shanty

    Hi Bea (por cierto, por tu nombre, quizá eres de habla latina, no lo sé).
    I’m researching doin’ the same as you, but grouping at secnd level, not first. I think mixing between Convert and putting a HierarchicalDataTemplate before CollectionViewSource, I’ll find the solution, but…
    Am I in the correct way??

    Regards

    • Bea

      Hi Shanty,

      If you want to do custom grouping at the second level, you can simply define another group converter and add a second PropertyGroupDescription to the GroupDescriptions collection of the CollectionViewSource with that converter. I uploaded a project with the xaml I described here. Let me know if this helps.

      Sorry, my spanish is not that great. :) I speak european portuguese.

      Bea

  9. Doug Hagan

    Hi Beatriz,

    Is there a way to specify styles for TreeViewItems based upon the Type of the TreeViewItem? For example, I want to have the Account expander and node to have a different style than the Campaign expander and node. Since I am stylizing the TreeViewItem type I’m not sure how to break out the underlying data types.

    Thanks for all of your helpful posts!

    <Style d:IsControlPart=”True” TargetType=”{x:Type TreeViewItem}”>
    .
    .
    .
    </Style>

    <src:CustomerDataList x:Key=”MyList”/>
    <HierarchicalDataTemplate DataType = “{x:Type src:CustomerData}” ItemsSource = “{Binding Path=AccountDataList}” >
    <TextBlock Text=”{Binding Path=Name}” />
    </HierarchicalDataTemplate>
    <HierarchicalDataTemplate DataType = “{x:Type src:AccountData}” ItemsSource = “{Binding Path=CampaignDataList}”>
    <TextBlock Text=”{Binding Path=Name}” />
    </HierarchicalDataTemplate>

    • Bea

      Hi Doug,

      I’ve already followed up with your personally, but I will reply to your question here too for future reference.

      TreeView has a property named ItemContainerStyleSelector which takes a StyleSelector object. This allows us to pick a Style for each item, based on whatever custom logic we want. In this case, the logic is simple, we simply need to check for the type of the data item, which we get as a parameter to the SelectStyle method:

      public class ContainerStyle : System.Windows.Controls.StyleSelector
      {
      (…)

      public override Style SelectStyle(object item, DependencyObject container)
      {
      if(item is ClassA)
      {
      return styleClassA;
      }
      else if(item is ClassB)
      {
      return styleClassB;
      }
      return null;
      }
      }

      You can find here a sample with this code.

      I’ve seen people get stuck with this scenario because we need access to the styles, which are typically defined in the main Window. In this app I simply added a couple of public properties in the ContainerStyle class of type Style, and I set those properties to the styles I defined in XAML. There are probably different ways to do this.

      Let me know if this helps and if you have any further questions.

  10. Chris

    Hi Bea.
    I’ve been looking over this code for grouping and sorting data in a treeview, it’s been very helpful there seem to be very few examples of how to do this.
    However I’ve run into a stumbling block. The converter for grouping the data seems to have a null CultureInfo. This is an issue for me, as we are grouping Dates by months and need to display the localised name.
    I was wondering if you knew if this was an issue with .NET, or there was something missing from the code?

    • Bea

      Hi Chris,

      The group converter’s CultureInfo is taken directly from the view’s Culture property. If you’re using a CollectionViewSource and set its Culture property, it will set the Culture property of the view it creates to be the same as its own property. If you’re not using a CollectionViewSource, you can set the Culture property of the view directly.

      This behavior is a bit inconsistent compared to the Binding’s converter, which always gets it culture from the target element’s Language property. It seems to me that if the group converter fell back to the element’s Language when the CVS’s Culture is null, your scenario would work as you expected.

      I wonder if getting the current culture through code (CultureInfo.CurrentCulture) in the converter would work for you. If not, could you set the CollectionViewSource’s Culture?

      Bea

  11. Andrew R

    Thanks, this helped me get exactly what I wanted without have to re-write my base classes.

Comments are closed.