Monthly Archives: July 2021

Navigating in a WinUI 3 Desktop application

In this article we describe a minimal framework for a navigation service in a WinUI 3 Desktop application on top of a NavigationView control. We will cover

  • navigating from a menu item to an application page,
  • navigating to a menu item from code behind,
  • retrieving the current menu item,
  • hiding and showing menu items, and
  • dynamically adding menu items.

Our purpose is to describe the interaction between some of the core classes and come up with a pattern that you can reuse in your own WinUI 3 apps. For that reason we deliberately stay away from MVVM and Dependency Injection libraries. We created a small sample app, here’s how it looks like:

Werchter

The app is a WinUI 3 Desktop app built on top of the new Windows App SDK v0.8 (previously known as Project Reunion) with the regular Visual Studio 2019 (no preview stuff needed). From their own documentation, we learn that 

the Windows App SDK is a set of new developer components and tools that represent the next evolution in the Windows app development platform. The Windows App SDK provides a unified set of APIs and tools that can be used in a consistent way by any desktop app on Windows 11 and downlevel to Windows 10, version 1809,

and

Windows UI Library (WinUI) 3 is a native user experience (UX) framework for building modern Windows apps. It ships independently from the Windows operating system as a part of Project Reunion (now called the Windows App SDK). The 0.8 Preview release provides Visual Studio project templates to help you start building apps with a WinUI 3-based user interface.

Check this link on how to prepare your development environment for this. When all prerequisites are met, you should be able to create new WinUI 3 projects:

ReunionTemplates

UWP developers will feel at home in a WinUI 3 Desktop application: it looks like UWP and it feels like UWP, except that

  • the solution currently (and temporarily) comes with a separate MSIX installation project, and
  • the main page (our Shell) is not a Page but a Window – one that supports both UWP and Win32 app models.

The main beef of our Shell Page Window is the WinUI 3 version of the NavigationView control. In the first releases of UWP we developers needed to create a navigation UI from scratch. In a couple of years modern XAML navigation UI evolved from DIY SplitView-based implementations (been there, done that) to simply putting a full fledged NavigationView control on the main page. NavigationView comes with different modes (left menu/top menu), built-in adaptive behavior, two-level hierarchical menu structure, footer menu items, back button support, animations, different icon types, … Apart from the menu, the control also comes with a Header, and a Frame to host the application pages.

Here’s the main structure of the Shell page in our sample app:

<NavigationView x:Name="NavigationView"
                Loaded="NavigationView_Loaded"
                SelectionChanged="NavigationView_SelectionChanged" 
                Header="WinUI 3 Navigation Sample"
                IsBackButtonVisible="Collapsed"
                IsSettingsVisible="False">
    <NavigationView.MenuItems>
        <NavigationViewItem Content="Home"
                            Tag="XamlBrewer.WinUI3.Navigation.Sample.Views.HomePage"
                            ToolTipService.ToolTip="Home">
            <NavigationViewItem.Icon>
                <BitmapIcon UriSource="/Assets/Home.png"
                            ShowAsMonochrome="False" />
            </NavigationViewItem.Icon>
        </NavigationViewItem>
        <!-- More items -->
    </NavigationView.MenuItems>
    <NavigationView.FooterMenuItems>
        <NavigationViewItem Content="About"
                            Tag="XamlBrewer.WinUI3.Navigation.Sample.Views.AboutPage">
            <NavigationViewItem.Icon>
                <BitmapIcon UriSource="/Assets/About.png"
                            ShowAsMonochrome="False" />
            </NavigationViewItem.Icon>
        </NavigationViewItem>
    </NavigationView.FooterMenuItems>
    <Frame x:Name="ContentFrame" />
</NavigationView>

The Festivals item demonstrates hierarchical navigation using nested menu items:

<NavigationViewItem Content="Festivals"
                    Tag="XamlBrewer.WinUI3.Navigation.Sample.Views.FestivalPage"
                    ToolTipService.ToolTip="Festivals">
    <NavigationViewItem.MenuItems>
        <NavigationViewItem Content="Tomorrowland"
                            Tag="XamlBrewer.WinUI3.Navigation.Sample.Views.FestivalDetailsPage"
                            ToolTipService.ToolTip="Tomorrowland" />
        <NavigationViewItem Content="Rock Werchter"
                            Tag="XamlBrewer.WinUI3.Navigation.Sample.Views.FestivalDetailsPage"
                            ToolTipService.ToolTip="Rock Werchter" />
    </NavigationViewItem.MenuItems>
</NavigationViewItem>

There’s much more on NavigationView than we cover in this article. For more details check these guidelines and play around with the WinUI 3 Controls Gallery app:

WinUI3ControlsGallery

Basic Navigation

Our navigation pattern assumes/enforces that all navigation in the app is initiated by the NavigationView instance in the Shell window. We believe that this is applicable to a huge number of apps – at least to the ones that we are currently migrating from UWP. All navigation requests must refer to a NavigationViewItem instance that corresponds with an entry in the menu. The menu items define the target page in their Tag and Content fields, as you saw in the XAML snippets above. It’s the SelectionChanged event that triggers the navigation:

private void NavigationView_SelectionChanged(
	NavigationView sender, 
	NavigationViewSelectionChangedEventArgs args)
{
    SetCurrentNavigationViewItem(args.SelectedItemContainer as NavigationViewItem);
}

This first call into our micro-framework looked up the selected menu item and updated the content frame. Here’s how the code is structured:

  1. All navigation-related code is implemented in a partial class of the Shell window,
  2. encapsulated in an interface that is
  3. exposed via the App instance to
  4. the different XAML pages.

ProjectStructure

When a menu item is selected, we look up the target page information from that menu item, and pass it to Frame.Navigate(). We set the appropriate page header and update the menu’s SelectedItem. That last line is needed in case the navigation was triggered from code behind.

public void SetCurrentNavigationViewItem(
	NavigationViewItem item)
{
    if (item == null)
    {
        return;
    }

    if (item.Tag == null)
    {
        return;
    }

    ContentFrame.Navigate(
	Type.GetType(item.Tag.ToString()), 
	item.Content);
    NavigationView.Header = item.Content;
    NavigationView.SelectedItem = item;
}

Feel free add a test to prevent navigating to an invisible menu item and some exception handling, if you want.

To avoid showing an empty content frame, the app auto-navigates to the Home page when the app starts. The navigation logic is the same throughout all use cases in the app:

  1. look up the menu item that corresponds to the target page, and
  2. use it in the SetCurrentNavigationViewItem() call.
private void NavigationView_Loaded(
	object sender, 
	RoutedEventArgs e)
{
    // Navigates, but does not update the Menu.
    // ContentFrame.Navigate(typeof(HomePage));

    SetCurrentNavigationViewItem(GetNavigationViewItems(typeof(HomePage)).First());
}

Finding menu items

GetNavigationViewItems() retrieves a flattened list of all menu items of the NavigationView: the MenuItems, the FooterMenuItems, and their children. We added two overloads – one to filter on page type (e.g. to find all detail pages in a list) and another to filter on page type and title (to find a specific detail page):

public List<NavigationViewItem> GetNavigationViewItems()
{
    var result = new List<NavigationViewItem>();
    var items = NavigationView.MenuItems.Select(i => (NavigationViewItem)i).ToList();
    items.AddRange(NavigationView.FooterMenuItems.Select(i => (NavigationViewItem)i));
    result.AddRange(items);

    foreach (NavigationViewItem mainItem in items)
    {
        result.AddRange(mainItem.MenuItems.Select(i => (NavigationViewItem)i));
    }

    return result;
}

public List<NavigationViewItem> GetNavigationViewItems(
	Type type)
{
    return GetNavigationViewItems().Where(i => i.Tag.ToString() == type.FullName).ToList();
}

public List<NavigationViewItem> GetNavigationViewItems(
	Type type, 
	string title)
{
    return GetNavigationViewItems(type).Where(ni => ni.Content.ToString() == title).ToList();
}

Feel free to filter away NavigationViewItemHeader and NavigationViewItemSeparator instances from the flat list, if they’re in your way.

We also disclose the currently selected menu item:

public NavigationViewItem GetCurrentNavigationViewItem()
{
    return NavigationView.SelectedItem as NavigationViewItem;
}

Exposing the menu items

The previous methods were all implemented in the Shell Window. To make them available all over the app, we first defined them in an interface:

public interface INavigation
{
    NavigationViewItem GetCurrentNavigationViewItem();

    List<NavigationViewItem> GetNavigationViewItems();

    List<NavigationViewItem> GetNavigationViewItems(Type type);

    List<NavigationViewItem> GetNavigationViewItems(Type type, string title);

    void SetCurrentNavigationViewItem(NavigationViewItem item);
}

Then we exposed the implementation via the App instance – it knows the Shell because it creates it on start-up:

private Shell shell;

public INavigation Navigation => shell;
        
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
    shell = new Shell();
    shell.Activate();
}

All parts of the code base can now easily access the lightweight Navigation Service:

(Application.Current as App).Navigation

Using the Navigation Service

Showing and hiding existing menu items

Our sample app has a ‘Beer’ page that only becomes visible when the user confirms she’s old enough to handle its content. The page is defined in the NavigationView menu, but is initially invisible. The Home page has a checkbox in the lower right corner:

HomePage

When the box is checked, the hidden menu item appears:

HomePageCheck

Here’s the code in the Homepage. It looks up the BeerPage NavigationViewItem, and manipulates its Visibility:

private static NavigationViewItem BeerItem => 
	(Application.Current as App)
		.Navigation
		.GetNavigationViewItems(typeof(BeerPage))
		.First();

private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
    BeerItem.Visibility = Visibility.Visible;
}

private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
{
    BeerItem.Visibility = Visibility.Collapsed;
}

Programmatically navigating to an existing menu item

The FormulaOnePage in our sample app has a hyperlink to the FestivalPage:

Formula1Page

The code behind that hyperlink looks up the target menu item using the GetNavigationViewItems overload with the page type, and then navigates to it – very straightforward:

private void Hyperlink_Click(
	Hyperlink sender, 
	HyperlinkClickEventArgs args)
{
    var navigation = (Application.Current as App).Navigation;
    var festivalItem = navigation.GetNavigationViewItems(typeof(FestivalPage)).First();
    navigation.SetCurrentNavigationViewItem(festivalItem);
}

There’s a similar hyperlink in the HomePage, to test whether we can reach footer menu items in the same way:

FestivalPage

Under the Festival menu item, there is a list of FestivalDetails pages – all of the same type, but with a different topic of course. The hyperlinks on that Festival page use the GetNavigationViewItems overload with page type and content, and also ensure that the parent (Festival) menu item gets expanded:

private void Hyperlink_Click(
	Hyperlink sender, 
	HyperlinkClickEventArgs args)
{
    var navigation = (Application.Current as App).Navigation;
    navigation.GetCurrentNavigationViewItem().IsExpanded = true;
    var festivalItem = navigation.GetNavigationViewItems(
	typeof(FestivalDetailsPage), 
	"Rock Werchter").First();
    navigation.SetCurrentNavigationViewItem(festivalItem);
}

Here’s one of the detail pages:

FestivalDetailPage

Dynamically adding menu items

The BeerPage has a button to programmatically add BeerDetailPage items:

BeerPage

It looks up the parent, adds a menu item of the appropriate type and with its specific title, and makes sure that the parent is expanded:

private void Button_Click(
	object sender, 
	RoutedEventArgs e)
{
    var beerItem = (Application.Current as App)
	.Navigation
	.GetNavigationViewItems(this.GetType())
	.First();
    beerItem.MenuItems.Add(new NavigationViewItem
    {
        Content = $"Round {beerItem.MenuItems.Count + 1}",
        Tag = typeof(BeerDetailsPage).FullName
    });
    beerItem.IsExpanded = true;
}

Here’s such a detail page:

BeerDetailsPage

It has to buttons to iterate back and forth through its list of siblings. The ‘previous’ button navigates backwards through the list of all BeerDetailPage items in the menu. These may be spread over multiple parent items. The ‘next’ button shows how to limit the navigation to the parent of the detail page. This algorithm is a bit more cumbersome since child menu items don’t have a reference to their parent:

private void Button_Click(
	object sender, 
	RoutedEventArgs e)
{
    // Navigation through colleagues
    var navigation = (Application.Current as App).Navigation;
    var item = navigation.GetCurrentNavigationViewItem();
    var siblings = navigation.GetNavigationViewItems(this.GetType());
    var index = siblings.IndexOf(item);
    if (index > 0)
    {
        navigation.SetCurrentNavigationViewItem(siblings[index - 1]);
    }
}

private void Button_Click_1(
	object sender, 
	RoutedEventArgs e)
{
    // Navigation within parent
    var navigation = (Application.Current as App).Navigation;
    var item = navigation.GetCurrentNavigationViewItem();
    var mainItems = navigation.GetNavigationViewItems();
    foreach (var mainItem in mainItems)
    {
        // Find the parent
        if (mainItem.MenuItems.Contains(item))
        {
            var siblings = mainItem.MenuItems;
            var index = siblings.IndexOf(item);
            if (index < siblings.Count - 1)
            {
                navigation.SetCurrentNavigationViewItem((NavigationViewItem)siblings[index + 1]);
            }
        }
    }
}

It’s a wrap

There is definitely room for extra helper methods and a higher abstraction level, but in just a handful lines of C# we created the core of a service that covers most of the navigation requirements for an WinUI 3 app that uses a NavigationView control.

Our sample app lives here on GitHub.

Enjoy!