Josh Smith

Follow me down the rabbit hole of cutting edge technology.
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.

Posted: 02 Jul 2008, 16:55

Comments

JayJay said:

Hey Josh,

Thank you for this post! With this post you single-handedly won back at least one Infragistics customer!

Now I just need to know how to get this to work when I have an additional level of hierarchy depth.

# July 4, 2008 7:02 AM

Joshua Smith said:

JayJay,

That's great to hear!  Adding in another level of the hierarchy should be a matter of just binding that level's Fields to the master level's Fields in the FieldLayoutInitialized handler.

Josh

# July 4, 2008 9:23 AM
Leave a Comment

(required) 

(required) 

(optional)

(required) 

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS