Search code examples
c#wpfgridsplittergridlength

Why does this data binding not change the GridUnitType?


My goal is to save a GridSplitter position for later recall. The splitter is inside a Grid control that has three columns defined thusly:

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="{Binding GridPanelWidth, Mode=TwoWay}" />
    <ColumnDefinition Width="3" />  <!--splitter itself is in this column-->
    <ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>

The property GridPanelWidth is defined this way in the view model:

private GridLength _gridPanelWidth = new GridLength(1, GridUnitType.Star);
public GridLength GridPanelWidth
{
    get { return _gridPanelWidth; }
    set
    {
        if (_gridPanelWidth != value)
            SetProperty(ref _gridPanelWidth, value, () => GridPanelWidth);
    }
}

The problem I am having is that when the splitter is moved, the binding updates only the Double (Value) component of the bound property, but not the GridUnitType part of it.

Example: the property defaults to 1*. User drags the splitter and the value becomes 354*, instead of just 354. On restoring the value, then, it's huge (354 times, not 354 pixels).

Why is this happening, and what would you do about it?


Solution

  • <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="{Binding GridPanelWidth, Mode=TwoWay}" />
            <ColumnDefinition Width="4" />
            <!--splitter itself is in this column-->
            <ColumnDefinition x:Name="RightColumn" Width="2*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
    
        <Border
            BorderBrush="Gray"
            BorderThickness="1"
            Grid.Column="0"
            Grid.Row="0"
            />
    
        <GridSplitter
            Background="SteelBlue"
            ResizeBehavior="PreviousAndNext"
            ResizeDirection="Columns"
            VerticalAlignment="Stretch"
            HorizontalAlignment="Stretch"
            ShowsPreview="False"
            Grid.Column="1"
            Grid.Row="0"
            />
    
        <Border
            BorderBrush="Gray"
            BorderThickness="1"
            Grid.Column="2"
            Grid.Row="0"
            />
    
        <StackPanel
            Grid.Row="1"
            Grid.ColumnSpan="3"
            Grid.Column="0"
            >
            <TextBlock>
                <Run>GridPanelWidth: </Run>
                <Run Text="{Binding GridPanelWidth.Value, Mode=OneWay}" />
                <Run Text="{Binding GridPanelWidth.GridUnitType, Mode=OneWay}" />
            </TextBlock>
            <TextBlock>
                <Run>RightColumn.Width: </Run>
                <Run Text="{Binding Width.Value, ElementName=RightColumn, Mode=OneWay}" />
                <Run Text="{Binding Width.GridUnitType, ElementName=RightColumn, Mode=OneWay}" />
            </TextBlock>
        </StackPanel>
    </Grid>
    

    Screenshot 1:

    enter image description here

    Screenshot 2:

    enter image description here

    Screenshot 3:

    enter image description here

    Res ipsa loquitor, as far as I'm concerned, but just to be on the safe side:

    Because the parent may be resized, the grid splitter changes the ratio between the two columns, while preserving the GridUnitType.Star for each one so that when the parent is resized, the ratio will naturally remain constant. This preserves the intent of the initial Width values in the XAML.

    Width.Value for the left column turns out to be identical to the left Border's ActualWidth, and the same holds true for the right column. You'll have to grab both and save the ratio.

    Update

    I find Grid/GridSplitter a bit overengineered for everyday use when all I want is Yet Another Navigation Pane, so I recently wrote a SplitterControl that has two content properties and sets up the grid and splitter, with styling, in the template. I hadn't gotten around to making the split ratio persistent, so I did that just now.

    What I did was rather painful because the control is configurable, and the code isn't all that great, but I can share if you're interested.

    The business end is simple:

    When a column resizes, set a flag to block recursion and

    PaneRatio = _PART_ContentColumn.Width.Value / _PART_NavColumn.Width.Value;
    

    When PaneRatio changes, if it wasn't set by the column size change handler

    _PART_NavColumn.Width = new GridLength(1, GridUnitType.Star);
    _PART_ContentColumn.Width = new GridLength(PaneRatio, GridUnitType.Star);
    

    In practice, the navigator/content columns can be swapped, or they can be rows instead. Both of those are done by switching templates on a HeaderedContentControl that's a child of the split control template.