How to implement custom grouping

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.
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!
February 13, 2006 at 1:23 pm
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
September 14, 2006 at 3:30 pm
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
February 14, 2006 at 6:57 am
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.
February 14, 2006 at 4:30 pm
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
February 15, 2006 at 2:56 pm
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.
February 16, 2006 at 4:35 pm
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?
February 18, 2006 at 7:28 am
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
February 18, 2006 at 8:44 pm
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.
February 20, 2006 at 11:38 pm
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
February 21, 2006 at 3:59 pm
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.
February 24, 2006 at 5:51 am
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
February 25, 2006 at 10:07 am
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
March 3, 2006 at 12:45 am
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
March 4, 2006 at 7:34 am
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>
August 12, 2007 at 10:55 am
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.
August 13, 2007 at 5:28 pm
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?
July 28, 2009 at 6:33 pm
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
November 20, 2009 at 11:35 am
Andrew R
Thanks, this helped me get exactly what I wanted without have to re-write my base classes.
October 24, 2010 at 10:57 am