Question

I have a WPF page, with a Grid on it.

There are three rows. Row 0 contains a GridView with Height="*". Row 1 contains a GridSplitter with Height="auto". Row 2 contains a details form with Height="2*".

Here's the thing - I have a button that is supposed to toggle the visibility of the details form. And that works just fine. Except that it just hides the form in Row 2, it doesn't expand the Grid in Row 0 to fill the space. What I want is for the button to toggle GridView in Row 0 to take up all the space, and then to toggle back to where things were.

Clearly playing around with the Visibility of the form inside the row won't accomplish what I want.

But what do I need to be playing around with?

Was it helpful?

Solution

I had to introduce an Attached Dependency Property to handle this in my own application:

<Grid c:GridSplitterController.Watch="{Binding ElementName=GS_DetailsView}">
    <Grid.RowDefinitions>
        <RowDefinition Height="1*" />
        <RowDefinition Height="200" />
    </Grid.RowDefinitions>

    <SomeControl Grid.Row="0" />

    <GridSplitter x:Name="GS_DetailsView"
                  Height="4"
                  Grid.Row="1"
                  VerticalAlignment="Top"
                  HorizontalAlignment="Stretch"
                  ResizeBehavior="PreviousAndCurrent"
                  ResizeDirection="Rows"
                  Visibility="{Binding ShowDetails,
                                       Converter={StaticResource boolvis}}" />

    <OtherControl Grid.Row="1"
                  Margin="0,4,0,0"
                  Visibility="{Binding ShowDetails,
                                       Converter={StaticResource boolvis}}" />
</Grid>

First define a suitable attached property on a DependencyObject:

public static GridSplitter GetWatch(DependencyObject obj)
{
    return (GridSplitter)obj.GetValue(WatchProperty);
}

public static void SetWatch(DependencyObject obj, GridSplitter value)
{
    obj.SetValue(WatchProperty, value);
}

public static readonly DependencyProperty WatchProperty =
    DependencyProperty.RegisterAttached(
        "Watch",
        typeof(GridSplitter),
        typeof(DependencyObject),
        new UIPropertyMetadata(null, OnWatchChanged));

Then listen for IsVisibleChanged:

private static void OnWatchChanged(DependencyObject obj,
    DependencyPropertyChangedEventArgs e)
{
    if (obj == null) return;
    if (obj is Grid)
    {
        var grid = obj as Grid;
        var gs = e.NewValue as GridSplitter;
        if (gs != null)
        {
            gs.IsVisibleChanged += (_sender, _e) =>
                {
                    UpdateGrid(
                        grid,
                        (GridSplitter)_sender,
                        (bool)_e.NewValue,
                        (bool)_e.OldValue);
                };
        }
    }
}

Once you are watching for these changes, you need to save or restore the GridLength values from the row or columns you are watching (for brevity I'm only including Rows):

// Given: static Dictionary<DependencyObject, GridLength> oldValues;
private static void UpdateGrid(Grid grid, GridSplitter gridSplitter, bool newValue, bool oldValue)
{
    if (newValue)
    {
        // We're visible again
        switch (gridSplitter.ResizeDirection)
        {
        case GridResizeDirection.Columns:
            break;
        case GridResizeDirection.Rows:
            int ridx = (int)gridSplitter.GetValue(Grid.RowProperty);
            var prev = grid.RowDefinitions.ElementAt(GetPrevious(gridSplitter, ridx));
            var curr = grid.RowDefinitions.ElementAt(GetNext(gridSplitter, ridx));
            if (oldValues.ContainsKey(prev) && oldValues.ContainsKey(curr))
            {
                prev.Height = oldValues[prev];
                curr.Height = oldValues[curr];
            }

            break;
        }
    }
    else
    {
        // We're being hidden
        switch (gridSplitter.ResizeDirection)
        {
        case GridResizeDirection.Columns:
            break;
        case GridResizeDirection.Rows:
            int ridx = (int)gridSplitter.GetValue(Grid.RowProperty);
            var prev = grid.RowDefinitions.ElementAt(GetPrevious(gridSplitter, ridx));
            var curr = grid.RowDefinitions.ElementAt(GetNext(gridSplitter, ridx));
            switch (gridSplitter.ResizeBehavior)
            {
                // Naively assumes only one type of collapsing!
                case GridResizeBehavior.PreviousAndCurrent:
                    oldValues[prev] = prev.Height;
                    prev.Height = new GridLength(1.0, GridUnitType.Star);

                    oldValues[curr] = curr.Height;
                    curr.Height = new GridLength(0.0);
                    break;
            }
            break;
        }
    }
}

All that is left is a suitable implementation of GetPrevious and GetNext:

private static int GetPrevious(GridSplitter gridSplitter, int index)
{
    switch (gridSplitter.ResizeBehavior)
    {
        case GridResizeBehavior.PreviousAndNext:
        case GridResizeBehavior.PreviousAndCurrent:
            return index - 1;
        case GridResizeBehavior.CurrentAndNext:
            return index;
        case GridResizeBehavior.BasedOnAlignment:
        default:
            throw new NotSupportedException();
    }
}

private static int GetNext(GridSplitter gridSplitter, int index)
{
    switch (gridSplitter.ResizeBehavior)
    {
        case GridResizeBehavior.PreviousAndCurrent:
            return index;
        case GridResizeBehavior.PreviousAndNext:
        case GridResizeBehavior.CurrentAndNext:
            return index + 1;
        case GridResizeBehavior.BasedOnAlignment:
        default:
            throw new NotSupportedException();
    }
}

OTHER TIPS

In case anyone wants a purely XAML solution, I was able to conjure a way to hide the splitter and relevant row using styles, setters, and triggers.

I used a static resource for my style, which was set to change the Height and MaxHeight when a specific bound boolean was set.

<Style x:Key="showRow" TargetType="{x:Type RowDefinition}">
  <Style.Setters>
    <Setter Property="Height" Value="*"/>
  </Style.Setters>
  <Style.Triggers>
    <DataTrigger Binding="{Binding MyShowRowBool}" Value="False">
      <DataTrigger.Setters>
        <Setter Property="Height" Value="0"/>
        <Setter Property="MaxHeight" Value="0"/>
      </DataTrigger.Setters>
    </DataTrigger>
  </Style.Triggers>
</Style>

I simply applied the style to the relevant row definitions, and it worked like a charm:

<Grid.RowDefinitions>
  <RowDefinition Height="*"/>
  <RowDefinition Style="{StaticResource showRow}"/>
  <RowDefinition Style="{StaticResource showRow}"/>
</Grid.RowDefinitions>

Notable is that I tried it without the MaxHeight property, and it wasn't collapsing properly. Adding it in seems to have done the trick for me.

Let's suppose I have this XAML layout:

  <Grid Name="MyGrid">
      <Grid.RowDefinitions>
          <RowDefinition />
          <RowDefinition Height="Auto" />
          <RowDefinition />
      </Grid.RowDefinitions>
      <MyControl1 ... Grid.Row="0" />
      <GridSplitter Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Stretch" ShowsPreview="True" Height="5" />
      <MyControl2 ... Grid.Row="2" />
  </Grid>

Then I can hide the second control (collapse the splitter down) with this code (equivalent of setting Height="0" in XAML):

  MyGrid.RowDefinitions[2].Height = new GridLength(0);

And uncollapse it with this code (equivalent of setting Height="1*" in XAML, which is the default for a RowDefinition):

  MyGrid.RowDefinitions[2].Height = new GridLength(1, GridUnitType.Star);

This is what the splitter does undercovers when the user moves it.

This GridExpander control that inherits form GridSpliter that might to the job you are looking for. Credit goes to Shemesh for writing the original Silverlight version that I adapted for my own usage in WPF. I find myself wanting this functionality almost everywhere I attempt to use GridSplitter so it can be quite handy.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top