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:
- We wrap the ItemsControl in a ScrollViewer so we’ll be able to scroll through the Grids that get put in the control
- 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
- 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();
6: int i = 0;
7: while (batch.Any())
8: {
9: int batchSize = Math.Min(batch.Count(), 13);
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:
- 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!
- We use a ContentControl to accomplish the rendering of each item since we can set DataContext on it.
- 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. Stay tuned for part 2 where we handle the stragglers!