Search code examples
c#wpfwpf-controlswpf-grid

How to reverse the order of columns in WPF Grid layout?


I am building a WPF application in C#. I am trying to implement a two column layout using Grid layout, similar to a browser sidebar.

Grid is implemented as a 3-column layout because I am using a GridSplitter to easily change the width of the sidebar.

The sidebar can be collapsed, so the Style is set up with reference to the following so that it can be collapsed without problems after using GridSplitter.
WPF: collapse GridSplitter?

By default, the sidebar is placed on the right, but I would like to reverse the order dynamically in code.

MainContent | GridSplitter | Sidebar <----> Sidebar | GridSplitter | MainContent

MainWindow.xaml

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        d:DataContext="{d:DesignInstance local:MainWindow}">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <!--Main Column-->
            <ColumnDefinition Width="*" />
            <!--GridSplitter -->
            <ColumnDefinition Width="Auto" />

            <!--Collapsable Sidebar Column -->
            <!--Can collapse entirely even if GridSplitter is used -->
            <!--ref: https://stackoverflow.com/questions/12483017/wpf-collapse-gridsplitter-->
            <ColumnDefinition>
                <ColumnDefinition.Style>
                    <Style TargetType="{x:Type ColumnDefinition}">
                        <Style.Setters>
                            <Setter Property="Width" Value="200"/>
                        </Style.Setters>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding ShowSidebar}" Value="False">
                                <DataTrigger.Setters>
                                    <Setter Property="Width" Value="0"/>
                                    <Setter Property="MaxWidth" Value="0"/>
                                </DataTrigger.Setters>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ColumnDefinition.Style>
            </ColumnDefinition>
        </Grid.ColumnDefinitions>

        <!--Main Column-->
        <Grid Grid.Column="0" Background="White">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>

            <StackPanel Orientation="Horizontal">
                <Button Content="Toggle sidebar" Click="ToggleSidebarButton_OnClick" />
                <Button Content="Swap sidebar" Click="SwapSidebarButton_OnClick" />
            </StackPanel>

            <TextBlock
                Grid.Row="1"
                Text="Main Content"
                FontSize="32"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"/>
        </Grid>

        <GridSplitter
            Grid.Column="1"
            Visibility="{Binding ShowSidebar, Converter={StaticResource BooleanToVisibilityConverter}}"
            Width="5"
            ShowsPreview="True"
            ResizeBehavior="PreviousAndNext"
            ResizeDirection="Columns" />

        <!--Sidebar Column-->
        <Grid
            Grid.Column="2"
            Visibility="{Binding ShowSidebar, Converter={StaticResource BooleanToVisibilityConverter}}"
            Background="Gray">

            <TextBlock
                Text="Sidebar"
                FontSize="32"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"/>
        </Grid>
    </Grid>
</Window>

MainWindow.xaml.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private bool _showSidebar = true;
        public bool ShowSidebar
        {
            get => _showSidebar;
            set => SetField(ref _showSidebar, value);
        }

        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;
        }

        private void ToggleSidebarButton_OnClick(object sender, RoutedEventArgs e)
        {
            ShowSidebar = !ShowSidebar;
        }

        private void SwapSidebarButton_OnClick(object sender, RoutedEventArgs e)
        {
            // TODO: How to swap the sidebar programmatically?
            throw new NotImplementedException();
        }

        #region IPC
        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
            field = value;
            OnPropertyChanged(propertyName);
            return true;
        }
        #endregion

    }
}

The TODO part is written in code-behind, but I would actually like to implement it in ViewModel with MVVM.

I have confirmed that it is possible to implement this by using 5-column layout as shown below, implemented by creating two variables ShowSidebarRight and ShowSidebarLeft, placing two SidebarView and GridSplitter each, and controlling the display with Visibility.

but I would like to implement it with only one instance of View since it would create an extra instance of SidebarView and ViewModel, which would cause performance concerns and redundancy in the code.

        <Grid.ColumnDefinitions>
            <!--Left Sidebar -->
            <ColumnDefinition>
                <ColumnDefinition.Style>
                    <Style TargetType="{x:Type ColumnDefinition}">
                        <Style.Setters>
                            <Setter Property="Width" Value="200"/>
                        </Style.Setters>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding ShowSidebarLeft}" Value="False">
                                <DataTrigger.Setters>
                                    <Setter Property="Width" Value="0"/>
                                    <Setter Property="MaxWidth" Value="0"/>
                                </DataTrigger.Setters>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ColumnDefinition.Style>
            </ColumnDefinition>

            <!--Left GridSplitter -->
            <ColumnDefinition Width="Auto" />
            <!--Main Column-->
            <ColumnDefinition Width="*" />
            <!--Right GridSplitter -->
            <ColumnDefinition Width="Auto" />
            <!--Right Sidebar -->
            <ColumnDefinition>
                <ColumnDefinition.Style>
                    <Style TargetType="{x:Type ColumnDefinition}">
                        <Style.Setters>
                            <Setter Property="Width" Value="200"/>
                        </Style.Setters>
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding ShowSidebarRight}" Value="False">
                                <DataTrigger.Setters>
                                    <Setter Property="Width" Value="0"/>
                                    <Setter Property="MaxWidth" Value="0"/>
                                </DataTrigger.Setters>
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </ColumnDefinition.Style>
            </ColumnDefinition>
        </Grid.ColumnDefinitions>

Solution

  • You can reverse the horizontal layout order by setting the FlowDirection property:

    <Grid>
        <Grid.Style>
            <Style TargetType="Grid">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding SideBarLeft}" Value="True">
                        <Setter Property="FlowDirection" Value="RightToLeft"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Grid.Style>
    
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="200"/>
        </Grid.ColumnDefinitions>
    
        <Grid Grid.Column="0" FlowDirection="LeftToRight">
            <TextBlock Text="Main Content"/>
        </Grid>
    
        <Grid Grid.Column="2" FlowDirection="LeftToRight">
            <TextBlock Text="Sidebar"/>
        </Grid>
    
        <GridSplitter
            Grid.Column="1"
            Width="5"
            ShowsPreview="True"
            ResizeBehavior="PreviousAndNext"
            ResizeDirection="Columns" />
    </Grid>