Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

GridEx for WPF: Automatic Placement of Children

4.79/5 (5 votes)
8 Apr 2014CPOL4 min read 16.5K   484  
GridEx makes WPF Grid definition and edition easier thanks to its automatic layout.

Image 1

Introduction

I was searching for an extended Grid which could offer the following benefits:

  • By default, it would automatically arrange the children on two columns one after another.
  • It wouldn’t need the Grid.Row and Grid.Column attached properties to be set on each child.
  • It wouldn’t need the Grid.RowDefinitions and Grid.ColumnDefinitions. In other words, it would add rows automatically if needed, and same for columns. Their Width/Height would be set on "Auto", except for the last column : "*" by default.
  • If told, it would allow an item to be placed relatively to the previous item:
    • on the same row
    • directly on the next row
    • in the same cell
    • at a specific position (old style using Grid.Row and Grid.Column attached properties)

I almost found the right control on Code Project. The TwoColumnGrid by Isaks was almost the grid I was looking for but I wanted a few more features.

While searching the Web, I also found a nice piece of code from Sam Naseri to make grid definitions shorter. I used his code in my article.

Using the code

The above grids were produced using the following XAML (I didn’t write the resource part for the sake of brevity):

First grid:

XML
<tools:GridEx>

    <Label Content="Name:" />
    <TextBox />

    <Label Content="First name:" />
    <TextBox />

    <Label Content="Age:" />
    <TextBox />
            
</tools:GridEx>

Second grid:

XML
<tools:GridEx>

    <!-- A series of pair of items --/>
    <Label Content="Name:" />
    <TextBox />

    <Label Content="First name:" />
    <TextBox />

    <Label Content="Age:" />
    <TextBox />
            
    <!-- here we want to jump to the next row --/>
    <Label Content="Interested in:"
           tools:GridEx.GridExBehavior="NextRow" />
    <CheckBox Content="Geography" />
    <!-- here we want all the following items to remain on the same row -->
    <CheckBox Content="History"
              tools:GridEx.GridExBehavior="SameRow" />
    <CheckBox Content="Literature"
              tools:GridEx.GridExBehavior="SameRow" />
    <CheckBox Content="Math"
              tools:GridEx.GridExBehavior="SameRow" />
    <CheckBox Content="Physics"
              tools:GridEx.GridExBehavior="SameRow" />

    <!-- We want our smiley to be at a specific place, so we set its behavior to Manual -->
    <ContentControl Content="{StaticResource smiley}"
                    tools:GridEx.GridExBehavior="Manual"
                    Grid.Row="0"
                    Grid.Column="10"
                    Grid.RowSpan="2" />

</tools:GridEx>

Third grid:

XML
<tools:GridEx>
    <!-- On this grid, we put 4 items per row
        and we don't want every column to be on Auto,
        so we define them manually using Sam Naseri's elegant property -->
    <tools:GridEx ItemsPerRow="4"
                  tools:GridEx.Columns="auto;*;auto;*">

    <Label Content="Name:" />
    <TextBox />
    <Label Content="First name:" />
    <TextBox />

    <Label Content="Address:" />
    <TextBox />
    <Label Content="City:" />
    <TextBox />

</tools:GridEx>

Do you see how clean and light our XAML looks now?

  • I didn’t write any RowDefinitions, nor ColumnDefinitions on the first and second grid.
  • On the third grid, I only wrote the column definitions using the elegant attached property by Sam Naseri.
  • There is only one item (the smiley) with its Grid.Row and Grid.Column set: because I wanted to place it manually.

Now let’s see step by step how we make this possible.

Automatic Row/Column index for each child

The idea is to arrange children as they are added to the Grid. The first child would be given Row=, Column=0, the second child Row=0, Column=1, the third one, Row=1, Column=0, and so on.

To achieve this, I first wanted to add an attached property to the Grid class and listen to its Children collection to be notified when it is modified. I thought I could use a CollectionChanged event or something similar, but unfortunately there is no such event. The only way I found is overloading the Grid.OnVisualChildrenChanged method, which means I had no choice but subclassing...

So I created my GridEx class and overloaded its OnVisualChildrenChanged method:

C#
public class GridEx : Grid
{
    /// <summary>
    /// Overriden to automatically set the Row and Column properties of each child.
    /// This method is called when children are added or removed from the Grid.Children collection.
    /// </summary>
    protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
    {
        UpdateChildren();
        base.OnVisualChildrenChanged(visualAdded, visualRemoved);
    }

    void UpdateChildren()
    {
        //current row and column
        //columnIndex is incremented for each item to place them one after the other
        int rowIndex = 0;
        int columnIndex = 0;
        foreach (UIElement child in Children)
        {
            //happens while editing the children collection in xaml
            //if we don't handle this, the Designer throws an exception
            if (child == null)
                continue;

            //if columnIndex is below ItemsPerRow, put this item on the current row
            //else put this item on the next row, first column
            if (columnIndex >= ItemsPerRow)
            {
                rowIndex++;
                columnIndex = 0;
            }
            Grid.SetRow(child, rowIndex);
            Grid.SetColumn(child, columnIndex++);
        }
    }
}

Then, I added the ItemsPerRow property:

C#
/// <summary>
/// Number of items per row.
/// Default is 2.
/// </summary>
public int ItemsPerRow
{
    get { return (int)GetValue(ItemsPerRowProperty); }
    set { SetValue(ItemsPerRowProperty, value); }
}
public static readonly DependencyProperty ItemsPerRowProperty = DependencyProperty.Register(
    "ItemsPerRow",
    typeof(int),
    typeof(GridEx),
    new FrameworkPropertyMetadata(2, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure, ItemsPerRow_PropertyChanged));
static void ItemsPerRow_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    GridEx grid = d as GridEx;
    if (grid != null)
        grid.UpdateChildren();
}

The FrameworkPropertyMetadataOptions on ItemsPerRow property means a new layout pass should be performed after this property has changed.

Automatic row and column definitions

The idea is to retrieve the maximum row index among all children and check whether we have to add extra rows. Same logic for the columns.

So after all children have received their row and column indices, we must find the correct place to add the RowDefinitions and ColumnDefinitions. Overriding the MeasureOverride method is probably the best option since it is called when the control is ready for layout and just before layout:

C#
protected override Size MeasureOverride(Size constraint)
{
    //OfType<> is used to get an enumerable collection of non-null objects.
    //Children may contain null objects at design time --> they should be ignored to avoid exceptions.
    IEnumerable<UIElement> children = Children.OfType<UIElement>();
    if (children.Count() > 0)
    {
        int maxRowIndex = children.Max(child => GetRow(child));
        int maxColumnIndex = children.Max(child => GetColumn(child));

        //automatically add rows if needed
        for (int i = RowDefinitions.Count; i <= maxRowIndex; i++)
            RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
        //automatically add columns if needed
        for (int i = ColumnDefinitions.Count; i <= maxColumnIndex; i++)
            ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
    }

    return base.MeasureOverride(constraint);
}

We set the extra rows and columns size to Auto but we would like the last column to be stretched by default. So let’s add another property for this, LastColumnWidth:

C#
/// <summary>
/// Width of the last automatically added column.
/// Default is "*".
/// Note that this property will be ignored if the last column was defined manually.
/// </summary>
public GridLength LastColumnWidth
{
    get { return (GridLength)GetValue(LastColumnWidthProperty); }
    set { SetValue(LastColumnWidthProperty, value); }
}
public static readonly DependencyProperty LastColumnWidthProperty = DependencyProperty.Register(
    "LastColumnWidth",
    typeof(GridLength),
    typeof(GridEx),
    new FrameworkPropertyMetadata(new GridLength(1, GridUnitType.Star), FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));

Now we modify the MeasureOverride method where the columns are added:

C#
//automatically add columns if needed
for (int i = ColumnDefinitions.Count; i <= maxColumnIndex; i++)
{
    GridLength unitType = new GridLength(1, GridUnitType.Auto);
    if (i == maxColumnIndex)
        unitType = LastColumnWidth;
    ColumnDefinitions.Add(new ColumnDefinition() { Width = unitType });
}

Our method is now OK. Note that extra definitions are added only if the max index exceeds the current count. This means we can still give our own RowDefinition/ColumnDefinition, GridEx will only add the missing ones.

Extra behaviors

We have almost completed all of our requirements, the only missing features are the last ones:

  • If told, it would allow an item to be placed relatively to the previous item:
    • on the same row
    • directly on the next row
    • in the same cell
    • at a specific position (old style using Grid.Row and Grid.Column attached properties)

We will use an attached property on the children for that. And the type of this property will be an enum:

C#
/// <summary>
/// Defines the behavior of a child item.
/// </summary>
public enum GridExBehavior
{
    /// <summary>
    /// Items are added one after the other, with 2 items per row by default.
    /// </summary>
    Default,
    /// <summary>
    /// This item should be placed on the same row, next column.
    /// </summary>
    SameRow,
    /// <summary>
    /// This item should be placed on the same row, same column.
    /// </summary>
    SameCell,
    /// <summary>
    /// This item should be placed on the next row, first column.
    /// </summary>
    NextRow,
    /// <summary>
    /// This item should be ignored.
    /// Its Grid.Row and Grid.Column properties should be set manually.
    /// </summary>
    Manual
}

Then the corresponding attached property. This property can be used on any type of child so we set its OwnerType to UIElement:

C#
public static GridExBehavior GetGridExBehavior(DependencyObject obj)
{
    return (GridExBehavior)obj.GetValue(GridExBehaviorProperty);
}
public static void SetGridExBehavior(DependencyObject obj, GridExBehavior value)
{    
    obj.SetValue(GridExBehaviorProperty, value);
}
public static readonly DependencyProperty GridExBehaviorProperty = DependencyProperty.RegisterAttached(
    "GridExBehavior",
    typeof(GridExBehavior),
    typeof(UIElement),
    new UIPropertyMetadata(GridExBehavior.Default, GridExBehavior_PropertyChangedCallback));
static void GridExBehavior_PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    GridEx grid = VisualTreeHelper.GetParent(d) as GridEx;
    if (grid != null)
        grid.UpdateChildren();
}

Note that if the property value changes, it calls UpdateChildren since all the indices might be affected by this change.

Finally, we have to change our UpdateChildren method to handle the different behaviors:

C#
void UpdateChildren()
{
    //current row and column
    //columnIndex is incremented for each item to place them one after the other
    int rowIndex = 0;
    int columnIndex = 0;
    foreach (UIElement child in Children)
    {
        //happens while editing the children collection in xaml
        //if we don't handle this, the Designer throws an exception
        if (child == null)
            continue;

        //we'll set the child's Row and Column properties depending on its attached behavior
        switch (GridEx.GetGridExBehavior(child))
        {
            case GridExBehavior.Default:
                //if columnIndex is below ItemsPerRow, put this item on the current row
                //else put this item on the next row, first column
                if (columnIndex >= ItemsPerRow)
                {
                    rowIndex++;
                    columnIndex = 0;
                }
                Grid.SetRow(child, rowIndex);
                Grid.SetColumn(child, columnIndex++);
                break;

            case GridExBehavior.SameRow:
                //put this item on the same row
                Grid.SetRow(child, rowIndex);
                Grid.SetColumn(child, columnIndex++);
                break;

            case GridExBehavior.SameCell:
                //put this item on the same row, same column (of the previous item)
                //just make sure we don't set a negative column index (happens sometimes when editing XAML code)
                Grid.SetRow(child, rowIndex);
                Grid.SetColumn(child, Math.Max(0, columnIndex - 1));
                break;

            case GridExBehavior.NextRow:
                //put this item on the next row, first column
                columnIndex = 0;
                Grid.SetRow(child, ++rowIndex);
                Grid.SetColumn(child, columnIndex++);
                break;

            case GridExBehavior.Manual:
                //don't change anything on this item
                break;
        }
    }
}

Our GridEx is now complete. You may also add Sam Naseri’s code inside it to take advantage of his nice attached properties.

Conclusion

It is not a lot of code and not a very complex control, but it is really much easier to use than the regular Grid. It improves readability and makes editing (moving rows, etc.) painless. I hope you will find it useful. There is one small issue I found: you will sometimes have to manually reload the Designer (by rebuilding your project for example) to reflect your changes.

History

  • 2014-04-07: First version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)