On a recent project, we wanted to lay out some dynamic content in a very specific way, and have that repeat a la the USA Today app for Windows 10.
The main challenge facing us when doing this is that Grid isn’t an ‘ItemsControl’ and therefore can’t be bound to a collection of data.
So how do we solve it?
The solution, like so many, can be arrived at by chunking the problem up in to pieces.
Step 1: Get a control to which we can bind a collection of data
We can accomplish this by utilizing WinRT’s generic ‘ItemsControl’ like so:
<ScrollViewer Grid.Row="1" HorizontalScrollMode="Disabled" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <ItemsControl ItemsSource="{x:Bind BatchedItems, Mode=OneWay}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Orientation="Vertical" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </ScrollViewer>
A few things are going on here:
1) We wrap the ItemsControl in a ScrollViewer so we’ll be able to scroll through the Grids that get put in the control
2) The ItemsSource isn’t bound directly to all our items (because we don’t know that answer up front so how could we write a grid for it?), instead it’s bound to a “batched” collection of our items. We’ll see what this looks like in a minute
3) The PanelTemplate for the items (ie: the panel used to house all the items shown in the control) is a simple vertical StackPanel
Step 2: Chunk up the collection in to batches
Let’s take a look at the batched items:
1: public IEnumerable<ItemViewModelBatch> BatchedItems
2: {
3: get
4: {
5: IEnumerable<ItemViewModel> batch = this.Items.ToList(); // To create a copy
6: int i = 0;
7: while (batch.Any())
8: {
9: int batchSize = Math.Min(batch.Count(), 13); // chunk in to groups of 13. This can be whatever number fits in to the grid you design
10: i += batchSize;
11: yield return new ItemViewModelBatch(batch.Take(batchSize));
12: batch = batch.Skip(batchSize);
13: }
14: }
15: }
This getter takes the Items collection on the View (we’re using x:Bind here) and chunks it up in to a collection of ItemViewModelBatch objects. Each Batch object has a finite (pre-defined) number of ItemViewModel objects in it, which came from the Items collection. These Batch objects are what our ItemsControl above uses as a single item as it renders.
Step 3: Render each of the batches
The easiest way to get this nice and manageable is to create a UserControl for each batch size you want to render. In our case, let’s look at a UserControl for 13 items:
<UserControl x:Class="MyApp.ItemGridLayouts.ItemGrid13" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:MyApp.ItemGridLayouts" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <ContentControl DataContext="{x:Bind ViewModel.Items[0]}" Grid.ColumnSpan="2" Grid.RowSpan="2" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_2x2}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[1]}" Grid.Column="2" Grid.RowSpan="2" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x2}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[2]}" Grid.Column="3" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x1}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[3]}" Grid.Column="3" Grid.Row="1" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x1}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[4]}" Grid.Column="4" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x1}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[5]}" Grid.Column="4" Grid.Row="1" Grid.RowSpan="2" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x2}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[6]}" Grid.Row="2" Grid.RowSpan="2" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x2}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[7]}" Grid.Column="1" Grid.Row="2" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x1}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[8]}" Grid.Column="1" Grid.Row="3" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x1}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[9]}" Grid.Column="2" Grid.Row="2" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x1}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[10]}" Grid.Column="2" Grid.Row="3" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x1}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[11]}" Grid.Column="3" Grid.Row="2" Grid.RowSpan="2" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x2}" /> <ContentControl DataContext="{x:Bind ViewModel.Items[12]}" Grid.Column="4" Grid.Row="3" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ContentTemplate="{StaticResource SingleItemTemplate_1x1}" /> </Grid> </UserControl>
public sealed partial class ItemGrid13 : UserControl { public ItemGrid13() { this.DataContextChanged += (s, e) => this.ViewModel = e.NewValue as ViewModels.ItemViewModelBatch; this.InitializeComponent(); } public ItemViewModelBatch ViewModel { get; private set; } }
The above layout, when properly templated, would yield a layout exactly like USA Today’s shown here:
1) Notice our Grid is statically laid out but we are binding to the contents of our ViewModel (batch) Items property for individual items in the collection by simply using indexers!
2) We use a ContentControl to accomplish the rendering of each item since we can set DataContext on it.
3) We template each item as it should be for the row/col span we’ve assigned to it (or any template we want for Item N, in reality). These templates simply render an Item the way we want it to; nothing special going on in them. In this case, the 1×2 template gets defined as:
<DataTemplate x:Key="SingleItemTemplate_1x2"> ... </DataTemplate>
The code thus far will work splendidly if your collection can be evenly batched by 13s. Check out Part 2 where we handle the “stragglers”!