wpfbindingvisibilitydatatrigger

wpf toggling visibility collapse only works once


What i want to do is collapse the bottom section of the WPF ui based on the checkbox toggle. This mostly works as expected so far. Below as you step through the images you'll see the collapse stops working once the grid splitter is moved. I do not understand why or how to fix this.

Start of the application, appears correct.

Initial opening of the application

Toggle the checkbox and the botttom section disappears as expected.

enter image description here

Toggle the checkbox once more and then move the splitter vertically, everything appears as expected.

enter image description here

Now toggle the checkbox one last time and you'll notice the top section doesn't fill the application like it once did before. This is where it appears broken! I would expect the yellow section to fill the UI like it did in the initial toggle.

enter image description here

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="250" Width="525"
        WindowStartupLocation="CenterScreen">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <CheckBox Name="ToggleVisibility" Margin="10" IsChecked="True" Content="Toggle Bottom Section"></CheckBox>

        <StackPanel Background="#feca00" Grid.Row="1">
            <TextBlock FontSize="20" Foreground="#58290A">Top Section</TextBlock>
        </StackPanel>

        <GridSplitter Grid.Row="2" Height="5" HorizontalAlignment="Stretch">
            <GridSplitter.Style>
                <Style TargetType="GridSplitter">
                    <Setter Property="Visibility" Value="Visible" />
                    <Setter Property="Background" Value="Red" />
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding ElementName=ToggleVisibility, Path=IsChecked}" Value="False">
                            <Setter Property="Visibility" Value="Collapsed"></Setter>
                            <Setter Property="Background" Value="Red"></Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </GridSplitter.Style>
        </GridSplitter>

        <StackPanel Grid.Row="3" Background="LightBlue">
            <StackPanel.Style>
                <Style TargetType="StackPanel">
                    <Setter Property="Visibility" Value="Visible" />
                    <Setter Property="Background" Value="Red" />
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding ElementName=ToggleVisibility, Path=IsChecked}" Value="False">
                            <Setter Property="Visibility" Value="Collapsed"></Setter>
                            <Setter Property="Background" Value="Red"></Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </StackPanel.Style>

            <TextBlock FontSize="20" Foreground="black">Bottom Section</TextBlock>
        </StackPanel>

    </Grid>
</Window>

Solution

  • The reason why this happens is that when you move the splitter it overwrites the Height property of the last RowDefinition and sets it to actual height of whatever is presented in the according row of the Grid - the second StackPanel (bottom section) in your case. It no longer is set to Auto, so even if you collapse the bottom section the row maintains it's height - hence the empty space.

    I sometimes find myself in a similar situation, and what I do then is to span the control, which I want to take up all the space, across the remaining rows using Grid.RowSpan attached property. In your case you could achieve it by applying the following style to your first StackPanel (top section):

    <Style TargetType="{x:Type StackPanel}">
        <Style.Triggers>
            <DataTrigger Binding="{Binding IsChecked, ElementName=ToggleVisibility}" Value="False">
                <Setter Property="Grid.RowSpan" Value="3" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
    

    Then the StackPanel will span to the bottom of the Grid regardless of the following rows' heights.

    EDIT

    Since you're not satisfied with spanning the control, here's another solution: derive from RowDefinition class and wire your own visibility logic. Here's an example:

    public class MyRowDefinition : RowDefinition
    {
        static MyRowDefinition()
        {
            HeightProperty.OverrideMetadata(
                typeof(MyRowDefinition),
                new FrameworkPropertyMetadata
                {
                    CoerceValueCallback = CoerceHeightPropertyValue
                });
        }
    
        private static object CoerceHeightPropertyValue(DependencyObject d, object baseValue)
        {
            if (Equals(d.GetValue(IsVisibleProperty), false))
                return new GridLength(0d);
            else
                return baseValue;
        }
    
        public bool IsVisible
        {
            get { return (bool)GetValue(IsVisibleProperty); }
            set { SetValue(IsVisibleProperty, value); }
        }
    
        public static readonly DependencyProperty IsVisibleProperty =
            DependencyProperty.Register(
                "IsVisible",
                typeof(bool),
                typeof(MyRowDefinition),
                new FrameworkPropertyMetadata
                {
                    DefaultValue = true,
                    PropertyChangedCallback = IsVisiblePropertyChanged
                });
    
        private static void IsVisiblePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            d.InvalidateProperty(RowDefinition.HeightProperty);
        }
    }
    

    Then use that class instead of RowDefinition in your grid and bind the IsVisible property to the check state of the checkbox.