One of the questions that got asked at a recent Windows 8 developer camp was around how the XAML GridView control can be used to set up a view that looks something like the one used in the People app which is a bit like this;
That is – a GridView that is grouping its data but is then displaying the group headers inline with the rest of the data. This is different from the way you’d see a GridView in the Grid application templates that ship in Visual Studio.
The GridView is a complex control with quite a lot of templates associated with it and, in some ways, working with it reminds me of working with the DataGrid control in WPF/Silverlight in that quite often I’m looking at a piece of UI and wondering which template is involved in putting that piece of UI onto the screen.
I’m not sure what the ‘best’ way is of figuring out a control like this one but one approach I sometimes take is to wander into Visual Studio and take an instance of the control and edit a copy of its template using the graphical editor. With a GridView that gives me;
<Style x:Key="GridViewStyle1" TargetType="GridView"> <Setter Property="Padding" Value="0,0,0,10" /> <Setter Property="IsTabStop" Value="False" /> <Setter Property="TabNavigation" Value="Once" /> <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" /> <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled" /> <Setter Property="ScrollViewer.HorizontalScrollMode" Value="Enabled" /> <Setter Property="ScrollViewer.IsHorizontalRailEnabled" Value="False" /> <Setter Property="ScrollViewer.VerticalScrollMode" Value="Disabled" /> <Setter Property="ScrollViewer.IsVerticalRailEnabled" Value="False" /> <Setter Property="ScrollViewer.ZoomMode" Value="Disabled" /> <Setter Property="IsSwipeEnabled" Value="True" /> <Setter Property="ItemContainerTransitions"> <Setter.Value> <TransitionCollection> <AddDeleteThemeTransition /> <ContentThemeTransition /> <ReorderThemeTransition /> <EntranceThemeTransition IsStaggeringEnabled="False" /> </TransitionCollection> </Setter.Value> </Setter> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <WrapGrid Orientation="Vertical" /> </ItemsPanelTemplate> </Setter.Value> </Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="GridView"> <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}"> <ScrollViewer x:Name="ScrollViewer" HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}" IsHorizontalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsHorizontalScrollChainingEnabled}" IsVerticalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsVerticalScrollChainingEnabled}" IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}" TabNavigation="{TemplateBinding TabNavigation}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}" VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}" ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}"> <ItemsPresenter HeaderTemplate="{TemplateBinding HeaderTemplate}" Header="{TemplateBinding Header}" HeaderTransitions="{TemplateBinding HeaderTransitions}" Padding="{TemplateBinding Padding}" /> </ScrollViewer> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
Apologies for pasting in such a large lump of XAML but the style for this control is a large lump of XAML 🙂
What this says to me is that;
- A GridView is a ScrollViewer wrapped around an ItemsPresenter.
- The items panel that is used is a WrapGrid.
- There are some intriguing Header and HeaderTemplate properties that can easily distract you from your purpose if you are thinking about displaying headers on grouped data.
If I take a simple use of a GridView;
<GridView x:Name="myGridView"> <GridView.Items> <x:String>One</x:String> <x:String>Two</x:String> <x:String>Three</x:String> <x:String>Four</x:String> <x:String>Five</x:String> <x:String>Six</x:String> <x:String>Seven</x:String> <x:String>Eight</x:String> <x:String>Nine</x:String> <x:String>Ten</x:String> </GridView.Items> </GridView>
Then that’s going to create and display my items for me and, in doing so, it’s going to do a few things;
- create a panel to put the set of items into.
- create an instance of the item template (if any) for each data item.
- (if necessary) create a GridViewItem to wrap around each instantiated item template and style that GridViewItem in accordance with the ItemContainerStyle.
I once drew a PowerPoint slide about this which I’ll repeat here – it’s probably not 100% perfect and it predates GridView but I think it’s fairly close to what goes on;
By default what I get displayed is;
but if I change the ItemTemplate;
<GridView x:Name="myGridView"> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding}" /> </Grid> </DataTemplate> </GridView.ItemTemplate> <GridView.Items> <x:String>One</x:String> <x:String>Two</x:String> <x:String>Three</x:String> <x:String>Four</x:String> <x:String>Five</x:String> <x:String>Six</x:String> <x:String>Seven</x:String> <x:String>Eight</x:String> <x:String>Nine</x:String> <x:String>Ten</x:String> </GridView.Items> </GridView>
then it does the right thing but looks terrible;
and you can spend many a long, happy hour trying to figure out where that alignment is coming from until you realise that each item is being wrapped in a GridViewItem in accordance with the ItemContainerStyle as in;
<GridView x:Name="myGridView"> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding}" TextAlignment="Center" VerticalAlignment="Center" /> </Grid> </DataTemplate> </GridView.ItemTemplate> <GridView.ItemContainerStyle> <Style TargetType="GridViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch"> </Setter> <Setter Property="VerticalContentAlignment" Value="Stretch"> </Setter> </Style> </GridView.ItemContainerStyle> <GridView.Items> <x:String>One</x:String> <x:String>Two</x:String> <x:String>Three</x:String> <x:String>Four</x:String> <x:String>Five</x:String> <x:String>Six</x:String> <x:String>Seven</x:String> <x:String>Eight</x:String> <x:String>Nine</x:String> <x:String>Ten</x:String> </GridView.Items> </GridView>
and that ItemContainerSyle is contributing things like hover and selection states as illustrated below;
and if you go so far as to change the control template for the GridViewItem within that style;
<GridView x:Name="myGridView"> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding}" TextAlignment="Center" VerticalAlignment="Center" /> </Grid> </DataTemplate> </GridView.ItemTemplate> <GridView.ItemContainerStyle> <Style TargetType="GridViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch"> </Setter> <Setter Property="VerticalContentAlignment" Value="Stretch"> </Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate> <ContentPresenter /> </ControlTemplate> </Setter.Value> </Setter> </Style> </GridView.ItemContainerStyle> <GridView.Items> <x:String>One</x:String> <x:String>Two</x:String> <x:String>Three</x:String> <x:String>Four</x:String> <x:String>Five</x:String> <x:String>Six</x:String> <x:String>Seven</x:String> <x:String>Eight</x:String> <x:String>Nine</x:String> <x:String>Ten</x:String> </GridView.Items> </GridView>
Then you lose quite a lot, including things like the hover behaviour and the selection behaviour and so on (unless you go to pains to put them back with your own variants). This is shown below where Item 10 is selected, but you wouldn’t know it any more.
Taking that modification away, if I want to change the panel that’s being used to display these items then I can do and it’s relatively easy to do and I can either stick with the WrapGrid that’s being used and tweak it so that it displays itself in different ways;
<GridView x:Name="myGridView"> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue" Width="192" Height="192"> <TextBlock Text="{Binding}" TextAlignment="Center" VerticalAlignment="Center" /> </Grid> </DataTemplate> </GridView.ItemTemplate> <GridView.ItemsPanel> <ItemsPanelTemplate> <WrapGrid Orientation="Horizontal" MaximumRowsOrColumns="3" /> </ItemsPanelTemplate> </GridView.ItemsPanel> <GridView.Items> <x:String>One</x:String> <x:String>Two</x:String> <x:String>Three</x:String> <x:String>Four</x:String> <x:String>Five</x:String> <x:String>Six</x:String> <x:String>Seven</x:String> <x:String>Eight</x:String> <x:String>Nine</x:String> <x:String>Ten</x:String> </GridView.Items> </GridView>
and that line 17 above will give me;
or I can change it again;
<GridView.ItemsPanel> <ItemsPanelTemplate> <WrapGrid Orientation="Vertical" MaximumRowsOrColumns="2" /> </ItemsPanelTemplate> </GridView.ItemsPanel>
and that line 5 will give me;
or maybe change the panel type altogether;
<GridView.ItemsPanel> <ItemsPanelTemplate> <StackPanel VerticalAlignment="Top" Orientation="Horizontal" /> </ItemsPanelTemplate> </GridView.ItemsPanel>
and that line 3-5 will give me;
Going back to that Header and HeaderTemplate property – what do they give me? They’re nothing to do with grouping of data and everything to do with just displaying a header on the top of the control so if I simply change my XAML to be;
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <GridView x:Name="myGridView"> <GridView.Header> <x:String>My GridView</x:String> </GridView.Header> <GridView.HeaderTemplate> <DataTemplate> <Border BorderThickness="1" BorderBrush="Silver"> <TextBlock Text="{Binding}" Style="{StaticResource HeaderTextStyle}" /> </Border> </DataTemplate> </GridView.HeaderTemplate> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue" Width="192" Height="192"> <TextBlock Text="{Binding}" TextAlignment="Center" VerticalAlignment="Center" /> </Grid> </DataTemplate> </GridView.ItemTemplate> <GridView.Items> <x:String>One</x:String> <x:String>Two</x:String> <x:String>Three</x:String> <x:String>Four</x:String> <x:String>Five</x:String> <x:String>Six</x:String> <x:String>Seven</x:String> <x:String>Eight</x:String> <x:String>Nine</x:String> <x:String>Ten</x:String> </GridView.Items> </GridView>
Then that simply adds a header to the GridView with (in this case) a silver border around it;
It’s worth saying that ItemTemplates can be controlled from code dynamically to vary on a per-item basis by using an ItemTemplateSelector as can the ItemContainerStyle (using a StyleSelector).
With all of that said, how does grouping creep into this control? First thing – as stated up in the docs – you have to use a CollectionViewSource in order to bind to a collection that is grouped and that brings in the properties IsSourceGrouped and ItemsPath.
If I take a blank or ‘vanilla’ GridView;
<GridView x:Name="myGridView"> </GridView>
and just ‘manually’ feed it some data rather than binding it;
Random rand = new Random(); CollectionViewSource collectionViewSource = new CollectionViewSource(); collectionViewSource.IsSourceGrouped = true; collectionViewSource.ItemsPath = new PropertyPath("Items"); var groups = Enumerable.Range((int)'A', 26).Select( letter => { var gp = new Group() { GroupName = string.Format("{0}", (char)letter) }; gp.Items = new List<Item>( Enumerable.Range(1, rand.Next(5, 15)).Select( index => new Item() { ItemName = string.Format("{0}{1}", (char)letter, index), Group = gp } ) ); return (gp); } ); collectionViewSource.Source = groups; this.myGridView.ItemsSource = collectionViewSource.View;
then it displays;
Then the control is definitely looking inside of my groups to find all of the items but the UI doesn’t really say anything about the groups themselves. Changing the item template still has an effect;
and, very clearly, the ItemContainerStyle is still doing its work and GridViewItems are being created. So, where are my groups? It comes into play (as the docs say) when the GroupStyle is changed. For example, even setting a blank looking GroupStyle has an impact;
<GridView x:Name="myGridView"> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding ItemName}" /> </Grid> </DataTemplate> </GridView.ItemTemplate> <GridView.GroupStyle> <GroupStyle /> </GridView.GroupStyle> </GridView>
in that I now have;
which is “interesting” in itself. The GroupStyle has a ContainerStyle (plus a ContainerStyleSelector such that it can be changed dynamically on a per-item basis from code) and then a HeaderTemplate (plus a HeaderTemplateSelector such that it can be changed dynamically on a per-item basis from code) and then a Panel.
In meeting this control for the first time, this was the bit that was most different to me and these items show up as editable templates in Visual Studio or Blend (as do all the other templates I’ve been talking about :-));
but only the item container offers a chance to create a copy of the default style.
If I manipulate the panel that’s being used for my items to be a StackPanel that’s orientated horizontally;
<GridView x:Name="myGridView"> <GridView.GroupStyle> <GroupStyle> <GroupStyle.Panel> <ItemsPanelTemplate> <StackPanel Margin="2" Orientation="Horizontal" Background="Green"/> </ItemsPanelTemplate> </GroupStyle.Panel> </GroupStyle> </GridView.GroupStyle> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding ItemName}" /> </Grid> </DataTemplate> </GridView.ItemTemplate> </GridView>
then that gives me a layout;
or I could use a WrapGrid to give me something like;
<GridView x:Name="myGridView"> <GridView.GroupStyle> <GroupStyle> <GroupStyle.Panel> <ItemsPanelTemplate> <VariableSizedWrapGrid Background="Green" Margin="2" Orientation="Horizontal" MaximumRowsOrColumns="3" /> </ItemsPanelTemplate> </GroupStyle.Panel> </GroupStyle> </GridView.GroupStyle> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding ItemName}" /> </Grid> </DataTemplate> </GridView.ItemTemplate> </GridView>
giving me a layout;
And I can manipulate the way in which my headers are being displayed via the HeaderTemplate;
<GridView x:Name="myGridView"> <GridView.GroupStyle> <GroupStyle> <GroupStyle.Panel> <ItemsPanelTemplate> <VariableSizedWrapGrid Background="Green" Margin="2" Orientation="Horizontal" MaximumRowsOrColumns="3" /> </ItemsPanelTemplate> </GroupStyle.Panel> <GroupStyle.HeaderTemplate> <DataTemplate> <Grid> <Ellipse Width="48" Height="48" Fill="White" Margin="1" /> <TextBlock VerticalAlignment="Center" TextAlignment="Center" Foreground="Black" Style="{StaticResource SubheaderTextStyle}" Text="{Binding GroupName}" /> </Grid> </DataTemplate> </GroupStyle.HeaderTemplate> </GroupStyle> </GridView.GroupStyle> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding ItemName}" /> </Grid> </DataTemplate> </GridView.ItemTemplate> </GridView>
giving me (ok, I know the text isn’t aligned properly);
but what if I want to control the relationship between the header and the panel? That’s in the ContainerStyle which defines a Border wrapped around a Grid wrapper around a ContentControl and an ItemsControl. So, if I wanted my group header to be centred to the left of my panel;
<GridView x:Name="myGridView"> <GridView.GroupStyle> <GroupStyle> <GroupStyle.Panel> <ItemsPanelTemplate> <VariableSizedWrapGrid Background="Green" Margin="2" Orientation="Horizontal" MaximumRowsOrColumns="3" /> </ItemsPanelTemplate> </GroupStyle.Panel> <GroupStyle.HeaderTemplate> <DataTemplate> <Grid> <Ellipse Width="48" Height="48" Fill="White" Margin="1" /> <TextBlock VerticalAlignment="Center" TextAlignment="Center" Foreground="Black" Style="{StaticResource SubheaderTextStyle}" Text="{Binding GroupName}" /> </Grid> </DataTemplate> </GroupStyle.HeaderTemplate> <GroupStyle.ContainerStyle> <Style TargetType="GroupItem"> <Setter Property="IsTabStop" Value="False" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="GroupItem"> <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <ContentControl x:Name="HeaderContent" VerticalContentAlignment="Center" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTransitions="{TemplateBinding ContentTransitions}" Content="{TemplateBinding Content}" IsTabStop="False" Margin="{TemplateBinding Padding}" TabIndex="0" /> <ItemsControl x:Name="ItemsControl" IsTabStop="False" ItemsSource="{Binding GroupItems}" Grid.Column="1" TabIndex="1" TabNavigation="Once"> <ItemsControl.ItemContainerTransitions> <TransitionCollection> <AddDeleteThemeTransition /> <ContentThemeTransition /> <ReorderThemeTransition /> <EntranceThemeTransition IsStaggeringEnabled="False" /> </TransitionCollection> </ItemsControl.ItemContainerTransitions> </ItemsControl> </Grid> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </GroupStyle.ContainerStyle> </GroupStyle> </GridView.GroupStyle> <GridView.ItemTemplate> <DataTemplate> <Grid Background="Blue"> <TextBlock Text="{Binding ItemName}" /> </Grid> </DataTemplate> </GridView.ItemTemplate> </GridView>
then that gives me;
and one thing that style seems to tell me (via its TargetType of GroupItem) is that when the control is displaying grouped data it does not display a GridViewItem for each group but, instead, a GroupItem as the docs state;
“ContainerStyle – Gets or sets the style that is applied to the GroupItem generated for each item”
Returning to “The Point”
With all of that said, can the control achieve that layout that I wanted of inlining the header content with the item detail for each item in the group? It’s hard to see how it’s going to do it. In the diagram;
the panel in red would presumably need to be some kind of wrap panel oriented vertically in order to wrap groups into the next column when they overflow the current one but then a group brings its own panel with it and that panel would seem to need to span two columns of the parent wrap panel in order to work and it’s hard to see how that would happen.
As far as I can figure out, a way to do this would be to flatten the data and use some kind of template selector to display groups differently from items. For example – keeping the original construction of the data but trying to flatten it out;
Random rand = new Random(); // Set up the grouped data exactly as before. var groups = Enumerable.Range((int)'A', 26).Select( letter => { var gp = new Group() { GroupName = string.Format("{0}", (char)letter) }; gp.Items = new List<Item>( Enumerable.Range(1, rand.Next(5, 15)).Select( index => new Item() { ItemName = string.Format("{0}{1}", (char)letter, index), Group = gp } ) ); return (gp); } ); // and flatten it into a list of anonymous types - probably a much smarter way // of doing this. var g = groups.Select(gp => new { IsGroup = true, Name = gp.GroupName }); var i = groups.SelectMany(gp => gp.Items).Select(item => new { IsGroup = false, Name = item.ItemName }); var u = g.Union(i).OrderBy(item => item.Name); this.myGridView.ItemsSource = u;
where I’m relying on the names of my data items and groups when I use that OrderBy and then if I have a little template selector;
public class MyTemplateSelector : DataTemplateSelector { public DataTemplate Group { get; set; } public DataTemplate Item { get; set; } protected override Windows.UI.Xaml.DataTemplate SelectTemplateCore(object item, Windows.UI.Xaml.DependencyObject container) { // unfortunately, our item is an anonymous type because I was lazy and now // I'm being more lazy. dynamic d = (dynamic)item; return (d.IsGroup ? this.Group : this.Item); } }
I can bring that into my XAML and define a couple of templates (which really need some constants/styles defined for them to tidy them up);
<Page.Resources> <DataTemplate x:Key="groupTemplate"> <Grid Width="192" Margin="0,0,6,0" Background="LightBlue"> <TextBlock Margin="3" Foreground="White" Style="{StaticResource HeaderTextStyle}" Text="{Binding Name}" TextAlignment="Left"/> </Grid> </DataTemplate> <DataTemplate x:Key="itemTemplate"> <Grid Background="LightGreen" Width="192"> <TextBlock Foreground="White" Style="{StaticResource SubheaderTextStyle}" Text="{Binding Name}" TextAlignment="Left" Margin="3"/> </Grid> </DataTemplate> <local:MyTemplateSelector x:Key="mySelector" Item="{StaticResource itemTemplate}" Group="{StaticResource groupTemplate}"> </local:MyTemplateSelector> </Page.Resources> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <GridView x:Name="myGridView" ItemTemplateSelector="{Binding Source={StaticResource mySelector}}"> </GridView> </Grid>
giving me a layout of;
which is about as close as I’m going to get for this post – I haven’t managed to do this while keeping the grouped nature of the data so if there’s a way to do that, let me know.