After this problem, I want to share what I have discovered so far.
DataGrid uses two different methods for this.
First: RowHeader
This is a simplified Template for DataGridRow :
<Border x:Name="DGR_Border" ... > <SelectiveScrollingGrid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <DataGridRowHeader Grid.RowSpan="2" SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical" ... /> <DataGridCellsPresenter Grid.Column="1" ... /> <DataGridDetailsPresenter Grid.Column="1" Grid.Row="1" SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}, Path=AreRowDetailsFrozen, Converter={x:Static DataGrid.RowDetailsScrollingConverter}, ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}}" ... /> </SelectiveScrollingGrid> </Border>
As you can see, the DataGrid uses the attached property SelectiveScrollingOrientation to hold the RowHeader in position. If this property is set (or changed), it creates an adapted TranslateTransform ScrollViewer with the parent ScrollViewer Offset for the element. See the source code for more details.
Second: FrozenColumns
This stuff takes place in a DataGridCellsPanel ArrangeOverride() . It uses the private ArrangeState class to maintain state between the locations of multiple children.
private class ArrangeState { public ArrangeState() { FrozenColumnCount = 0; ChildHeight = 0.0; NextFrozenCellStart = 0.0; NextNonFrozenCellStart = 0.0; ViewportStartX = 0.0; DataGridHorizontalScrollStartX = 0.0; OldClippedChild = null; NewClippedChild = null; } public int FrozenColumnCount { get; set; } public double ChildHeight { get; set; } public double NextFrozenCellStart { get; set; } public double NextNonFrozenCellStart { get; set; } public double ViewportStartX { get; set; } public double DataGridHorizontalScrollStartX { get; set; } public UIElement OldClippedChild { get; set; } public UIElement NewClippedChild { get; set; } }
After initializing the state with
private void InitializeArrangeState(ArrangeState arrangeState) { DataGrid parentDataGrid = ParentDataGrid; double horizontalOffset = parentDataGrid.HorizontalScrollOffset; double cellsPanelOffset = parentDataGrid.CellsPanelHorizontalOffset; arrangeState.NextFrozenCellStart = horizontalOffset; arrangeState.NextNonFrozenCellStart -= cellsPanelOffset; arrangeState.ViewportStartX = horizontalOffset - cellsPanelOffset; arrangeState.FrozenColumnCount = parentDataGrid.FrozenColumnCount; }
he calls
ArrangeChild(children[childIndex] as UIElement, i, arrangeState);
for all implemented children and calculates the estimated width for unrealized children / columns.
double childSize = GetColumnEstimatedMeasureWidth(column, averageColumnWidth); arrangeState.NextNonFrozenCellStart += childSize;
At the end, the values will be set in the appropriate fields in the DataGrid .
private void FinishArrange(ArrangeState arrangeState) { DataGrid parentDataGrid = ParentDataGrid; // Update the NonFrozenColumnsViewportHorizontalOffset property of datagrid if (parentDataGrid != null) { parentDataGrid.NonFrozenColumnsViewportHorizontalOffset = arrangeState.DataGridHorizontalScrollStartX; } // Remove the clip on previous clipped child if (arrangeState.OldClippedChild != null) { arrangeState.OldClippedChild.CoerceValue(ClipProperty); } // Add the clip on new child to be clipped for the sake of frozen columns. _clippedChildForFrozenBehaviour = arrangeState.NewClippedChild; if (_clippedChildForFrozenBehaviour != null) { _clippedChildForFrozenBehaviour.CoerceValue(ClipProperty); } }
Details for ArrangeChild(UIElement child, int displayIndex, ArrangeState arrangeState) can be found on line 1470 in the source code .
Conclusion
It is not like just making the columns frozen. Even if this works (except for clipping and scrollbar across the entire width)
<ListView ItemsSource="some rows"> <ListView.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="Fixed" Background="LightBlue" Width="300" SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical" /> <TextBlock Grid.Column="1" Text="Scrolled" Background="LightGreen" Width="300" /> </Grid> </DataTemplate> </ListView.ItemTemplate> </ListView>
it will not be:
<ScrollViewer HorizontalScrollBarVisibility="Auto"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="Fixed" Background="LightBlue" Width="300" SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical" /> <TextBlock Grid.Column="1" Text="Scrolled" Background="LightGreen" Width="300" /> </Grid> </ScrollViewer>
The reason is that DataGridHelper.FindVisualParent<ScrollViewer>(element) (see line 149 in the code ) in the SelectiveScrollingOrientation attached property fails, you may find workarounds, for example. create your own attached property with a copy of the source code, but get a ScrollViewer by name. Otherwise, I think you should do a lot of things from scratch.