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:
<tools:GridEx>
<Label Content="Name:" />
<TextBox />
<Label Content="First name:" />
<TextBox />
<Label Content="Age:" />
<TextBox />
</tools:GridEx>
Second grid:
<tools:GridEx>
<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" />
<ContentControl Content="{StaticResource smiley}"
tools:GridEx.GridExBehavior="Manual"
Grid.Row="0"
Grid.Column="10"
Grid.RowSpan="2" />
</tools:GridEx>
Third grid:
<tools:GridEx>
<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:
public class GridEx : Grid
{
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
UpdateChildren();
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
}
void UpdateChildren()
{
int rowIndex = 0;
int columnIndex = 0;
foreach (UIElement child in Children)
{
if (child == null)
continue;
if (columnIndex >= ItemsPerRow)
{
rowIndex++;
columnIndex = 0;
}
Grid.SetRow(child, rowIndex);
Grid.SetColumn(child, columnIndex++);
}
}
}
Then, I added the ItemsPerRow
property:
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:
protected override Size MeasureOverride(Size constraint)
{
IEnumerable<UIElement> children = Children.OfType<UIElement>();
if (children.Count() > 0)
{
int maxRowIndex = children.Max(child => GetRow(child));
int maxColumnIndex = children.Max(child => GetColumn(child));
for (int i = RowDefinitions.Count; i <= maxRowIndex; i++)
RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
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
:
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:
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:
public enum GridExBehavior
{
Default,
SameRow,
SameCell,
NextRow,
Manual
}
Then the corresponding attached property. This property can be used on any type of child so we set its OwnerType
to UIElement
:
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:
void UpdateChildren()
{
int rowIndex = 0;
int columnIndex = 0;
foreach (UIElement child in Children)
{
if (child == null)
continue;
switch (GridEx.GetGridExBehavior(child))
{
case GridExBehavior.Default:
if (columnIndex >= ItemsPerRow)
{
rowIndex++;
columnIndex = 0;
}
Grid.SetRow(child, rowIndex);
Grid.SetColumn(child, columnIndex++);
break;
case GridExBehavior.SameRow:
Grid.SetRow(child, rowIndex);
Grid.SetColumn(child, columnIndex++);
break;
case GridExBehavior.SameCell:
Grid.SetRow(child, rowIndex);
Grid.SetColumn(child, Math.Max(0, columnIndex - 1));
break;
case GridExBehavior.NextRow:
columnIndex = 0;
Grid.SetRow(child, ++rowIndex);
Grid.SetColumn(child, columnIndex++);
break;
case GridExBehavior.Manual:
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.