Insulating against grid row order changes in WPF

Tuesday, December 16, 2008 1:40:21 AM (GMT Standard Time, UTC+00:00)

One of the things that I don't like about the WPF Grid is the fact that if you insert new rows into a grid layout control you have to update the Grid.Row attached property to ensure that the rows render in proper order.

For example, if you supplied xaml that showed contact information and looked something like this:

image

here is the xaml representation:

<Border BorderBrush="LightGray" BorderThickness="1" Padding="10" CornerRadius="10">
            <Grid>
                <Grid.Resources>
                    <Thickness x:Key="Rowspacing">7</Thickness>
                </Grid.Resources>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"  />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
 
                <Label  Grid.Row="0" Content="First Name:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right" />
                <Label  Grid.Row="1" Content="Last Name:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
                <Label  Grid.Row="2" Content="Street Address:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
                <Label  Grid.Row="3" Content="City:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
                <Label  Grid.Row="4" Content="State:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
                <Label  Grid.Row="5" Content="Zip:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
                <Label  Grid.Row="6" Content="Company:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
 
 
                <TextBox  Grid.Column="1" Grid.Row="0" Text="Shannon" MinWidth="300" VerticalAlignment="Center" />
                <TextBox  Grid.Column="1" Grid.Row="1" Text="Braun" AcceptsReturn="True"   VerticalAlignment="Center"></TextBox>
                <TextBox  Grid.Column="1" Grid.Row="2" Text="111 Main Street" AcceptsReturn="True"   VerticalAlignment="Center"></TextBox>
                <TextBox  Grid.Column="1" Grid.Row="3" Text="Minneapolis" AcceptsReturn="True"   VerticalAlignment="Center"></TextBox>
                <TextBox  Grid.Column="1" Grid.Row="4" Text="Mn" AcceptsReturn="True"   VerticalAlignment="Center"></TextBox>
                <TextBox  Grid.Column="1" Grid.Row="5" Text="55414" AcceptsReturn="True"  VerticalAlignment="Center"></TextBox>
                <TextBox  Grid.Column="1" Grid.Row="6" Text="Sysknowlogy" AcceptsReturn="True"  VerticalAlignment="Center"></TextBox>
                
            </Grid>
        </Border>

If I wanted to insert a row between Grid.Row="0" (First Name) and Grid.Row="1" (Last Name) I would have to also update the Grid.Row attached property for all of the rows after the insert. 

One of the ways to get around this is to bind the Grid.Row attached property on the Labels and Textboxes to the Index of the RowDefinition. When a RowDefinition is inserted or moved there is no need to update the indexes.  Unfortunately the RowDefinition doesn't expose a Index, so using the Binding facilities of WPF won't work here.  Instead we will leverage a MarkupExtension to translate the the reference to the RowDefinition name to the actual index of that RowDefinition, insulating us from any row inserts or changes in order.

The first step is to add a name to each of the RowDefinitions so that we can reference them.

<RowDefinition x:Name="Row_FirstName" Height="Auto"  />
<RowDefinition x:Name="Row_LastName" Height="Auto" />
<RowDefinition x:Name="Row_Street" Height="Auto" />
<RowDefinition x:Name="Row_City" Height="Auto" />
<RowDefinition x:Name="Row_State" Height="Auto" />
<RowDefinition x:Name="Row_Zip" Height="Auto" />
<RowDefinition x:Name="Row_Company" Height="Auto" />

Write the MarkUpExtension that will support the translation from element name to row index... the result looks like the following:

[MarkupExtensionReturnType(typeof(int))]
public class MapTo : MarkupExtension
{
    /// <summary>
    /// Initializes a new instance of the <see cref="MapTo"/> class.
    /// </summary>
    public MapTo()
    {
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="MapTo"/> class.
    /// </summary>
    /// <param name="rowName">Name of the row.</param>
    public MapTo(string rowName)
    {
        _name = rowName;
    }
 
    private string _name;
    /// <summary>
    /// Gets or sets the name of the element.
    /// </summary>
    /// <value>The name of the element.</value>
    public string ElementName
    {
        get { return _name; }
        set { _name = value; }
    }
 
    /// <summary>
    /// When implemented in a derived class, returns an object that is set as the value of the target property for this markup extension.
    /// </summary>
    /// <param name="serviceProvider">Object that can provide services for the markup extension.</param>
    /// <returns>
    /// The object value to set on the property where the extension is applied.
    /// </returns>
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
 
        IProvideValueTarget ipvt = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
 
        int idx = -1;
        if (ipvt != null)
        {
            
            FrameworkElement target = ipvt.TargetObject as FrameworkElement;
            if (target != null)
            {
                
                Grid grd = target.Parent as Grid;
                if (grd != null)
                {
                    idx = GetRowIndex(grd);
                }
                else
                    throw new NullReferenceException("Grid was not found as the parent of element " + _name);
            }
        }
 
        return idx;
 
    }
 
    /// <summary>
    /// Gets the index of the row based on the name of the element.
    /// </summary>
    /// <param name="parent">The parent.</param>
    /// <returns></returns>
    private int GetRowIndex(Grid parent)
    {
        int idx = -1;
        if (parent != null)
        {
            RowDefinition rowDefinition = parent.FindName(_name) as RowDefinition;
 
            if (rowDefinition != null)
            {
                idx = parent.RowDefinitions.IndexOf(rowDefinition);
            }
            else
                throw new NullReferenceException("RowDefinition was not found for name " + _name);
        }
 
        return idx;
    }
}

Next we use the MapTo MarkUpExtension in xaml to identify the row index for both the Labels and TextBoxes:

<Border BorderBrush="LightGray" BorderThickness="1" Padding="10" CornerRadius="10">
            <Grid>
                <Grid.Resources>
                    <Thickness x:Key="Rowspacing">7</Thickness>
                </Grid.Resources>
                <Grid.RowDefinitions>
                    <RowDefinition x:Name="Row_FirstName" Height="Auto"  />
                    <RowDefinition x:Name="Row_LastName" Height="Auto" />
                    <RowDefinition x:Name="Row_Street" Height="Auto" />
                    <RowDefinition x:Name="Row_City" Height="Auto" />
                    <RowDefinition x:Name="Row_State" Height="Auto" />
                    <RowDefinition x:Name="Row_Zip" Height="Auto" />
                    <RowDefinition x:Name="Row_Company" Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
 
                <Label  Grid.Row="{local:MapTo Row_FirstName}" Content="First Name:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right" />
                <Label  Grid.Row="{local:MapTo Row_LastName}" Content="Last Name:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
                <Label  Grid.Row="{local:MapTo Row_Street}" Content="Street Address:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
                <Label  Grid.Row="{local:MapTo Row_City}" Content="City:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
                <Label  Grid.Row="{local:MapTo Row_State}" Content="State:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
                <Label  Grid.Row="{local:MapTo Row_Zip}" Content="Zip:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
                <Label  Grid.Row="{local:MapTo Row_Company}" Content="Company:" Margin="{StaticResource Rowspacing}" HorizontalAlignment="Right"/>
 
 
                <TextBox  Grid.Column="1" Grid.Row="{local:MapTo Row_FirstName}" Text="Shannon" MinWidth="300" VerticalAlignment="Center" />
                <TextBox  Grid.Column="1" Grid.Row="{local:MapTo Row_LastName}" Text="Braun" AcceptsReturn="True"   VerticalAlignment="Center"></TextBox>
                <TextBox  Grid.Column="1" Grid.Row="{local:MapTo Row_Street}" Text="111 Main Street" AcceptsReturn="True"   VerticalAlignment="Center"></TextBox>
                <TextBox  Grid.Column="1" Grid.Row="{local:MapTo Row_City}" Text="Minneapolis" AcceptsReturn="True"   VerticalAlignment="Center"></TextBox>
                <TextBox  Grid.Column="1" Grid.Row="{local:MapTo Row_State}" Text="Mn" AcceptsReturn="True"   VerticalAlignment="Center"></TextBox>
                <TextBox  Grid.Column="1" Grid.Row="{local:MapTo Row_Zip}" Text="55414" AcceptsReturn="True"  VerticalAlignment="Center"></TextBox>
                <TextBox  Grid.Column="1" Grid.Row="{local:MapTo Row_Company}" Text="Sysknowlogy" AcceptsReturn="True"  VerticalAlignment="Center"></TextBox>
 
            </Grid>
        </Border>

To add additional rows, add a new RowDefinition with a name and then map the Grid.Row attached property to the name of the row.

Download Project: GridLayoutApproach.zip

Tuesday, December 30, 2008 6:46:36 AM (GMT Standard Time, UTC+00:00)
exactly the issue I want to solve! Thanks!

However, VS2008 designer has some problem to load the xaml, error message:
Grid was not found as the parent of element Row_FirstName

The project can still be compiled and executed successfully though.
denis
Comments are closed.