Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Tidy Up XAML with the ApexGrid

0.00/5 (No votes)
31 Jul 2011 1  
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:

 lt;!-- Too verbose! -->

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:

<!-- 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):

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:

/// <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.

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

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

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

/// <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'.

//  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.

//  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.

/// <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.

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

 #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.

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.

        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:

<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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here