Static Grid, Dynamic Content – Part 2: The Stragglers

By | October 23, 2015

In my previous post I talked about how to get a dynamic set of content chunked up in to batches that could then be displayed in a static grid, stacked within an items control. This works great if your dataset is evenly divisible by the number of items in your grid, but what if it’s not?

There are two pieces to the magic you’ll see today. The first is flagging a batch as “incomplete” aka “these are the stragglers”. The second is determining how to display those guys vs the nice pretty static grid that was all laid out nicely based on being full.

Part 1: Flag the straggler batches

In the previous post we had a method that chunked up our dataset in to batches. Here’s a refresher:

   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: }

Now the caveat here is that the last batch returned, if the collection isn’t divisible by 13, will have < 13 items in it. This is a problem for the ensuing XAML because the indexers go to [12] regardless of the content; the control assumes 13 (hence why I elected to put ‘13’ in the class name). If you don’t feed it at least 13 items, boom.

So, let’s see how we could take care of this:

   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 13;
  10:             i += batchSize;
  11:
  12:             if (batchSize < 13) batchSize = 0 - batchSize; // flip the sign if we're a straggler set;
  13:
  14:             yield return new ItemViewModelBatch(batch.Take(Math.Abs(batchSize)), batchSize < 0);
  15:             batch = batch.Skip(Math.Abs(batchSize));
  16:         }
  17:     }
  18: }

We’re doing some fancy footwork here; if we determine that we’re a “straggler set,” flip the sign of the size of the batch, and feed the sign of the batch size in to the ItemViewModelBatch class. That class has been changed like this:

   1: public class ItemViewModelBatch
   2: {
   3:     public ItemViewModelBatch(IEnumerable<ItemViewModel> items, bool asRemainder = false)
   4:     {
   5:         this.Items = items.ToList();
   6:         this.AsRemainder = asRemainder;
   7:     }
   8:
   9:     public IList<ItemViewModel> Items { get; }
  10:     public bool AsRemainder { get; }
  11: }

So now we can know if this batch is a “straggler” batch (which I’ve named a bit more professional with “AsRemainder”.

Ok sweet. We’ve got our batches and we’re noting which one is made up of stragglers. Let’s handle displaying them now.

Step 2: Determine when to display the 13-item grid or the stragglers

Any ItemsControl has a nifty little property on it called ItemTemplateSelector. The purpose of this property is to specify which Template is used to display a particular item. Now, our items are all ItemViewModelBatch objects. All of which have their # of items, and whether or not they contain the stragglers. See where I’m headed?

Set the ItemTemplateSelector property of our ItemsControl to an instance of a new class, let’s call it ItemDataTemplateSelector (because I’m super original) like so:

   1: <ItemsControl x:Name="Items"
   2:                 ItemsSource="{x:Bind ViewModel.BatchedItems, Mode=OneWay}">
   3:     <ItemsControl.ItemsPanel>
   4:         <ItemsPanelTemplate>
   5:             <StackPanel HorizontalAlignment="Stretch"
   6:                         VerticalAlignment="Stretch"
   7:                         Orientation="Vertical" />
   8:         </ItemsPanelTemplate>
   9:     </ItemsControl.ItemsPanel>
  10:     <ItemsControl.ItemTemplateSelector>
  11:         <myNs:ItemDataTemplateSelector>
  12:             <myNs:ItemDataTemplateSelector.StandardGrid>
  13:                 <DataTemplate>
  14:                     <myControls:ItemGrid13 />
  15:                 </DataTemplate>
  16:             </myNs:ItemDataTemplateSelector.StandardGrid>
  17:             <myNs:ItemDataTemplateSelector.RemainderGrid>
  18:                 <DataTemplate x:DataType="vm:ItemViewModelBatch">
  19:                     <ItemsControl ItemsSource="{x:Bind Items}">
  20:                         <ItemsControl.ItemsPanel>
  21:                             <ItemsPanelTemplate>
  22:                                 <GridView MaximumNumberOfRowsOrColumns="4" />
  23:                             </ItemsPanelTemplate>
  24:                         </ItemsControl.ItemsPanel>
  25:                         <ItemsControl.ItemTemplate>
  26:                             <!-- Template your stragglers here -->
  27:                         </ItemsControl.ItemTemplate>
  28:                     </ItemsControl>
  29:                 </DataTemplate>
  30:             </myNs:ItemDataTemplateSelector.RemainderGrid>
  31:         </myNs:ItemDataTemplateSelector>
  32:     </ItemsControl.ItemTemplateSelector>
  33: </ItemsControl>

Our new ItemDataTemplateSelector will look like this:

   1: class ItemDataTemplateSelector : DataTemplateSelector
   2: {
   3:     public DataTemplate RemainderGrid { get; set; }
   4:     public DataTemplate StandardGrid { get; set; }
   5:     protected override DataTemplate SelectTemplateCore(object item) => base.SelectTemplateCore(item);
   6:     protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
   7:     {
   8:         var batch = item as ItemViewModelBatch;
   9:         if (batch != null)
  10:         {
  11:             return batch.AsRemainder ? this.RemainderGrid : this.StandardGrid;
  12:         }
  13:
  14:         return base.SelectTemplateCore(item, container);
  15:     }
  16: }

In the XAML you can see we set the Remainder and Standard grids to be what we want, and then we let the selector choose based on the flag we’ve set on our batch. Voila! In this example, the “Standard” grid is our 13-item user control, while the “Remainder” grid is a GridView with all our items in it (templated as you define in the XAML).