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:
<!---->
<Grid Rows="2*,Auto,*,66" Columns="2*,*,Auto">
<!---->
</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
{
public class ApexGrid : Grid
{
}
We're going to want to add two new properties to the Grid - Rows
and Columns
. These properties will be string
s 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:
private static readonly DependencyProperty rowsProperty =
DependencyProperty.Register("Rows", typeof(string), typeof(ApexGrid));
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.
public string Rows
{
get { return (string)GetValue(rowsProperty); }
set { SetValue(rowsProperty, 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):
private static readonly DependencyProperty rowsProperty =
DependencyProperty.Register("Rows", typeof(string), typeof(ApexGrid),
new PropertyMetadata(null, new PropertyChangedCallback(OnRowsChanged)));
private static readonly DependencyProperty columnsProperty =
DependencyProperty.Register("Columns", typeof(string), typeof(ApexGrid),
new PropertyMetadata(null, new PropertyChangedCallback(OnColumnsChanged)));
And add the 'OnChanged
' functions below:
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
'.
ApexGrid apexGrid = dependencyObject as ApexGrid;
apexGrid.RowDefinitions.Clear();
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
.
ApexGrid apexGrid = dependencyObject as ApexGrid;
apexGrid.ColumnDefinitions.Clear();
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.
private static List<GridLength> StringLengthsToGridLengths(string lengths)
{
List<GridLength> gridLengths = new List<GridLength>();
if (string.IsNullOrEmpty(lengths))
return gridLengths;
string[] theLengths = lengths.Split(',');
We create the list of GridLength
s 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 string
s, separated by the comma character.
#if !SILVERLIGHT
GridLengthConverter gridLengthConverter = new GridLengthConverter();
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
foreach(var length in theLengths)
{
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("*"))
{
double coefficient = 1;
string starVal = length.Replace("*", "");
if (starVal.Length > 0 && double.TryParse(starVal, out coefficient) == false)
throw new Exception("'" + length + "' is not a valid 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
{
double pixelVal = 0;
if(double.TryParse(length, out pixelVal) == false)
throw new Exception("'" + length + "' is not a valid value.");
gridLengths.Add(new GridLength(pixelVal, GridUnitType.Pixel));
}
}
#endif
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">
-->
<a:ApexGrid Rows="2*,Auto,*,66" Columns="2*,*,Auto">
-->
</a:ApexGrid>
</Page>
The ApexGrid
works in exactly the same way regardless of whether you are using WPF, Silverlight or WP7:
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.