Dynamically change the layout of the WPF app to show one or two components on the grid

92 Views Asked by At

I have a WPF app with a main view of a treelist and I also want to be able to open a second component when I press on a button (and hide it when pressed again).

But I cannot make the second component to take the needed space and the tree view the rest, without overlapping.

In XAML, I declared a grid with 5 rows as follows:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="50"/>
        <RowDefinition />
        <RowDefinition Height="auto"/>
        <RowDefinition />
        <RowDefinition Height="35" />
    </Grid.RowDefinitions>

    <views:InputsControl Grid.Row="0"/>

    <views:TreeViewControl Grid.Row="1" />

    <GridSplitter Grid.Row="2"
          HorizontalAlignment="Stretch"
          Height="3" 
          Visibility="{Binding LoggerVisibility}" >
    </GridSplitter>

    <TextBox Grid.Row="3"
             BorderThickness="1"
             Margin="2 ,0, 2, 2"
             IsReadOnly="True"
             Visibility="{Binding LoggerVisibility}">
        Some logger info
    </TextBox>

    <views:StatusBar Grid.Row="4"/>
</Grid>

ViewModel:

private Visibility _LoggerVisibility = Visibility.Collapsed;
public Visibility LoggerVisibility
{
    get => _LoggerVisibility;
    set
    {
        _LoggerVisibility = value;
        OnPropertyChanged(nameof(LoggerVisibility));
    }
}

private void OnPropertyChanged(string propertyName) =>
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

public MainWindowViewModel()
{
    Messenger.Default.Register<Actions>(this, ProcessAction);
}

private void ProcessAction(Actions action)
{
    if (action == Actions.ToggleLogger)
    {
        if (LoggerVisibility == Visibility.Collapsed)
        {
            LoggerVisibility = Visibility.Visible;
        }
        else if (LoggerVisibility == Visibility.Visible)
        {
            LoggerVisibility = Visibility.Collapsed;
        }
    }
}

Here is how the app looks like:

when it's first run enter image description here

after the first button click enter image description here

after the second button click enter image description here

So I want it to go the the layout in the first photo after the second click.

I noticed that I can get my desired behavior, in separate steps, if I change the row span for the tree list view row. If the logger is visible, the code from above works perfectly fine. If the logger is closed and I add Grid.RowSpan = "3" to the treelist row, is also looks good. So I figured I can bind the value of the rowspan in code behind and keep it synced with LoggerVisibility which is also toggled in code behind when the button is pressed.

But for some reason, the rowspan is not updated. I supposed the way I bind it is correct, since I did the same thing for the LoggerVisibility binding, and it does its job and the logger can indeed be collapsed or visible when the button is pressed.

Can you, please, help me?

1

There are 1 best solutions below

2
BionicCode On BEST ANSWER

The purpose of the GridSpliter is to redistribute space between rows/columns. It does this by changing the size of its adjacent rows/columns. In other words, the GridSplitter overwrites the original values of the RowDefinition or ColumnDefinition. This means you have to backup the original GridLength values and restore them if the GridSplitter is collapsed.

The following example shows a simple attached behavior to handle this by setting/binding the GridSplitterService.IsVisible attached property on the GridSplitter.
Because we can now introduce a boolean property to toggle the visibility from the view model the solution becomes cleaner in terms of MVVM.

ViewModel.cs

private bool isLoggerEnabled;
public bool IsLoggerEnabled
{
    get => this.isLoggerEnabled;
    set
    {
        this.isLoggerEnabled = value;
        OnPropertyChanged(nameof(IsLoggerEnabled));
    }
}

private void ProcessAction(Actions action)
{
  if (action == Actions.ToggleLogger)
  {
    // Simply toggle the boolean value using XOR
    this.IsLoggerEnabled ^= true;     
  }
}

MainWindow.xaml

<Grid>
  <Grid.Resources>

    <!-- use the built-in BooleanToVisibilityConverter -->
    <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
  </Grid.Resources>

  <Grid.RowDefinitions>
    <RowDefinition Height="50"/>
    <RowDefinition />
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="35" />
  </Grid.RowDefinitions>

  <views:InputsControl Grid.Row="0"/>

  <views:TreeViewControl Grid.Row="1" />

  <GridSplitter Grid.Row="2"
                HorizontalAlignment="Stretch"
                Height="3" 
                GridSplitterService.IsVisible="{Binding IsLoggerEnabled}" />

  <!-- Consider to use the lightweight TextBlock instead -->
  <TextBox Grid.Row="3"
           BorderThickness="1"
           Margin="2 ,0, 2, 2"
           IsReadOnly="True"
           Visibility="{Binding IsLoggerEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"
           Text="Some logger info" />

  <views:StatusBar Grid.Row="4"/>
</Grid>

GridSplitterService.cs

public class GridSplitterService : DependencyObject
{
  class BackupInfo
  {
    public BackupInfo(GridLength adjacentPreviousGridLength, GridLength adjacentNextGridLength, DefinitionBase adjacentPreviousGridElementDefinition, DefinitionBase adjacentNextGridElementDefinition)
    {
      this.AdjacentPreviousGridLength = adjacentPreviousGridLength;
      this.AdjacentNextGridLength = adjacentNextGridLength;
      this.AdjacentPreviousGridElementDefinition = adjacentPreviousGridElementDefinition;
      this.AdjacentNextGridElementDefinition = adjacentNextGridElementDefinition;
    }

    public void RestoreAdjacentColumns()
    {
      ((ColumnDefinition)this.AdjacentPreviousGridElementDefinition).Width = this.AdjacentPreviousGridLength;
      ((ColumnDefinition)this.AdjacentNextGridElementDefinition).Width = this.AdjacentNextGridLength;
    }

    public void RestoreAdjacentRows()
    {
      ((RowDefinition)this.AdjacentPreviousGridElementDefinition).Height = this.AdjacentPreviousGridLength;
      ((RowDefinition)this.AdjacentNextGridElementDefinition).Height = this.AdjacentNextGridLength;
    }

    public GridLength AdjacentPreviousGridLength { get; }
    public GridLength AdjacentNextGridLength { get; }
    public DefinitionBase AdjacentPreviousGridElementDefinition { get; }
    public DefinitionBase AdjacentNextGridElementDefinition { get; }
  }

  public static bool GetIsVisible(GridSplitter attachingElement) 
    => (bool)attachingElement.GetValue(IsVisibleProperty);

  public static void SetIsVisible(GridSplitter attachingElement, bool value) 
    => attachingElement.SetValue(IsVisibleProperty, value);

  public static readonly DependencyProperty IsVisibleProperty = DependencyProperty.RegisterAttached(
    "IsVisible", 
    typeof(bool), 
    typeof(GridSplitterService), 
    new PropertyMetadata(default(bool), OnIsVisibleChanged));

  private static ConditionalWeakTable<GridSplitter, BackupInfo> OriginalGridLengthMap { get; }
    = new ConditionalWeakTable<GridSplitter, BackupInfo>();

  private static void OnIsVisibleChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
  {
    if (attachingElement is not GridSplitter gridSplitter)
    {
      throw new ArgumentException($"Attaching element must be of type {typeof(GridSplitter).FullName}.", nameof(attachingElement));
    }

    bool isResizingRows = gridSplitter.ResizeDirection is GridResizeDirection.Rows
      || gridSplitter.HorizontalAlignment is HorizontalAlignment.Stretch;
    var host = (Grid)gridSplitter.Parent;

    if (!GridSplitterService.OriginalGridLengthMap.TryGetValue(gridSplitter, out _))
    {
      StoreOriginalGridLengthBeforeGridSplitterResize(gridSplitter, isResizingRows, host);
    }

    bool isVisible = (bool)e.NewValue;
    if (isVisible)
    {
      gridSplitter.Visibility = Visibility.Visible;
    }
    else if (GridSplitterService.OriginalGridLengthMap.TryGetValue(gridSplitter, out BackupInfo backupInfo))
    {
      gridSplitter.Visibility = Visibility.Collapsed;

      if (isResizingRows)
      {
        backupInfo.RestoreAdjacentRows();
      }
      else
      {
        backupInfo.RestoreAdjacentColumns();
      }
    }
  }

  private static void StoreOriginalGridLengthBeforeGridSplitterResize(GridSplitter gridSplitter, bool isResizingRows, Grid host)
  {
    GridLength originalAdjacentPreviousGridLength;
    GridLength originalAdjacentNextGridLength;
    DefinitionBase originalAdjacentPreviousGridElementDefinition;
    DefinitionBase originalAdjacentNextGridElementDefinition;

    if (isResizingRows)
    {
      int gridSplitterRowIndex = Grid.GetRow(gridSplitter);
      int gridSplitterTopRowIndex = gridSplitterRowIndex - 1;
      int gridSplitterBottomRowIndex = gridSplitterRowIndex + 1;

      originalAdjacentPreviousGridElementDefinition = host.RowDefinitions[gridSplitterTopRowIndex];
      originalAdjacentPreviousGridLength = ((RowDefinition)originalAdjacentPreviousGridElementDefinition).Height;
      originalAdjacentNextGridElementDefinition = host.RowDefinitions[gridSplitterBottomRowIndex];
      originalAdjacentNextGridLength = ((RowDefinition)originalAdjacentNextGridElementDefinition).Height;
    }
    else
    {
      int gridSplitterColumnIndex = Grid.GetColumn(gridSplitter);
      int gridSplitterLeftColumnIndex = gridSplitterColumnIndex - 1;
      int gridSplitterRigthColumnIndex = gridSplitterColumnIndex + 1;

      originalAdjacentPreviousGridElementDefinition = host.ColumnDefinitions[gridSplitterLeftColumnIndex];
      originalAdjacentPreviousGridLength = ((ColumnDefinition)originalAdjacentPreviousGridElementDefinition).Width;
      originalAdjacentNextGridElementDefinition = host.ColumnDefinitions[gridSplitterRigthColumnIndex];
      originalAdjacentNextGridLength = ((ColumnDefinition)originalAdjacentNextGridElementDefinition).Width;
    }

    var backupInfo = new BackupInfo(originalAdjacentPreviousGridLength, originalAdjacentNextGridLength, originalAdjacentPreviousGridElementDefinition, originalAdjacentNextGridElementDefinition);
    GridSplitterService.OriginalGridLengthMap.Add(gridSplitter, backupInfo);
  }
}