Click here to Skip to main content
15,879,535 members
Articles / Mobile Apps / Windows Phone 7

Tidy Up XAML with the ApexGrid

Rate me:
Please Sign up or sign in to vote.
4.82/5 (30 votes)
31 Jul 2011CPOL5 min read 57.4K   1.1K   49   26
A small and neat addition to the Grid control which can tidy up XAML in WPF, Silverlight and WP7
Splashscreen.png

Introduction

The Grid is one of the most crucial parts of any WPF, Silverlight or WP7 developer's toolkit. However, the XAML required to create the row and column definitions is a little too verbose - particularly if you use grids everywhere, in DataTemplates, ControlTemplates, List Items and so on.

In this article, I will show you how to extend the standard Grid class to have two new properties - Rows and Columns that'll let us define rows and columns inline.

How This Ties in with Apex

This is one of the many controls in my Apex library. I'm uploading them one by one. However, you don't need the Apex library or ANY of the other files to use this class - you can just add it straight to your project.

Apex works for WPF, Silverlight and WP7. I'll show you step-by-step in this article how to make this class work for each platform.

The Problem

Defining even a fairly simple grid is fairly verbose:

XML
 lt;!-- Too verbose! -->
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="2*" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
        <RowDefinition Height="66" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="2*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <!-- Grid content goes here. -->
</Grid> 

We may only have a few controls in the actual grid, but we've got eleven lines just to define the rows and columns. Wouldn't it be nice if we could do this:

XML
<!-- Tidier and cleaner. -->
<Grid Rows="2*,Auto,*,66" Columns="2*,*,Auto">
    <!-- Grid content goes here. -->
</Grid> 

Well we can - although the end result won't be a grid, rather a class defined from it. (You can actually extend the existing Grid to do this by using Attachable Properties.)

The Solution

Add a new class to your WPF, Silverlight or WP7 project. We don't need to use the User Control template because we'll derive from an existing class. We don't need to use the Custom Control template because we don't need to define any XAML for this class. Let's get started - derive the class from Grid (I'm using the namespace and class name as in the Apex project, if you're using this as a baseline for your own class, then obviously name it as you see fit):

C#
using System.Windows.Controls;
using System.Windows;
using System.ComponentModel;
using System.Collections.Generic;
using System;
namespace Apex.Controls
{
  /// <summary>
  /// The ApexGrid control is a Grid that supports easy definition of rows and columns.
  /// </summary>
  public class ApexGrid : Grid
  {
  } 

We're going to want to add two new properties to the Grid - Rows and Columns. These properties will be strings that can be used to set the row and column definition. We need to add these properties not as standard properties but as Dependency Properties, so that we can perform bindings and so on, just like with the other properties of the Grid. Add the two dependency properties to the class and wire them in by using the DependencyProperty.Register function:

C#
/// <summary>
/// The rows dependency property.
/// </summary>
private static readonly DependencyProperty rowsProperty =
    DependencyProperty.Register("Rows", typeof(string), typeof(ApexGrid));
    
/// <summary>
/// The columns dependency property.
/// </summary>
private static readonly DependencyProperty columnsProperty =
    DependencyProperty.Register("Columns", typeof(string), typeof(ApexGrid)); 

One thing that's critical is that we also provide the standard CLR properties that return the value of these dependency properties.

C#
/// <summary>
/// Gets or sets the rows.
/// </summary>
/// <value>The rows.</value>
public string Rows
{
  get { return (string)GetValue(rowsProperty); }
  set { SetValue(rowsProperty, value); }
}
/// <summary>
/// Gets or sets the columns.
/// </summary>
/// <value>The columns.</value>
public string Columns
{
  get { return (string)GetValue(columnsProperty); }
  set { SetValue(columnsProperty, value); }
} 

We've got the properties now - but they don't do anything. What we need is for setting the property to create the appropriate set of grid or column definitions. This is where we have to be careful - look at the code below:

C#
public string Columns
{
    get { return (string)GetValue(columnsProperty); }
    set
    {
        SetValue(columnsProperty, value);
        BuildTheColumns();
    }
} 

This is not going to work - and this is very important to know about dependency properties. Unlike standard properties, these properties aren't always used. The Framework can call SetValue on the static readonly dependency property instance in the class - skipping the property accessor completely! In a nutshell, never do anything in a dependency property accessor other than the standard GetValue/SetValue - it just leads to trouble.

So how do we know when the property is changed? Well we can pass a PropertyChangedCallback delegate to the Register function of the DependencyProperty. This will allow us to specify a function that is called whenever the property changes.

Change the dependency property definitions as below (in bold):

C#
/// <summary>
/// The rows dependency property.
/// </summary>
private static readonly DependencyProperty rowsProperty =
    DependencyProperty.Register("Rows", typeof(string), typeof(ApexGrid),
    new PropertyMetadata(null, new PropertyChangedCallback(OnRowsChanged)));
    
/// <summary>
/// The columns dependency property.
/// </summary>
private static readonly DependencyProperty columnsProperty =
    DependencyProperty.Register("Columns", typeof(string), typeof(ApexGrid),
    new PropertyMetadata(null, new PropertyChangedCallback(OnColumnsChanged))); 

And add the 'OnChanged' functions below:

C#
/// <summary>
/// Called when the rows property is changed.
/// </summary>
/// <param name="dependencyObject">The dependency object.</param>
/// <param name="args">The <see cref="
System.Windows.DependencyPropertyChangedEventArgs"/> 
instance containing the event data.</param>
private static void OnRowsChanged(DependencyObject dependencyObject, 
DependencyPropertyChangedEventArgs args)
{
}

/// <summary>
/// Called when the columns property is changed.
/// </summary>
/// <param name="dependencyObject">The dependency object.</param>
/// <param name="args">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> 
instance containing the event data.</param>
private static void OnColumnsChanged(DependencyObject dependencyObject, 
DependencyPropertyChangedEventArgs args)
{
} 

We now have an entry point for actually providing the real functionality of this class. Add the following to 'OnRowsChanged'.

C#
//  Get the apex grid.
ApexGrid apexGrid = dependencyObject as ApexGrid;

//  Clear any current rows definitions.
apexGrid.RowDefinitions.Clear();

//  Add each row from the row lengths definition.
foreach (var rowLength in StringLengthsToGridLengths(apexGrid.Rows))
    apexGrid.RowDefinitions.Add(new RowDefinition() { Height = rowLength }); 

This is all we need - it's very simple. Get the grid (passed as the first parameter to the function). Then clear all of the rows. Then call our hypothetical StringLengthsToGridLengths function - which given a string should return an enumerable collection of GridLength objects. It's then a simple case of adding a RowDefinition of the specified height to the set of row definitions.

Finish off the OnColumnsChanged function by adding the below - then we'll get onto the final part, StringLengthsToGridLengths.

C#
//  Get the apex grid.
ApexGrid apexGrid = dependencyObject as ApexGrid;

//  Clear any current column definitions.
apexGrid.ColumnDefinitions.Clear();

//  Add each column from the column lengths definition.
foreach (var columnLength in StringLengthsToGridLengths(apexGrid.Columns))
    apexGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = columnLength }); 

There is only one thing left to do - actually write the StringLengthsToGridLengths function. I'll take you through it blow-by-blow.

C#
/// <summary>
/// Turns a string of lengths, such as "3*,Auto,2000" into a set of gridlength.
/// </summary>
/// <param name="lengths">The string of lengths, separated by commas.</param>
/// <returns>A list of GridLengths.</returns>
private static List<GridLength> StringLengthsToGridLengths(string lengths)
{
    //  Create the list of GridLengths.
    List<GridLength> gridLengths = new List<GridLength>();
    
    //  If the string is null or empty, this is all we can do.
    if (string.IsNullOrEmpty(lengths))
        return gridLengths;
        
    //  Split the string by comma. 
    string[] theLengths = lengths.Split(','); 

We create the list of GridLengths that we will eventually return. If the string is null or empty, return the empty list. This'll happen quite often - imagine you are in the XAML editor replacing "3*,2*" with "4*,3*" - we'd delete each character and then retype - so at some point, an empty string will be passed to the function. Calling 'Split' will break the string into an array of strings, separated by the comma character.

C#
//  If we're NOT in silverlight, we have a gridlength converter
//  we can use.
#if !SILVERLIGHT

//  Create a grid length converter.
GridLengthConverter gridLengthConverter = new GridLengthConverter();

//  Use the grid length converter to set each length.
foreach (var length in theLengths) 
    gridLengths.Add((GridLength)gridLengthConverter.ConvertFromString(length)); 

If we're in a WPF project, then it's really easy - the GridLengthConverter class will allow us to turn each string into a GridLength. However, this class must also work in Silverlight - which doesn't have a GridLengthConverter (and therefore nor does WP7!) so we must do it slightly differently:

C#
#else
     //  We are in silverlight and do not have a grid length converter.
     //  We can do the conversion by hand.
     foreach(var length in theLengths)
     {
       //  Auto is easy.
       if(length == "Auto")
       {
         gridLengths.Add(new GridLength(1, GridUnitType.Auto));
       }

If the string is simply 'Auto', we've got the fairly trivial case above.

C#
else if (length.Contains("*"))
{
  //  It's a starred value, remove the star and get the coefficient as a double.
  double coefficient = 1;
  string starVal = length.Replace("*", "");
  
  //  If there is a coefficient, try and convert it.
  //  If we fail, throw an exception.
  if (starVal.Length > 0 && double.TryParse(starVal, out coefficient) == false)
    throw new Exception("'" + length + "' is not a valid value."); 
    
  //  We've handled the star value.
  gridLengths.Add(new GridLength(coefficient, GridUnitType.Star));
} 

If the string contains a star, we can assume it is a starred value. We try and get the number before the star (if there is one) and then add the appropriate GridLength to the gridLengths list.

C#
        else
        {
          //  It's not auto or star, so unless it's a plain old pixel 
          //  value we must throw an exception.
          double pixelVal = 0;
          if(double.TryParse(length, out pixelVal) == false)
            throw new Exception("'" + length + "' is not a valid value.");
          
          //  We've handled the star value.
          gridLengths.Add(new GridLength(pixelVal, GridUnitType.Pixel));
        }
      }
#endif

            //  Return the grid lengths.
            return gridLengths;
        } 

If we don't have a star or Auto, then we've just got a number of pixels. Try and convert it and add it to the list if we do so successfully.

That's it! The whole thing now works - here's an example for Silverlight:

XML
<Page x:Class="Apex.Page1"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:a="clr-namespace:Apex.Controls"
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"
      Title="Page1">
    
        <!-- Tidier and cleaner. -->
        <a:ApexGrid Rows="2*,Auto,*,66" Columns="2*,*,Auto">
            <!-- Grid content goes here. -->
        </a:ApexGrid>
</Page> 

The ApexGrid works in exactly the same way regardless of whether you are using WPF, Silverlight or WP7:

Samples.png

Enjoy!

Check back for updates and keep an eye on the Introducing Apex article - I'll be uploading more code for WPF, Silverlight and WP7 over the next few weeks and will keep an index at the top of the Introducing Apex article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United Kingdom United Kingdom
Follow my blog at www.dwmkerr.com and find out about my charity at www.childrenshomesnepal.org.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Super Lloyd23-May-13 0:48
Super Lloyd23-May-13 0:48 
GeneralRe: My vote of 5 Pin
Dave Kerr23-May-13 5:08
mentorDave Kerr23-May-13 5:08 
QuestionTypeConverter! Pin
Lutosław10-Aug-11 1:27
Lutosław10-Aug-11 1:27 
GeneralMy vote of 5 Pin
isaks7-Aug-11 21:03
isaks7-Aug-11 21:03 
GeneralMy vote of 5 Pin
Charles T II6-Aug-11 14:51
Charles T II6-Aug-11 14:51 
QuestionSimplicity itself Pin
Greg Russell5-Aug-11 0:14
professionalGreg Russell5-Aug-11 0:14 
So why the bloody hell didn't Microsoft do this?Confused | :confused:
QuestionTypeConverter... Pin
Paulo Zemek3-Aug-11 4:59
mvaPaulo Zemek3-Aug-11 4:59 
GeneralMy vote of 5 Pin
Ubloobok2-Aug-11 20:10
Ubloobok2-Aug-11 20:10 
GeneralMy vote of 5 Pin
Monjurul Habib2-Aug-11 8:17
professionalMonjurul Habib2-Aug-11 8:17 
GeneralBrilliant, just brilliant! Pin
Marcelo Ricardo de Oliveira1-Aug-11 9:21
Marcelo Ricardo de Oliveira1-Aug-11 9:21 
GeneralRe: Brilliant, just brilliant! Pin
Dave Kerr1-Aug-11 11:01
mentorDave Kerr1-Aug-11 11:01 
GeneralMy vote of 5 Pin
John Schroedl1-Aug-11 6:58
professionalJohn Schroedl1-Aug-11 6:58 
QuestionGreat idea Pin
Sacha Barber1-Aug-11 1:21
Sacha Barber1-Aug-11 1:21 
QuestionMy vote of 5... Pin
Paul Conrad31-Jul-11 12:40
professionalPaul Conrad31-Jul-11 12:40 
GeneralI like it! Pin
Adrian Cole31-Jul-11 12:22
Adrian Cole31-Jul-11 12:22 
QuestionGreat Stuff! Pin
Dewey31-Jul-11 11:01
Dewey31-Jul-11 11:01 
AnswerRe: Great Stuff! Pin
Dave Kerr31-Jul-11 12:08
mentorDave Kerr31-Jul-11 12:08 
QuestionExcellent Pin
Pete O'Hanlon31-Jul-11 6:53
mvePete O'Hanlon31-Jul-11 6:53 
AnswerRe: Excellent Pin
Dave Kerr31-Jul-11 7:14
mentorDave Kerr31-Jul-11 7:14 
GeneralRe: Excellent Pin
Pete O'Hanlon31-Jul-11 7:40
mvePete O'Hanlon31-Jul-11 7:40 
GeneralRe: Excellent Pin
Dave Kerr31-Jul-11 7:53
mentorDave Kerr31-Jul-11 7:53 
GeneralRe: Excellent Pin
Pete O'Hanlon31-Jul-11 7:59
mvePete O'Hanlon31-Jul-11 7:59 
GeneralRe: Excellent Pin
Pete O'Hanlon31-Jul-11 10:17
mvePete O'Hanlon31-Jul-11 10:17 
GeneralRe: Excellent Pin
Louis T Klauder Jr2-Aug-11 4:57
professionalLouis T Klauder Jr2-Aug-11 4:57 
AnswerRe: Excellent Pin
Richard Deeming2-Aug-11 5:56
mveRichard Deeming2-Aug-11 5:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.