Navigating in a WinUI 3 Desktop application | XAML Brewer, by Diederik Krols
source link: https://xamlbrewer.wordpress.com/2021/07/06/navigating-in-a-winui-3-desktop-application/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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:
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,
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:
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:
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:
- All navigation-related code is implemented in a partial class of the Shell window,
- encapsulated in an interface that is
- exposed via the App instance to
- the different XAML pages.
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:
- look up the menu item that corresponds to the target page, and
- 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:
When the box is checked, the hidden menu item appears:
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:
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:
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:
Dynamically adding menu items
The BeerPage has a button to programmatically add BeerDetailPage items:
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:
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!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK