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

WPF Padded Grid

, 6 Sep 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
A padded grid for WPF that is ideal for laying out forms.

Introduction

Wouldn't it be nice if the WPF Grid control had a 'Padding' property? In this article, I will show you how to create the PaddedGrid control, a custom control that derives from Grid and provides this much needed property.

Here's a picture of the PaddedGrid being used in the Visual Studio XAML designer window. As you can see, the labels and text boxes are spaced apart from each other by the amount specified in the Padding property.

Designer.png

Background

I often use Grids for laying out data entry sections of forms. Typically, there will be a column of Labels on the left and a column of fields on the right. As the Grid control doesn't have a Padding property, the Margin of the controls in the Grid has to be set to an appropriate value to space things out properly. This can clutter up the XAML, and is a bit time consuming. Having a Padding property on the parent Grid makes things much cleaner.

Planning the Control

Before I go through the control code step-by-step, I'll explain how the padding will work.

With WPF, there are generally many ways to achieve things like this; each technique will have its own benefits and drawbacks. Ideally, we would wrap each grid cell in a container of some kind, and set the margin on that. This would give the correct layout in all cases, but is needlessly complex. A more simple solution is to have the control do what I have been doing manually - set the margin property of the child controls. In fact, if we can bind the Margin property of the grid's immediate children to the Padding property of the parent PaddedGrid, then we will have what we need.

By binding the children's Margin to the parent's Padding (a technique that is commonly used in DataTemplates, ItemTemplates, and so on), we avoid the need to do any complicated custom layout in procedural code. By making Padding a Dependency Property, we keep the PaddedGrid consistent with the standard WPF controls.

The PaddedGrid Control

I'll take you through the PaddedGrid control step-by-step. I've tried to do this in the most logical order for the article; the code file itself is in a slightly different order. Here we go:

public class PaddedGrid : Grid
{
    /// <summary>
    /// The internal dependency property object for the 'Padding' property.
    /// </summary>
    private static readonly DependencyProperty PaddingProperty =
        DependencyProperty.Register("Padding", 
            typeof(Thickness), typeof(PaddedGrid),
            new UIPropertyMetadata(new Thickness(0.0), 
            new PropertyChangedCallback(OnPaddingChanged)));

First, we create the class and derive it from Grid. Then we create the static Dependency Property for Padding. Dependency Properties are absolutely critical to WPF; if you are unfamiliar with them, start with the MSDN article: 'Dependency Properties Overview'.

We register the Dependency Property as a Thickness type. This is the same data type as the Margin property that most controls have. It allows a padding on the left, right, top, and bottom to be specified (or a single padding for each side). The default value is set to zero, which means that, by default, the PaddedGrid will look just like a standard grid. One thing to note is that we also specify a PropertyChangedCallback - this will be called every time Padding is changed. More on this later.

public PaddedGrid()
{
    //  Add a loded event handler.
    Loaded += new RoutedEventHandler(PaddedGrid_Loaded);
}

The constructor is very straightforward - it simply adds an event handler for the Loaded event. It is in the Loaded event handler that we will bind the Margin properties of the child controls to the Padding property of the PaddedGrid.

void PaddedGrid_Loaded(object sender, RoutedEventArgs e)
{
    //  Get the number of children.
    int childCount = VisualTreeHelper.GetChildrenCount(this);

    //  Go through the children.
    for (int i = 0; i < childCount; i++)
    {
        //  Get the child.
        DependencyObject child = VisualTreeHelper.GetChild(this, i);

        //  Try and get the margin property.
        DependencyProperty marginProperty = GetMarginProperty(child);

        //  If we have a margin property, bind it to the padding.
        if (marginProperty != null)
        {
            //  Create the binding.
            Binding binding = new Binding();
            binding.Source = this;
            binding.Path = new PropertyPath("Padding");

            //  Bind the child's margin to the grid's padding.
            BindingOperations.SetBinding(child, marginProperty, binding);
        }
    }
}

We use the VisualTreeHelper to get the number of immediate children in the visual tree. We can then use a loop and the VisualTreeHelper to get each child as a DependencyObject. By calling the helper function GetMarginProperty (which is described later), we attempt to get the Margin property for the child. If it exists, we create a binding to the PaddedGrid's Padding property; then we set it to the Margin property of the child. This way, each immediate child will have its Margin property bound to the Padding of the parent PaddedGrid.

By using a binding like this, we ensure that changes to the padding at run-time will be correctly reflected in the Margin of each child control.

protected static DependencyProperty GetMarginProperty(DependencyObject dependencyObject)
{
    //  Go through each property for the object.
    foreach (PropertyDescriptor propertyDescriptor in 
                TypeDescriptor.GetProperties(dependencyObject))
    {
        //  Get the dependency property descriptor.
        DependencyPropertyDescriptor dpd = 
           DependencyPropertyDescriptor.FromProperty(propertyDescriptor);

        //  Have we found the margin?
        if (dpd != null && dpd.Name == "Margin")
        {
            //  We've found the margin property, return it.
            return dpd.DependencyProperty;
        }
    }

    //  Failed to find the margin, return null.
    return null;
}

Getting the Margin dependency property for a dependency object is simple enough. We use the TypeDescriptor object to iterate through each property. The DependencyPropertyDescriptor.FromProperty function will get a reference to the dependency property - but only if the property is a dependency property (rather than a standard property). If it isn't, it will return null. This is the quickest way I found to enumerate the dependency properties.

If we have found a dependency property, we check to see if it is the 'Margin' property. If it is, we return it. Easy!

This function could be generalised to find any dependency property by name, but I'm keeping things simple in the article to make sure that the logic is easy to follow.

The last thing to do is implement the OnPaddingChanged callback we set in the constructor of the Padding dependency property.

private static void OnPaddingChanged(DependencyObject dependencyObject, 
               DependencyPropertyChangedEventArgs args)
{
    //  Get the padded grid that has had its padding changed.
    PaddedGrid paddedGrid = dependencyObject as PaddedGrid;

    //  Force the layout to be updated.
    paddedGrid.UpdateLayout();
}

The callback is very simple. We get the PaddedGrid that has had its padding changed and force it to update its layout.

Using the PaddedGrid

First, add a reference to the PaddedGrid assembly to your project, then add the namespace in the XAML of the Window (or Page) that you will use it in:

<Window x:Class="PaddedGridExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:paddedGrid="clr-namespace:PaddedGrid;assembly=PaddedGrid"
        Title="MainWindow" Height="350" Width="525">

Once you have the namespace defined, you can use the PaddedGrid (and its Padding) like so:

<!-- The padded grid. -->
<paddedGrid:PaddedGrid x:Name="paddedGrid" Padding="2,6,3,6" >
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="2*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <!-- ...etc... -->
     </Grid.RowDefinitions>

     <!-- Some simple fields. -->
     <Label Grid.Row="0" Grid.Column="0">Name</Label>
     <TextBox Grid.Row="0" Grid.Column="1" Text="Enter Name Here" />

     <!-- More controls etc etc -->

</paddedGrid:PaddedGrid>

Limitations

I have only used the PaddedGrid in fairly simple layouts. The trick of setting the margin of the immediate children may not work in every case. This is a simple control for simple scenarios - however, if you have any problems with it, do let me know and I will endeavour to resolve the issue and post updates.

Improvements

If you have any suggestions for improvements, then let me know. I'd be happy to extend the control and the article if there is more that could be done!

The PaddedGrid library and a sample project can be downloaded from the link above. Enjoy!

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

 
QuestionDesigner support? PinmemberChrister V. Aas21-Mar-11 3:30 
QuestionEven simpler? PinmemberJürgen Röhr16-Sep-10 23:24 
AnswerRe: Even simpler? PinmemberDaveKerr17-Sep-10 1:55 
GeneralMy vote of 5 PinmemberEric Xue (brokensnow)7-Sep-10 0:22 
QuestionAnother soloution? Pinmemberseesharper6-Sep-10 21:22 
AnswerRe: Another soloution? PinmemberDaveKerr6-Sep-10 21:25 
Very nice - I particularly like the automatic indexing of panel items!
GeneralMy vote of 5 Pinmembersam.hill6-Sep-10 16:50 
GeneralNot being funny but... PinmvpSacha Barber6-Sep-10 4:51 
GeneralRe: Not being funny but... PinmemberDaveKerr6-Sep-10 6:33 
GeneralRe: Not being funny but... PinmvpSacha Barber6-Sep-10 7:21 

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.141216.1 | Last Updated 6 Sep 2010
Article Copyright 2010 by Dave Kerr
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid