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.