Synchronizing Field Widths between FieldLayouts in XamDataGrid

This blog post shows how to force the width of Fields in a child FieldLayout to have the same width as the Fields in the parent/master FieldLayout.  This is a stopgap solution, useful only until XamDataGrid has native support for this feature.  The desired result is that the XamDataGrid's top-level records have resizable cells, and the non-resizable cells in child records keep their widths the same as the corresponding cells in their parent record.  Also, we will align the text in the child records with the text in the parent record, so that the XamDataGrid ends up looking like this:


In the screenshot above, the XamDataGrid has two master records, each of which represent a country.  The master records have child records, which represent states or cities within that country.  Every location in the grid has a name and a description, so it makes sense to keep all of the fields the same width and have their values line up.  This technique uses the hierarchical display support of XamDataGrid to provide a clean, simple listing of hierarchical data.  In addition, if you were to adjust the width of the Field headers or cells in a master record, the corresponding cells in the detail records would resize to the same width.

First, let's examine the XAML in the demo application's main Window that configures the XamDataGrid.  The most important parts are bold:

<igDP:XamDataGrid
  x:Name="xamDataGrid"
  DataSource="{Binding}"
  FieldLayoutInitialized="xamDataGrid_FieldLayoutInitialized"
  GroupByAreaLocation="None"
  >

  <igDP:XamDataGrid.Resources>
    <!--
    This Style enables us to monitor changes to field widths.
    -->
    <Style TargetType="{x:Type igDP:LabelPresenter}">
      <EventSetter
        Event="SizeChanged"
        Handler="OnLabelPresenterSizeChanged"
        />
    </Style>
  </igDP:XamDataGrid.Resources>

  <igDP:XamDataGrid.DataContext>
    <ObjectDataProvider
      MethodName="GetData"
      ObjectType="{x:Type local:DataSource}"
      />
  </igDP:XamDataGrid.DataContext>

  <igDP:XamDataGrid.FieldLayouts>
    <igDP:FieldLayout Key="master">
      <igDP:FieldLayout.Fields>
        <igDP:Field Name="ID" Visibility="Collapsed" />
        <igDP:Field Name="Name" />
        <igDP:Field Name="Description">
          <igDP:Field.Settings>
            <igDP:FieldSettings LabelWidth="260" CellWidth="260" />
          </igDP:Field.Settings>
        </igDP:Field>
      </igDP:FieldLayout.Fields>
      <igDP:FieldLayout.FieldSettings>
        <igDP:FieldSettings>
          <igDP:FieldSettings.CellValuePresenterStyle>
            <Style TargetType="{x:Type igDP:CellValuePresenter}">
              <Setter Property="FontWeight" Value="Bold" />
            </Style>
          </igDP:FieldSettings.CellValuePresenterStyle>
        </igDP:FieldSettings>
      </igDP:FieldLayout.FieldSettings>
    </igDP:FieldLayout>

    <igDP:FieldLayout Key="detail">
      <igDP:FieldLayout.Fields>
        <igDP:Field Name="ParentID" Visibility="Collapsed" />
        <igDP:Field Name="Name" />
        <igDP:Field Name="Description" />
      </igDP:FieldLayout.Fields>
      <igDP:FieldLayout.FieldSettings>
        <igDP:FieldSettings AllowResize="False" LabelMaxHeight="0">
          <igDP:FieldSettings.CellValuePresenterStyle>
            <!--
            This Style aligns the child record cell text with the parent cells
            regardless of whether the record is selected, mouseover, etc.
            -->
            <Style TargetType="{x:Type igDP:CellValuePresenter}">
              <Setter Property="Margin" Value="-3,0,0,0" />
              <Style.Triggers>
                <DataTrigger
                  Binding="{Binding
                    RelativeSource={RelativeSource AncestorType={x:Type igDP:DataRecordPresenter}},
                    Path=IsMouseOver}"
                  Value="True"
                  >
                  <Setter Property="Margin" Value="-3,0,0,0" />
                </DataTrigger>
                <DataTrigger
                  Binding="{Binding
                    RelativeSource={RelativeSource AncestorType={x:Type igDP:DataRecordPresenter}},
                    Path=IsSelected}"
                  Value="True"
                  >
                  <Setter Property="Margin" Value="-3,0,0,0" />
                </DataTrigger>
              </Style.Triggers>
            </Style>
          </igDP:FieldSettings.CellValuePresenterStyle>
        </igDP:FieldSettings>
      </igDP:FieldLayout.FieldSettings>
      <igDP:FieldLayout.Settings>
        <igDP:FieldLayoutSettings HighlightAlternateRecords="True" />
      </igDP:FieldLayout.Settings>
    </igDP:FieldLayout>
  </igDP:XamDataGrid.FieldLayouts>

  <igDP:XamDataGrid.FieldSettings>
    <igDP:FieldSettings AllowEdit="False" CellClickAction="SelectRecord" />
  </igDP:XamDataGrid.FieldSettings>

</igDP:XamDataGrid>

All of the magic here is done via Styles.  The control's Resources collection has a typed Style that targets LabelPresenter.  That Style adds a handler to a LabelPresenter's SizeChanged event.  This is how we can detect changes to the width of Fields at runtime.  The other Style is applied to the child FieldLayout's CellValuePresenterStyle.  It ensures that the text in the child record cells is pushed a little to the left, thus ensuring that it lines up with the text in the parent row.

The code-behind is not too complicated.  It just handles the SizeChanged event of every LabelPresenter to verify that the child Fields are the same width as the parent Fields.  It also handles the FieldLayoutInitialized event of the XamDataGrid so that it can bind the child Field widths to the parent Field widths.  The demo Window's code is below:

public partial class Window1 : Window
{
    FieldLayout _masterFieldLayout;

    public Window1()
    {
        InitializeComponent();
    }

    // Invoked when a field in the datagrid is resized.
    void OnLabelPresenterSizeChanged(object sender, SizeChangedEventArgs e)
    {
        var pres = sender as LabelPresenter;
        if (pres == null || pres.Field.Owner != _masterFieldLayout)
            return;

        // Ignore tiny changes because they can lead to infinite layout loops.
        double diff = Math.Abs(pres.Field.Settings.LabelWidth - pres.ActualWidth);
        if (diff <= 1)
            return;

        // Set the LabelWidth property so that the LabelWidthResolved is recalculated.
        // That forces the binding to update the width of the corresponding field in
        // the detail layout.
        pres.Field.Settings.LabelWidth = pres.ActualWidth;
    }

    void xamDataGrid_FieldLayoutInitialized(object sender, FieldLayoutInitializedEventArgs e)
    {
        if (_masterFieldLayout == null)
        {
            _masterFieldLayout = e.FieldLayout;
        }
        else
        {
            // Get all of the visible fields in the master layout.
            List<Field> masterFields =
                (from f in _masterFieldLayout.Fields
                 where f.VisibilityResolved == Visibility.Visible
                 select f)
                .ToList();

            // Get all of the visible fields in the detail layout.
            List<Field> detailFields =
                (from f in e.FieldLayout.Fields
                 where f.VisibilityResolved == Visibility.Visible
                 select f)
                .ToList();

            int iterations = Math.Min(masterFields.Count, detailFields.Count);

            // Bind the width of each field in the detail layout to
            // the resolved/actual width of the corresponding field
            // in the master layout.
            for (int n = 0; n < iterations; ++n)
            {
                BindingOperations.SetBinding(
                    detailFields[ n ].Settings,
                    FieldSettings.LabelWidthProperty,
                    new Binding
                    {
                        Path = new PropertyPath("LabelWidthResolved"),
                        Source = masterFields[ n ]
                    });
            }
        }
    }
}

Download the source code here.  The solution was built and tested in Visual Studio 2008, using NetAdvantage for WPF v8.1.


Comments  (7 )

Leiqi Bi
on Tue, Oct 27 2009 3:09 PM

Thanks Josh, this is very useful to us and it solved the width sync issue.  However, if you drag the NAME label to the right of DESCRIPTION, the child fields don't follow.  Do you have a solution to that as well?  I would be very much appreciated if you could let me know if you have one.

Mani Kumar
on Thu, Aug 12 2010 6:53 AM

This one does not work with version 10.1.

ludkim
on Tue, Aug 17 2010 1:11 PM

Great Information!!!... could you please do the same with a Data Base conexion???... in this example you add the data manually and i want to do this with a table.. Help me!!!

dougiejay
on Thu, Mar 10 2011 4:38 AM

THis does not work with 10.3. Can someone please check this sample?

shaktishekhar
on Thu, Sep 29 2011 10:17 AM

In order to make it work with 10.3, please open solution in VS2010, point referances to 10.3 infragistics assmblies and change Target Framework of Project to 4.0.

Sujit Dhawde
on Tue, May 14 2013 10:45 AM

Any addtional changes do we need to do in 10.1v? This approach is not working.

Eswar Yennam
on Tue, Nov 18 2014 11:07 AM

Great one. This solved my issue. I have another one. When I export above results to PDF the United States and Germany Sub Headers are not aligned to Name and Description headers. What best we can do to achieve this?

Thanks for the good post.

Add a Comment

Please Login or Register to add a comment.