Static Grid, Dynamic Content
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.
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(); // 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:
- 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!