Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF: Compact Navigation Menu

0.00/5 (No votes)
3 Aug 2020 1  
Creating a compact navigation menu using a ListBox
Learn how to create a compact navigation menu for a WPF application using a ListBox and without any code-behind
Image 1

Introduction

Navigation menus can enhance the quality of the user experience of your WPF application but how do you go about adding a compact navigation menu without using a third party control or the UWP NavigationView? In this article, I'll go over how you can create one using a ListBox. All the functionality required to create the navigation menu will be done using XAML only; so no code-behind, commands or frameworks.

Background

The sample application for this article has three views which the user can switch between: HomeView, EmailView and CloudView. The views are user controls and don't have much content except for an icon and text indicating which view is which.

<UserControl x:Class="CompactNavigationMenu.Views.EmailView"

            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 

            xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 

            xmlns:local="clr-namespace:CompactNavigationMenu.Views"

            xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"

            xmlns:vm="clr-namespace:CompactNavigationMenu.ViewModels"

            mc:Ignorable="d" 

            d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.DataContext>
        <vm:EmailViewModel/>
    </UserControl.DataContext>

    <Grid>
        <StackPanel Orientation="Vertical"

                    HorizontalAlignment="Center"

                    VerticalAlignment="Center">
            <iconPacks:PackIconMaterialDesign HorizontalAlignment="Center"

                                            Foreground="{StaticResource PrimaryDarkBrush}"

                                            Width="100" Height="100" Kind="Email"/>

            <TextBlock FontSize="40" FontWeight="Bold" 

                         VerticalAlignment="Center" HorizontalAlignment="Center"

                         Foreground="{StaticResource PrimaryDarkBrush}"

                         Text="{Binding Title}"/>
        </StackPanel>
    </Grid>
</UserControl>

Navigation Menu

A compact navigation menu only displays icons, which the user should be able to easily interpret. As stated in the introduction, I'll highlight how to make a navigation menu using a ListBox but first let's take a look at the layout of the MainWindow.

<Window x:Class="CompactNavigationMenu.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"

        mc:Ignorable="d"

        Height="400" Width="600" Background="#FFEAEBEC"

        Title="Nav" WindowStartupLocation="CenterScreen">

        ...

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <ListBox x:Name="NavigationMenuListBox"

                Style="{StaticResource NavigationListBoxStyle}"

                ItemContainerStyle="{DynamicResource NavigationListBoxItemStyle}"

                ItemTemplate="{DynamicResource NavigationDataTemplate}"

                ItemsSource="{StaticResource NavigationMenuItems}"/>

        <ContentControl Grid.Column="1" Style="{StaticResource NavigationContentStyle}"/>
    </Grid>
</Window>    

The Window has a Grid with two columns: The first column hosts the ListBox that acts as the navigation menu while the second column hosts a ContentControl where the three views are switched.

To get the ListBox to act as a suitable menu, a custom ListBox style is required.

<Style x:Key="NavigationListBoxStyle" TargetType="{x:Type ListBox}">
    <Setter Property="Background" Value="{StaticResource PrimaryDarkBrush}"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="HorizontalContentAlignment" Value="Center"/>
    <Setter Property="SelectedIndex" Value="0"/>
    <Setter Property="Width" Value="Auto"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBox}">
                <Border Background="{TemplateBinding Background}"

                        BorderThickness="0"

                        Padding="0"

                        SnapsToDevicePixels="true">
                    <ScrollViewer Padding="{TemplateBinding Padding}"

                                    Focusable="false">
                        <ItemsPresenter SnapsToDevicePixels=
                                    "{TemplateBinding SnapsToDevicePixels}"/>
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>    

By default, the Border that makes up the ControlTemplate of a ListBox has its Padding and BorderThickness set to a value of 1. This has the unintended consequence of creating a slight gap between a ListBoxItem and the edges of the ListBox.

Image 2

In the image above, you can notice a slight gap between the selected item, i.e., the first item, and the edges of the ListBox in its default state. The gap can be partially eliminated by setting the value of the Padding of the Border in the ControlTemplate to zero.

A custom ListBoxItem style is also required to completely eliminate the gap and to give ListBoxItems a custom look when in various states; like a blue Background and white Foreground when selected.

<Style x:Key="NavigationListBoxItemStyle" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Foreground" Value="{StaticResource PrimaryLightBrush}"/>
    <Setter Property="Margin" Value="-1"/>
    <Setter Property="ToolTip" Value="{Binding}"/>
    <Setter Property="HorizontalContentAlignment" 

            Value="{Binding HorizontalContentAlignment, 
            RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
    <Setter Property="VerticalContentAlignment" 

            Value="{Binding VerticalContentAlignment, 
            RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <Border x:Name="Bd"

                        BorderBrush="{TemplateBinding BorderBrush}"

                        BorderThickness="{TemplateBinding BorderThickness}" 

                        Background="{TemplateBinding Background}"

                        Padding="{TemplateBinding Padding}"

                        SnapsToDevicePixels="true">
                    <ContentPresenter HorizontalAlignment=
                                    "{TemplateBinding HorizontalContentAlignment}"

                                    VerticalAlignment=
                                    "{TemplateBinding VerticalContentAlignment}"

                                    SnapsToDevicePixels=
                                    "{TemplateBinding SnapsToDevicePixels}"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsSelected" Value="true">
                        <Setter TargetName="Bd" Property="Background" 

                         Value="{StaticResource PrimaryLightBrush}"/>
                        <Setter Property="Foreground" Value="White"/>
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                            <Condition Property="Selector.IsSelectionActive" Value="false"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Background" TargetName="Bd" 

                         Value="{StaticResource PrimaryLightBrush}"/>
                        <Setter Property="Foreground" Value="White"/>
                    </MultiTrigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" 

                         Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>    

Notice I've set the Margin of the ListBoxItem to -1. This completely eliminates the gap between the ListBoxItem and the edges of the ListBox.

A DataTemplate is required for the ListBox ItemTemplate. The DataTemplate will contain the icon displayed in a ListBoxItem.

<DataTemplate x:Key="NavigationDataTemplate">
    <iconPacks:PackIconMaterialDesign x:Name="MenuItemIcon" VerticalAlignment="Center" 

                                        HorizontalAlignment="Center" Margin="12"/>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding}" Value="Home">
            <Setter TargetName="MenuItemIcon" Property="Kind" Value="Home"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding}" Value="Email">
            <Setter TargetName="MenuItemIcon" Property="Kind" Value="Email"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding}" Value="Cloud">
            <Setter TargetName="MenuItemIcon" Property="Kind" Value="Cloud"/>
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>    

I'm using a DataTrigger to change the icon based on a binding which is of type string. You may recall that the ListBox's ItemsSource property was bound to a StaticResource named NavigationMenuItems. The resource is just a collection of strings.

<x:Array x:Key="NavigationMenuItems" Type="system:String">
    <system:String>Home</system:String>
    <system:String>Email</system:String>
    <system:String>Cloud</system:String>
</x:Array>    

And that's all that's required to get the ListBox appropriately set up. The ContentControl where the views are switched only requires some minor styling to play along with the ListBox.

<Style x:Key="NavigationContentStyle" TargetType="ContentControl">
    <Setter Property="ContentTemplate" Value="{StaticResource HomeViewTemplate}"/>
    <Style.Triggers>
        <DataTrigger Binding="{Binding ElementName=NavigationMenuListBox, Path=SelectedItem}"

                     Value="Email">
            <Setter Property="ContentTemplate" Value="{StaticResource EmailViewTemplate}"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding ElementName=NavigationMenuListBox, Path=SelectedItem}" 

                     Value="Cloud">
            <Setter Property="ContentTemplate" Value="{StaticResource CloudViewTemplate}"/>
        </DataTrigger>
    </Style.Triggers>
</Style>          

Views are switched based on the value of the ListBox's SelectedItem. The ContentTemplate of the ContentControl can be either one of three DataTemplates.

<DataTemplate x:Key="HomeViewTemplate">
    <views:HomeView/>
</DataTemplate>

<DataTemplate x:Key="EmailViewTemplate">
    <views:EmailView/>
</DataTemplate>

<DataTemplate x:Key="CloudViewTemplate">
    <views:CloudView/>
</DataTemplate>        

Conclusion

That's it, I hope you've gained some useful knowledge from this article. If you prefer taking another approach, you can look at the other available options which I mentioned in the introduction.

History

  • 4th August, 2020: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here