Grid Size Sharing in WPF

The flexibility of the WPF ItemsControls allows you build column-row views of lists of data by building templates to represent each row. Although a GridView used in a ListView can also be used to provide these views the GridView introduces added complexity along with its extra features, making it more difficult to style and sometimes causing performance problems. To create a table layout with an ItemsControl (or ListBox) a top level Grid should be set up with 2 rows to contain the column headers and the ItemsControl itself. The Grids for the columns of the individual data rows are then set up inside a DataTemplate that is applied to the ItemsControl.ItemTemplate property so it will be repeated for each data item. This creates individual cells for each header and data value but to get a usable table we need the columns for all these Grids to line up properly.

There are a few different choices for lining up column sizes between these multiple Grids. The simplest option is just to set fixed Pixel sizes for all of the columns and make sure the ColumnDefinitions in the main Grid and DataTemplate Grid are the same. The Width values can also be refactored as Doubles out to Resources so any changes are applied to both Grids. Here’s the ColumnDefinitions for both the headers and template:

<Grid.ColumnDefinitions>  <ColumnDefinition Width="110"/>  <ColumnDefinition Width="90"/>  <ColumnDefinition Width="65"/>  <ColumnDefinition Width="55"/>  <ColumnDefinition Width="80"/>  </Grid.ColumnDefinitions>

This is fine if you need something quick, but a fixed width, non-resizable table isn’t going to be something that’s used very often. Making the columns resizable would make a lot more sense. So what happens when GridSplitters get added into this setup? The logical choice is to add them to the top level Grid that defines the headers, but when these are used they only resize the headers themselves. So how about inside the template? This is even worse because now you get individual resizing for each column in each row! This method obviously won’t work with resizing unless we add a lot of code.

To get all of these different Grids to stay synchronized, WPF includes size sharing functionality, accessed through the Grid.IsSharedSizeScope attached property and the SharedSizeGroup property available on ColumnDefinition and RowDefinition. The groups act like the GroupName on RadioButtons, with all elements that share a group name working together to determine a common result. In this case, group names are matched anywhere under a parent element that has IsSharedSizeScope set to true, which can even be our header Grid in this example. In addition to getting rid of the pixel width definitions in our template’s Grid, this also causes all of the Grid columns to update when the header Grid columns are resized using GridSplitters. Here’s the headers:

<Grid.ColumnDefinitions>  <ColumnDefinition Width="110" SharedSizeGroup="A"/>  <ColumnDefinition Width="90" SharedSizeGroup="B"/>  <ColumnDefinition Width="65" SharedSizeGroup="C"/>  <ColumnDefinition Width="Auto" SharedSizeGroup="D"/>  <ColumnDefinition Width="*" SharedSizeGroup="E"/>  </Grid.ColumnDefinitions>

And in the template:

<Grid.ColumnDefinitions>  <ColumnDefinition SharedSizeGroup="A"/>  <ColumnDefinition SharedSizeGroup="B"/>  <ColumnDefinition SharedSizeGroup="C"/>  <ColumnDefinition SharedSizeGroup="D"/>  <ColumnDefinition SharedSizeGroup="E"/>  </Grid.ColumnDefinitions>

We could just stop here and be happy with our resizable pixel-width columns, but what if we want to use one of the other GridLength sizing methods: Auto or Star? It turns out that Auto sizing works well with SharedSizeGroup because all it needs to do when determining initial column width is take the largest width of any of the “cells” in the group, which is probably the behavior you want most of the time. The resizing GridSplitters don’t add any more complication to this because once they start modifying an Auto column width, it switches to a Pixel value anyway.

Now on to the most useful sizing method in WPF dynamic layouts: Star sizing. Unfortunately, this is where the SharedSizeGroup method breaks down. If you want to have a shared Grid that uses the whole available width and automatically adjusts when that space changes you’re going to need a different method. A column set to * in a shared group acts just like an Auto column and won’t fill or stay within the given space. So what needs to happen to get back the Star sizing behavior?

Luckily ColumnDefinition.Width is a DependencyProperty, so it accepts Bindings. By binding the template’s columns to the header Grid’s we can get back Star sizing behavior and still keep them all in sync, even through resizes. The resizing behavior can look a little strange if you have multiple * columns since they will make whatever changes they need to to keep their size relative to each other the same while still filling the available space, and do this in real time as you drag. This isn’t a problem in itself, but may confuse your users, so consider only using a single * column in a resizable setup like this. The FindAncestor binding used is a long one, but stays the same for each definition with the exception of the collection index on each column. The header’s definitions:

<Grid.ColumnDefinitions>  <ColumnDefinition Width="2*"/>  <ColumnDefinition Width="Auto"/>  <ColumnDefinition Width="70"/>  <ColumnDefinition Width="65"/>  <ColumnDefinition Width="3*"/>  </Grid.ColumnDefinitions>

And the template’s:

<Grid.ColumnDefinitions>  <ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[0].Width}" />  <ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[1].Width}"/>  <ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[2].Width}"/>  <ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[3].Width}"/>  <ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[4].Width}" />  </Grid.ColumnDefinitions>

The Binding method is more of a dictatorship than the SharedSizeGroup’s democratic method of deciding sizing. Rather than taking the widest column value from the whole group, Binding just passes the header Grid’s definition (the source) down to the template’s Grids (the targets), in this case our smart * values.

Unfortunately we still have a problem with the current solution. Although the Binding passes its value exactly, that value can be misinterpreted by the bound columns. If you use the Binding method on a column using Auto sizing, the bound value will be Auto, rather than the calculated width of the header column. Each cell will then set its own size to Auto and determine its actual width based on its own content. This new problem resolves itself quickly if the Auto column is resized, because at that point the header reverts to a fixed Pixel size which will trickle down to all of the child columns. The thing that saves this Binding method is the fact that the problem only comes up with Auto sized columns, which are handled correctly by SharedSizeGroup, so using the two methods in combination allows the use of any GridLength sizing method and manual resizing. Here’s the final combined header:

<Grid.ColumnDefinitions>  <ColumnDefinition Width="2*"/>  <ColumnDefinition Width="Auto" SharedSizeGroup="B"/>  <ColumnDefinition Width="Auto" SharedSizeGroup="C"/>  <ColumnDefinition Width="Auto" SharedSizeGroup="D"/>  <ColumnDefinition Width="3*"/>  </Grid.ColumnDefinitions>

And the template:

<Grid.ColumnDefinitions>  <ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[0].Width}" />  <ColumnDefinition SharedSizeGroup="B"/>  <ColumnDefinition SharedSizeGroup="C"/>  <ColumnDefinition SharedSizeGroup="D"/>  <ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}, AncestorLevel=2}, Path=ColumnDefinitions[4].Width}" />  </Grid.ColumnDefinitions>

Download sample code demonstrating all these methods here.

Leave a Reply

Your email address will not be published. Required fields are marked *