W10: Getting a grouped variable sized GridView to do what its supposed to do.

Got a GridView with a VariableSizedWrapGrid and groups? Want it working on W10/UWP? Yea, that doesn’t quite work out of the box now does it…

Took me a bit, but was able to get it to do its job.

Getting a GridView up and running with some groups in an uniform grid is definitely possible out of the box.
GridView with uniform grid and groups

Plenty of material can be found on how to do this, I won’t go into the details here.

Now, what if want to use a VariableSizedWrapGrid inside this GridView? For simplicity sake (and because that was all we needed at this moment), we only have items that could span multiple rows, and don’t span multiple columns.
We’d like to get to this layout:
Variable Grid

What do we need to do for to get this.
first of all, we define our GridView:


  
    
    
  
  
    
      
    
  
  
    
      
        
          
        
      
      
        
          
        
      
    
  

For the purpose of this article, take note of the defined ItemWidth of 100 and ItemHeight of 50.

As you can see, I have two behaviours defined, and am using an ItemTemplateSelector.
I had to use an ItemTemplateSelector to switch templates if an item needs a RowSpan of 2 (or more).
The templates:


  
    
  



  
	
  

Note that the widths of these templates are the same as our ItemWidth, and that the LongItemTemplate uses twice the height.

The TemplateSelector is defined as followed and references these two templates.


Its class file:

public class TemplateSelector : DataTemplateSelector
{
  public DataTemplate DefaultTemplate { get; set; }
  public DataTemplate LongTemplate { get; set; }

  protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
  {
    var data = item as IGridViewSize;
    var uiEle = container as UIElement;
	
    if (data == null)
      return DefaultTemplate;
	
    if (data.RowSpan == 2)
    {
      VariableSizedWrapGrid.SetRowSpan(uiEle, 2);
      return LongTemplate;
    }
    else
      return DefaultTemplate;
  }
}

We defined the interface IGridViewSize on our items, just to have them define a RowSpan. Its a bit crude here, as I’m only checking for a RowSpan of 2, but you get the idea. Use the LongTemplate if its a RowSpan of 2, and set the attached property for the VariableSizedWrapGrid.

For the behaviours: The first one “FixVariableSizedWrapGridsHeightBehaviour” makes sure the VariableSizedWrapGrid inside the GridView (not the ones inside the GroupItems!) gets the correct height. By default, this seems to stretch as far as its children require. But we don’t want this. We want to set its maximum height to the height of the Gridview. This behaviour does that:

public class FixVariableSizedWrapGridsHeightBehaviour : DependencyObject, IBehavior
{
  [...]

  protected virtual void ElementAttached()
  {
    [...]
    
    ((GridView)AssociatedObject).SizeChanged += FixVariableSizedWrapGridsHeightBehaviour_SizeChanged;
  }

  private void FixVariableSizedWrapGridsHeightBehaviour_SizeChanged(object sender, SizeChangedEventArgs e)
  {
    var innerVariableSizedWrapGrid = 
      ControlHelper.FindChild(((GridView)AssociatedObject));

    if (innerVariableSizedWrapGrid != null)
      innerVariableSizedWrapGrid.MaxHeight = ((GridView)AssociatedObject).ActualHeight;
  }
}

(ControlHelper contains some methods to traverse the VisualTree to find some child/parent elements, many variants can be found online.)

Now, without anything else, we’d have a semi-working GridView. It will wrap the items, and these items can have varying RowSpans. BUT, the GridView’s VariableSizedWrapGrid will determine the required width for the first GroupItem and apply it to all other GroupItems. This causes some elements to fall outside of the bounds, or create a lot of whitespaces.

The second behaviour “DetermineCorrectGroupItemWidthsBehaviour” fixes this by calculating the actual size needed for each GroupItem.

class DetermineCorrectGroupItemWidthsBehaviour : DependencyObject, IBehavior
{
  [...]
  protected virtual void ElementAttached()
  {
    ((GridView)AssociatedObject).LayoutUpdated 
      += DetermineCorrectGroupItemWidthsBehaviour_LayoutUpdated;
  }
  
  private void DetermineCorrectGroupItemWidthsBehaviour_LayoutUpdated(object sender, object e)
  {
    //Assumes a vertically stacked gridview
    if (((GridView)AssociatedObject) == null)
      return;
    
    var outerVarWrapGrid = (VariableSizedWrapGrid)((GridView)AssociatedObject).ItemsPanelRoot;
    var firstGroupItem = ControlHelper.FindChild(((GridView)AssociatedObject));
    var innerVarWrapGrid = ControlHelper.FindChild(firstGroupItem);
    if (innerVarWrapGrid == null)
      return;
    
    //If I set this from the xaml, it doesn't get redrawn properly >_> ...
    outerVarWrapGrid.ItemWidth = innerVarWrapGrid.ItemWidth;
    
    var groupItems = ControlHelper.FindChildren(((GridView)AssociatedObject));
    foreach (var groupItem in groupItems)
    {
      var varWrapGrid = ControlHelper.FindChild(groupItem);
      if (varWrapGrid == null)
        continue;
    
      var itemHeight = varWrapGrid.ItemHeight;

      //Find the groups header to get its height:
      double headerHeight = 0;
      var header = ControlHelper.FindChild(groupItem, s => s.Name == "HeaderContent");
      if (header != null)
        headerHeight = header.ActualHeight;

      var maxItemsPerColumn = Math.Floor((groupItem.ActualHeight - headerHeight) / itemHeight);
      
      var items = ControlHelper.FindChildren(groupItem);
      
      //Because some items have a rowspan of 2 (or more), take these into account:
      int columns = 1;
      int itemsForCol = 0;
      foreach (var item in items)
      {
        var rowSpan = VariableSizedWrapGrid.GetRowSpan(item);
        if (itemsForCol + rowSpan <= maxItemsPerColumn)
        {
          //Still fits in this column:
          itemsForCol += rowSpan;
        }
        else
        {
          //Start a new column
          columns++;
          itemsForCol = rowSpan;
        }
      }
	
	  //the extra column fixes for some (dunno where) margin
      var requiredWidth = varWrapGrid.ItemWidth * (columns + 1); 
	
      groupItem.MinWidth = requiredWidth;
      groupItem.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, columns);
      groupItem.Height = outerVarWrapGrid.ActualHeight;
    }
  }
}

It's important to set the ItemWidth of the outer VariableSizedWrapGrid to the same dimension of the inner ones. This makes the math more simple 😉
Basicly, we give each GroupItem a RowSpan equal to the amount of rows required.

You can download a sample app with a "normal" uniform GridView and the (working) VariableSizedWrapGrid here.

-------

Some references that helped me along the way:
http://blogs.msdn.com/b/walaa/archive/2014/02/23/gridview-amp-multiple-item-templates.aspx
http://stackoverflow.com.exactgo.com/questions/11615802/variablesizedwrapgrid-and-wrapgrid-children-size-measuring

2 Responses so far.

  1. Micah Lewis says:
    Instead of the behaviors you’ve added as a workaround try adding ScrollViewer.VerticalScrollBarVisibility=”Disabled” to your .

Leave a Reply to Micah Lewis Cancel reply