Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does WPF DataGrid get frozen rows/columns working?

I created a user control based on Grid (not DataGrid), which is wrapped in a ScrollViewer. Now I would like to have frozen rows/columns capability just like in DataGrid, but couldn't figure out how.

Can somebody give me some insight how it is done in WPF DataGrid?

like image 972
newman Avatar asked Dec 08 '12 04:12

newman


1 Answers

After having this problem by myself I want to share what I've found out so far.

DataGrid uses two different methods for that.


First: The RowHeader


This is the 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 DataGrid uses the SelectiveScrollingOrientation attached property to hold the RowHeader in position. If this property is set (or changing) it creates an adapted TranslateTransform bound to the parental ScrollViewer Offset for the element. See the details in source code.


Second: The FrozenColumns


This stuff takes place in DataGridCellsPanel ArrangeOverride(). It uses a private ArrangeState class "to maintain state between arrange 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;
}

it calls

ArrangeChild(children[childIndex] as UIElement, i, arrangeState);

for all realized childs and calculates the estimated width for non realized childs/columns.

double childSize = GetColumnEstimatedMeasureWidth(column, averageColumnWidth);
arrangeState.NextNonFrozenCellStart += childSize;

At the end the values will be set in the appropriate fields in 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);
    }
}

The details for ArrangeChild(UIElement child, int displayIndex, ArrangeState arrangeState) you can find from line 1470 in source code.


Conclusion


It's not as simple making columns are frozen. Even though this will work (apart from clipping and scrollbar over whole 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>

this will not:

<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 from line 149 in souce code) in SelectiveScrollingOrientation attached property fails. Maybe you find workarounds e.g. create your own attached property with a copy of the original code but get the ScrollViewer by name. Otherwise I think you have to do many things from scratch.

like image 200
LPL Avatar answered Sep 28 '22 14:09

LPL