Creating a table-like user interface

Table layout is a popular placement strategy, supported by the Grid panel. Let's examine the Grid and see what it's capable of.

Getting ready

Make sure Visual Studio is up and running.

How to do it...

We'll create a simple UI that benefits from a grid-like layout and demonstrate some of its features:

  1. Create a new WPF application named CH03.GridDemo.
  2. Open MainWindow.xaml. There's already a Grid placed inside the Window. That's because the Grid is typically used as the main layout panel within a window.
  3. Change the Title of Window to Grid Demo.
  4. Inside the Grid, add the following markup to create some rows and columns:
     <Grid.RowDefinitions>
         <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
         <RowDefinition Height="Auto" />
         <RowDefinition />
     </Grid.RowDefinitions>
     <Grid.ColumnDefinitions>
         <ColumnDefinition Width="Auto" />
         <ColumnDefinition />
     </Grid.ColumnDefinitions>
  5. This creates a 4 rows by 2 columns Grid. Now let's add some elements and controls to host the grid:
    <TextBlock Grid.ColumnSpan="2" HorizontalAlignment="Center" 
                Text="Book Details" FontSize="20" Margin="4"/>
    <TextBlock Grid.Row="1" HorizontalAlignment="Right" 
                Text="Name:" Margin="4" />
    <TextBlock Grid.Row="2" HorizontalAlignment="Right" 
                Text="Author:" Margin="4" />
    <TextBlock Grid.Row="1" Grid.Column="1" 
                Text="Windows internals" Margin="4" />
    <TextBlock Grid.Row="2" Grid.Column="1" 
                Text="Mark Russinovich" Margin="4" />
    <Rectangle Grid.Column="1" Grid.Row="3" Margin="4" 
             StrokeThickness="4" Stroke="Black" Fill="Red" />
    <TextBlock Grid.Column="1" Grid.Row="3" 
                Text="Book Cover" VerticalAlignment="Center"   
                FontSize="16"  HorizontalAlignment="Center"/>
  6. Run the application. It should look as follows:
    How to do it...
  7. Resize the window and watch the layout changes. Note that the Grid rows marked with a Height of Auto remain fixed in size, while the row that has no Height setting fills the remaining space (it is the same idea for the columns):
    How to do it...

How it works...

The Grid panel creates a table-like layout of cells. The number of rows and columns is not specified by simple properties. Instead, it's specified using RowDefinition objects (for rows) and ColumnDefinition objects (for columns). The reason has to do with the size and behavior that can be specified on a row and/or column basis.

A RowDefinition has a Height property, while a ColumnDefintion has a Width property. Both are of type GridLength. There are three options for setting a GridLength:

  • A specific length
  • A "star" (relative) based factor (this is the default, and factor equals 1)
  • Automatic length

Setting Height (of a RowDefintion) or Width (of a ColumnDefinition) to a specific number makes that row/column that particular size. In code it's equivalent to new GridLength(len).

Setting Height or Width to Auto (in XAML) makes the row/column as high/wide as it needs to be based on the tallest/widest element placed within that row/column. In code, it's equivalent to GridLength.Auto.

The last option (which is the default) is setting Height/Width to n* in XAML, where n is some number (1 if omitted). This sets up a relationship with other rows/columns that have a "star" length. For example, here are three rows of a Grid:

<RowDefinition Height="2*" />
<RowDefinition />
<RowDefinition Height="3*" />

This means that the first row is twice as tall as the second row (Height="*"). The last row is three times taller than the second row and is one and a half times taller than the first row. These relations are maintained even if the window is resized.

There's more...

Elements are placed in grid cells using the attached Grid.Row and Grid.Column properties (both default to zero, meaning the first row/column).

Elements occupy one cell by default. This can be changed by using the Grid.RowSpan and Grid.ColumnSpan properties (these were set for the first TextBlock in the code). If we need an element to stretch to the end of the Grid (for example, through all columns from some starting column), it's ok to specify some large number (it will be constrained to the actual number of rows/columns of the Grid).

Shared row/column size

There may be times when more than one grid should have the same row height (or column width). This is possible using the SharedSizeGroup property of DefinitionBase (the base class of RowDefinition and ColumnDefinition). This is just a string; if RowDefinition or ColumnDefinition objects from two separate grids have the same value for this property, they would maintain identical length. To make this work, the attached property Grid.IsSharedSizeScope must be set to true on a common parent of those Grid instances.

This feature is most useful with DataTemplate properties (which are discussed in detail in Chapter 6, Data Binding) when binding to an ItemsControl control (or one of its derivatives).

Here's a quick example: A ListBox defines a DataTemplate for its items (ItemTemplate property) for displaying information on Person objects, defined as follows:

class Person {
   public string Name { get; set; }
   public int Age { get; set; }
}

The ListBox is defined with the following markup:

<ListBox ItemsSource="{Binding}" 
    Grid.IsSharedSizeScope="True">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" 
                        SharedSizeGroup="abc" />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <TextBlock Text="{Binding Name}" FontSize="20"
                           Margin="4"/>
                <TextBlock Grid.Column="1" FontSize="16"
          Text="{Binding Age, StringFormat=is {0} years old}"  
                    VerticalAlignment="Bottom" Margin="4"/>
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Note the attached property Grid.IsSharedSizeScope is set to true on a common parent of all involved Grid instances (the ListBox is an obvious choice) and the SharedSizeGroup property of RowDefinition is set to some string ("abc"). The string itself does not matter – what matters is that it's the same string for all relevant Grid instances. Since this is part of a DataTemplate, Grid instances are created for every item in the ListBox. Here's the result when binding to some collection of Person objects:

Shared row/column size

Here's what happens if one of the above properties is missing (note the misalignments):

Shared row/column size

The complete project is named CH03.SharedGridSizeDemo and is available in the downloadable source for this chapter.

Placement in the same cell

If more than one element is placed in the same cell, they sit one on top of the other – by default, elements appearing later in the XAML are on top. We can change that by using the attached Panel.ZIndex property (default is zero). Higher values make the element on top (negative values are accepted).

The power of the Grid

The Grid is so flexible, that it can emulate most of the standard WPF panels, all except the WrapPanel (which is too chaotic for the Grid), namely the StackPanel, Canvas, and DockPanel. This is why Visual Studio chooses the Grid as the default root layout panel when a new window is generated.

If the Grid can do almost anything, why use other panels at all? There are two reasons. The first is convenience: although a Grid can emulate a StackPanel, that's cumbersome and leads to increased markup with no real gains. The second reason is performance: the StackPanel (for instance) has very little to worry about, while the Grid has a lot. For a layout consisting of many elements the complexity of Grid may degrade performance, so it's usually best to use the lightest panel possible that gets the job done.

Adding rows/columns dynamically

Although Grid instances are typically constructed in XAML and have a fixed number of columns and rows, that doesn't have to be the case. It's possible to add (or remove) rows/columns at runtime if so desired. Here's an example that adds three rows to an existing Grid named _grid:

RowDefinition[] rows = {
   new RowDefinition { Height = new GridLength(100) },
   new RowDefinition { Height = GridLength.Auto },
   new RowDefinition { Height = new GridLength(2, 
      GridUnitType.Star) }
};
Array.ForEach(rows, row => _grid.RowDefinitions.Add(row));

The first row has a fixed height of 100, the second is auto-sized, and the third is a star row with a factor of 2.

The UniformGrid

There is a simpler grid in WPF, the UniformGrid (in the System.Windows.Controls.Primitives namespace). This grid has two properties to set the grid size: Rows and Columns (both default to 1). Every cell in a UniformGrid is, well, uniform (same width and height). Every new element is placed in the next cell starting from the top left, moving left to right, and then down to the next row, starting from the left again.

The UniformGrid may be used as a simple shortcut for a full-blown grid (it's more lightweight), if the need arises. A more creative use of the UniformGrid is a custom panel hosting elements in an ItemsControl.

Here's a quick example: A ListBox holds a collection of numbers, displayed by default in a row layout (a VirtualizingStackPanel, similar to a StackPanel for our purposes, is the default panel for laying out items in an ListBox). Let's change that to a UniformGrid. Here's the complete ListBox markup:

<ListBox ItemsSource="{Binding}" FontSize="25" 
    SelectionMode="Multiple">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid Rows="4" Columns="4" />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

Here's the ListBox at runtime when bound to a list of numbers:

The UniformGrid

Despite appearances, this is still a regular ListBox! The complete source is in the CH03.UniformGridLayout project available in the downloadable source for this chapter.