WPF: collapse GridSplitter?
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.
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(); }}