Click here to Skip to main content
Click here to Skip to main content

Tidy Up XAML with the ApexGrid

, 31 Jul 2011 CPOL
Rate this:
Please Sign up or sign in to vote.
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Dave Kerr
Software Developer
United Kingdom United Kingdom
Follow my blog at www.dwmkerr.com and find out about my charity at www.childrenshomesnepal.org.
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 PinmemberSuper Lloyd23-May-13 1:48 
GeneralRe: My vote of 5 PinmvpDave Kerr23-May-13 6:08 
QuestionTypeConverter! PinmemberJacek Gajek10-Aug-11 2:27 
GeneralMy vote of 5 Pinmemberisaks7-Aug-11 22:03 
GeneralMy vote of 5 PinmemberCharles T II6-Aug-11 15:51 
QuestionSimplicity itself PinmemberGreg Russell5-Aug-11 1:14 
QuestionTypeConverter... PinmemberPaulo Zemek3-Aug-11 5:59 
GeneralMy vote of 5 PinmemberUbloobok2-Aug-11 21:10 
GeneralMy vote of 5 PinmemberMonjurul Habib2-Aug-11 9:17 
GeneralBrilliant, just brilliant! PinmvpMarcelo Ricardo de Oliveira1-Aug-11 10:21 
GeneralRe: Brilliant, just brilliant! PinmemberDaveKerr1-Aug-11 12:01 
GeneralMy vote of 5 PinmemberJohn Schroedl1-Aug-11 7:58 
QuestionGreat idea PinmvpSacha Barber1-Aug-11 2:21 
QuestionMy vote of 5... PinmemberPaul Conrad31-Jul-11 13:40 
GeneralI like it! PinmemberAdrian Cole31-Jul-11 13:22 
QuestionGreat Stuff! PinmemberDewey31-Jul-11 12:01 
AnswerRe: Great Stuff! PinmemberDaveKerr31-Jul-11 13:08 
QuestionExcellent PinprotectorPete O'Hanlon31-Jul-11 7:53 
AnswerRe: Excellent PinmemberDaveKerr31-Jul-11 8:14 
GeneralRe: Excellent PinprotectorPete O'Hanlon31-Jul-11 8:40 
GeneralRe: Excellent PinmemberDaveKerr31-Jul-11 8:53 
GeneralRe: Excellent PinprotectorPete O'Hanlon31-Jul-11 8:59 
GeneralRe: Excellent PinprotectorPete O'Hanlon31-Jul-11 11:17 
GeneralRe: Excellent PinmemberLouis T Klauder Jr2-Aug-11 5:57 
AnswerRe: Excellent PinmemberRichard Deeming2-Aug-11 6:56 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141220.1 | Last Updated 31 Jul 2011
Article Copyright 2011 by Dave Kerr
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid