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

WPF Stylizing RowDefinitions and ColumnDefinitions

0.00/5 (No votes)
12 Dec 2012 1  
Enhance WPF skinnability and resource definitions using attached properties and style setters

Introduction

My favorite part of programming in WPF is the ease with which we compartmentalize the application components. Every Page or UserControl of course has a markup and a code-behind file, but if you are serious about architectural patterns like I am, then you also have a ViewModel and probably a related resource file as well.

I prefer to keep my markup files clean, defining visual attributes in a separate resource file. In doing so, adding or removing elements later, or altering visual properties, is done more quickly and with more flexibility...all while providing the serenity that comes with S.O.C. (Separation of Concerns).

Whether your styles are defined in the local file markup or a merged resource dictionary, there are some inherent limitations to stylization that can quite easily be overcome. Take for instance RowDefinitions and ColumnDefinitions...you can be diligent about parsing out the visual attributes of your UserControl, but ultimately you are forced to define these in your markup file, and unless you modify it in managed code at runtime, these values are inevitably static. Not very friendly to a skinnable interface, is it?

Background

When I decided that this was a problem that I wanted to resolve, I knew that it had to be something simple. My first thought was a custom Behavior, but I quickly got away from that idea. Instead, I went to one of the most underrated elements in WPF - the Attached Property.

Attached Property is a special type of Dependency Property that can be assigned to any Dependency Object. Since a Grid just happens to be a Dependency Object, the solution was not only sufficient, it was elegant. I use Attached Properties daily, and for a wide variety of purposes. I rarely build an app without them!

Using the code

Attached Properties can be defined within any class in your application. For demonstration purposes, this article will assume that you placed the declaration in the Application class.

public static DependencyProperty GridRowsProperty = 
  DependencyProperty.RegisterAttached("GridRows", typeof(string), 
  MethodBase.GetCurrentMethod().DeclaringType, new FrameworkPropertyMetadata(string.Empty, 
  FrameworkPropertyMetadataOptions.AffectsArrange, new PropertyChangedCallback(GridRowsPropertyChanged)));
Public Shared GridRowsProperty As DependencyProperty = 
  DependencyProperty.RegisterAttached("GridRows", GetType(String), 
  MethodBase.GetCurrentMethod().DeclaringType, New FrameworkPropertyMetadata(String.Empty, 
  FrameworkPropertyMetadataOptions.AffectsArrange, New PropertyChangedCallback(AddressOf GridRowsPropertyChanged)))

We declare the Attached Property as a String type, so that in markup we can enter a comma separated list of Column/Row definitions.

Note: Because I use an extensive library of code snippets, I use Reflection to set the ownerType in my Dependency Property declarations. It is perfectly fine to replace MethodBase.GetCurrentMethod().DeclaringType with GetType(Application) (Or whatever class you are declaring the Attached property within).

In order for an Attached Property to function, we must define the Get and Set Accessors. If you are new to the Dependency Property system, it is important to note that naming convention is very specific.

public static string GetGridRows(Grid This)
{
    return Convert.ToString(This.GetValue(GridRowsProperty));
}

public static void SetGridRows(Grid This, string Value)
{
    This.SetValue(GridRowsProperty, Value);
}
Public Shared Function GetGridRows(ByVal This As Grid) As String    
    Return CType(This.GetValue(GridRowsProperty), String)
End Function

Public Shared Sub SetGridRows(ByVal This As Grid, ByVal Value As String)
    This.SetValue(GridRowsProperty, Value)
End Sub

In the GridRowsProperty declaration, we assigned the PropertyChangedCallback method GridRowsPropertyChanged. This method will be called when the value of the GridRowsProperty is initialized or modified.

Private Shared Sub GridRowsPropertyChanged(ByVal Sender As Object, ByVal e As DependencyPropertyChangedEventArgs)
    Dim This = TryCast(Sender, Grid)
    If This Is Nothing Then Throw New Exception("Only elements of type 'Grid' can utilize the 'GridRows' attached property")
    DefineGridRows(This)
End Sub
private static void GridRowsPropertyChanged(object Sender, DependencyPropertyChangedEventArgs e)
{
    object This = Sender as Grid;
    if (This == null)
        throw new Exception("Only elements of type 'Grid' can utilize the 'GridRows' attached property");
    DefineGridRows(This);
}

In the callback method, we are simply checking that the property was attached to a Grid element, and then calling the method that modifies the RowDefinitions.

private static void DefineGridRows(Grid This)
{
    object Rows = GetGridRows(This).Split(Convert.ToChar(","));
    This.RowDefinitions.Clear();
    foreach ( Row in Rows) {
        switch (Row.Trim.ToLower) {
            case "auto":
                This.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
                break;
            case "*":
                This.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
                break;
            default:
                if (System.Text.RegularExpressions.Regex.IsMatch(Row, "^\\d+\\*$")) {
                    This.RowDefinitions.Add(new RowDefinition { Height = new GridLength(
                      Convert.ToInt32(Row.Substring(0, Row.IndexOf(Convert.ToChar("*")))), GridUnitType.Star) });
                } else if (Information.IsNumeric(Row)) {
                    This.RowDefinitions.Add(new RowDefinition { Height = new GridLength(Convert.ToDouble(Row), GridUnitType.Pixel) });
                } else {
                    throw new Exception("The only acceptable value for the 'GridRows' " + 
                      "attached property is a comma separated list comprised of the following options:" + 
                      Constants.vbCrLf + Constants.vbCrLf + "Auto,*,x (where x is the pixel " + 
                      "height of the row), x* (where x is the row height multiplier)");
                }
                break;
        }
    }
}
Private Shared Sub DefineGridRows(ByVal This As Grid)
    Dim Rows = GetGridRows(This).Split(CChar(","))
    This.RowDefinitions.Clear()
    For Each Row In Rows
        Select Case Row.Trim.ToLower
            Case "auto"
                This.RowDefinitions.Add(New RowDefinition With {.Height = New GridLength(1, GridUnitType.Auto)})
            Case "*"
                This.RowDefinitions.Add(New RowDefinition With {.Height = New GridLength(1, GridUnitType.Star)})
            Case Else
                If System.Text.RegularExpressions.Regex.IsMatch(Row, "^\d+\*$") Then
                    This.RowDefinitions.Add(New RowDefinition With {.Height = New _
                      GridLength(CInt(Row.Substring(0, Row.IndexOf(CChar("*")))), GridUnitType.Star)})
                ElseIf IsNumeric(Row) Then
                    This.RowDefinitions.Add(New RowDefinition With {.Height = New GridLength(CDbl(Row), GridUnitType.Pixel)})
                Else
                    Throw New Exception("The only acceptable value for the 'GridRows' " & _ 
                       "attached property is a comma separated list comprised of the following options:" & _
                       vbCrLf & vbCrLf & "Auto,*,x (where x is the pixel " & _ 
                       "height of the row), x* (where x is the row height multiplier)")
                End If
        End Select
    Next
End Sub

The DefineGridRows method first calls the Get Accessor for the GridRowsProperty, which returns the string that represents a comma separated list of RowDefinition values.

Next we clear any existing RowDefinitions from the RowDefinitionCollection.

Using a String type allows us to define an indefinite number of Rows/Columns in XAML or through databinding, but it means we have to parse the string data to determine what our RowDefinitions will actually be.

Before we evaluate the string value, we call the String functions Trim() and ToLower(). This adds to the flexibility of the XAML definition (or Binding) because we need not worry about case sensitivity or white space.

Our attached property will support the 4 (I know it's literally 3, but since there's really 4 implementations it makes more sense to call it so) types of Row/Column definitions, explained in detail here. You can see above that we iterate through the supplied values and append a new RowDefinition according to the value.

Note that if there is a parsing error, we are throwing a descriptive error message for clarity during the debug process.

That's it for the code! Now all that's left is to mark it up. Make sure that you reference your assembly in your directive:

<UserControl x:class="MyUserControl" mc:ignorable="d"
    xmlns:local="clr-namespace:MyApplication"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"/>

Now you can define the RowDefinitions in XAML like this:

<Grid local:Application.GridRows="Auto,50,*,Auto" />

Which is the equivalent of:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="50"/>
        <RowDefinition/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
</Grid>

Or more importantly, you can define the RowDefinitions from within a Style defined anywhere in your application as a resource:

<Style x:Key="StylizedRowDefinitions" TargetType="Grid">
    <Setter Property="local:Application.GridRows" Value="3*,5*,Auto"/>
</Style>
<Grid Style="{DynamicResource StylizedRowDefinitions}">
</Grid>

Now we will implement the GridColumnsProperty, and we can see all the code together:

Public Shared GridColumnsProperty As DependencyProperty = 
  DependencyProperty.RegisterAttached("GridColumns", GetType(String), 
  MethodBase.GetCurrentMethod().DeclaringType, New FrameworkPropertyMetadata(String.Empty, 
  FrameworkPropertyMetadataOptions.AffectsArrange, New PropertyChangedCallback(AddressOf GridColumnsPropertyChanged)))

Private Shared Sub GridColumnsPropertyChanged(ByVal Sender As Object, ByVal e As DependencyPropertyChangedEventArgs)
    RaiseEvent _GridColumnsChanged(Sender, e)
    Dim This = TryCast(Sender, Grid)
    If This Is Nothing Then
    Throw New Exception("Only elements of type 'Grid' " + 
      "can utilize the 'GridColumns' attached property")
    DefineGridColumns(This)
End Sub

Public Shared Function GetGridColumns(ByVal This As Grid) As String
    Return CType(This.GetValue(GridColumnsProperty), String)
End Function

Public Shared Sub SetGridColumns(ByVal This As Grid, ByVal Value As String)
    This.SetValue(GridColumnsProperty, Value)
End Sub

Private Shared Sub DefineGridColumns(ByVal This As Grid)
    Dim Columns = GetGridColumns(This).Split(CChar(","))
    This.ColumnDefinitions.Clear()
    For Each Column In Columns
        Select Case Column.Trim.ToLower
            Case "auto"
                This.ColumnDefinitions.Add(New ColumnDefinition With {.Width = New GridLength(1, GridUnitType.Auto)})
            Case "*"
                This.ColumnDefinitions.Add(New ColumnDefinition With {.Width = New GridLength(1, GridUnitType.Star)})
            Case Else
                If System.Text.RegularExpressions.Regex.IsMatch(Column, "^\d+\*$") Then
                    This.ColumnDefinitions.Add(New ColumnDefinition With {.Width = _
                         New GridLength(CInt(Column.Substring(0, _
                         Column.IndexOf(CChar("*")))), GridUnitType.Star)})
                ElseIf IsNumeric(Column) Then
                    This.ColumnDefinitions.Add(New ColumnDefinition With _
                         {.Width = New GridLength(CDbl(Column), GridUnitType.Pixel)})
                Else
                    Throw New Exception("The only acceptable value for " &_
                      "the 'GridColumns' attached property is a comma separated " & _ 
                      "list comprised of the following options:" & vbCrLf & _
                      vbCrLf & "Auto,*,x (where x is the pixel width of the " & _ 
                      "column), x* (where x is the column width multiplier)")
                End If
            End Select
        Next
End Sub
public static DependencyProperty GridColumnsProperty = 
  DependencyProperty.RegisterAttached("GridColumns", typeof(string), 
  MethodBase.GetCurrentMethod().DeclaringType, new FrameworkPropertyMetadata(string.Empty, 
  FrameworkPropertyMetadataOptions.AffectsArrange, new PropertyChangedCallback(GridColumnsPropertyChanged)));

private static void GridColumnsPropertyChanged(object Sender, DependencyPropertyChangedEventArgs e)
{
    if (_GridColumnsChanged != null) {
        _GridColumnsChanged(Sender, e);
    }
    object This = Sender as Grid;
    if (This == null)
        throw new Exception("Only elements of type 'Grid' can " + 
          "utilize the 'GridColumns' attached property");
    DefineGridColumns(This);
}

public static string GetGridColumns(Grid This)
{
    return Convert.ToString(This.GetValue(GridColumnsProperty));
}

public static void SetGridColumns(Grid This, string Value)
{
    This.SetValue(GridColumnsProperty, Value);
}

private static void DefineGridColumns(Grid This)
{
    object Columns = GetGridColumns(This).Split(Convert.ToChar(","));
    This.ColumnDefinitions.Clear();
    foreach ( Column in Columns) {
        switch (Column.Trim.ToLower) {
            case "auto":
                This.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
                break;
            case "*":
                This.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
                break;
            default:
                if (System.Text.RegularExpressions.Regex.IsMatch(Column, "^\\d+\\*$")) {
                    This.ColumnDefinitions.Add(new ColumnDefinition { Width = 
                      new GridLength(Convert.ToInt32(Column.Substring(0, 
                      Column.IndexOf(Convert.ToChar("*")))), GridUnitType.Star) });
                } else if (Information.IsNumeric(Column)) {
                    This.ColumnDefinitions.Add(new ColumnDefinition { 
                          Width = new GridLength(Convert.ToDouble(Column), GridUnitType.Pixel) });
                } else {
                    throw new Exception("The only acceptable value for the 'GridColumns' attached " + 
                       "property is a comma separated list comprised of the following options:" + 
                       Constants.vbCrLf + Constants.vbCrLf + 
                       "Auto,*,x (where x is the pixel width of the column), " + 
                       "x* (where x is the column width multiplier)");
                }
                break;
        }
    }
}

Now defining your Column Definitions is as simple as defining your RowDefinitions:

<Style x:Key="MyGridStyle" TargetType="Grid">
    <Setter Property="local:Application.GridColumns" Value="Auto,*"/>
    <Setter Property="local:Application.GridRows" Value="1*,*,Auto"/>
<Style>

<Grid Style="{DynamicResource MyGridStyle}">
<Grid>

Points of Interest

Stylizing row/column definitions bridges a gap that exists in WPF scalability, and helps make your application more dynamic. Attached Properties are a vastly underrated way of achieving scalability with often very little code compared to other approaches.

History

  • Submitted: 7/24/2012.
  • Added C# samples: 7/24/2012.

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