December 2017
Volume 32 Number 12
[Universal Windows Platform ]
A Developer’s Guide to the New Hamburger Menu in Windows 10
By Jerry Nixon
The Windows XAML team released the NavigationView control with the Windows 10 Fall Creators Update. Prior to the control, developers tasked with implementing a hamburger menu were limited to the rudimentary features of the SplitView control. The resulting interfaces were inconsistent in both visual presentation and behavior. Even first-party Microsoft apps like Groove, Xbox, News and Mail struggled with visual alignment across the portfolio. Often an internal problem drives an external solution, as is the case here with NavigationView.
The control gives XAML developers a fresh and beautiful visual, consistently implemented across devices with comprehensive support for adaptive scaling, localization, accessibility, and signature Windows experiences like Microsoft’s new Fluent design system. The control is beautiful, and end users are bound to lose endless hours of productivity just invoking the control’s selection animation over and over. It’s mesmerizing. Figure 1 shows NavigationView basic styling.
Figure 1 NavigationView Basic Style
The Basics
Adding the NavigationView to an app is simple. Generally, NavigationView is in a dedicated page, like ShellPage.xaml. Just a few lines of XAML give a menu, buttons and handlers to respond to typical user actions. See the NavigationViewItem in MenuItems in the following code:
<NavigationView SelectionChanged="SelectionChanged">
<NavigationView.MenuItems>
<NavigationViewItemHeader Content="Section A" />
<NavigationViewItem Content="Item 01" />
<NavigationViewItem Content="Item 02" />
</NavigationView.MenuItems>
<Frame x:Name="NavigationFrame" />
</NavigationView>
These are the primary navigation buttons. Other items supported are the NavigationViewItemHeader and NavigationViewItemSeparator, which, together, allow developers to compose beautiful and sophisticated menus. There are several things you should be thinking about as you work through the Navigation view.
Anatomy The parts of the NavigationView are as shown in Figure 2. Each area builds out a comprehensive UX. The Header, Pane Footer, Auto Suggest and Settings button are optional, depending on your app’s design requirements.
Figure 2 NavigationView Parts
Modes The control has three possible modes: Minimal, Compact and Extended, as depicted in Figure 3. Each is auto-selected based on a built-in and customizable view with thresholds. Modes allow the NavigationView to remain usable and practical as the size of the app or device changes.
Figure 3 NavigationView Modes
Real-World Problems
The NavigationView is simple to understand, but not always easy to implement, especially within the context of sophisticated, real-world scenarios. Controls made accessible to every developer use case often require some clear-headed coding. Here’s a rundown of issues and elements developers need to recognize. I’ll be addressing each of these later in this article.
Data Binding Personally, I think it’s ridiculous to data bind menu items to a top-level navigation control, but I realize not all developers agree. To that end, binding to the NavigationView from a codebehind is quite simple. Binding from a view-model requires breaking cardinal rules like referencing UI namespaces.
Navigating Developers might be surprised the NavigationView doesn’t navigate. It’s only a visual affordance for navigation. It doesn’t have a Frame or understand what its menu items should do. The first thing developers will need to solve is simple navigation with some logic around reloading pages.
Back Button Windows 10 provides a shell-drawn back button that’s optional in some cases, but required in others, like tablet mode. The back button saves canvas real estate and establishes a unified point for back navigation. Attaching to the universal WinRT BackRequested event is straight forward, but synchronizing NavigationView’s selection is another requirement.
Settings Button The NavigationView provides a default, localized Settings button at the bottom of the menu pane. It’s brilliant. The button establishes a single, standard point of invocation for a common user action. It’s the sort of thing designers and developers should learn from and adopt quickly for the sake of a visually aligned UX across the ecosystem.
Implementation of the Settings button is simple and clean, but it’s another requirement of the NavigationView that’s not delivered right out of the box. The problem lies in every XAML developer’s desire to declare a control’s behavior, rather than code it.
Header Items The NavigationView’s MenuItem property accepts NavigationViewItemHeader objects used to visually bookend buttons; it’s particularly useful to partition NavigationViewItems. But opening and closing the NavigationView’s menu pane truncates the content of a header. Developers need to be able to control menu look and structure in both narrow and wide modes.
Real-World Solutions
XAML developers have several tools for solving problems. Inheriting from a control lets developers extend its behavior (bit.ly/2gQ4vN4), extension methods enhance the base implementation of even sealed controls (bit.ly/2ik1rfx) and attached properties can broaden the capabilities of a control (bit.ly/2giDGAn), even supporting declaration in XAML.
Data Binding Since 2006, when the XAML team invented it, Model-View-ViewModel (MVVM) has been the darling pattern of XAML developers, including Microsoft’s own first-party apps. One principle of the design pattern is to prevent the reliance on and reference to UI namespaces in view-models. There are many reasons this is smart. As shown in the following code snippet, NavigationView supports the data binding of NavigationViewItems to the MenuItemsSource property, similar to ListView.ItemsSource, but it precludes UI namespaces. That’s fine in codebehind, but a problem to solve for view-models:
public IEnumerable<object> MenuItems
{
get
{
return new[]
{
new NavigationViewItem { Content = "Home" },
new NavigationViewItem { Content = "Reports" },
new NavigationViewItem { Content = "Calendar" },
};
}
}
To side-step referencing Windows.UI.Xaml.Controls in my view-model, I abstract the NavigationViewItem to a DTO. I repeat this process for each potential peer object. Every item’s ordinal position is the responsibility of the view-model and should be maintained by the view logic. These abstractions are simple and easy for the view-model to provide, as shown in this code:
public class NavItemEx
{
public string Icon { get; set; }
public string Text { get; set; }
}
public class NavItemHeaderEx
{
public string Text { get; set; }
}
public class NavItemSeparatorEx { }
However, the NavigationView doesn’t know my custom classes and they need to be converted to proper NavigationView controls for rendering. Binding to custom classes requires significant custom code in the NavigationView to coerce rendering, so we’ll avoid this. Note: I am intentionally avoiding custom templates, so I don’t mistakenly spoil accessibility or miss out on template improvements in subsequent platform releases. To make conversion easy, I introduce a value converter I can reference in my XAML binding. Figure 4 shows the code responsible for taking my enumerable of custom classes and returning the objects that the NavigationView expects.
Figure 4 Converter for NavItems
public class INavConverter : IvalueConverter
{
public object Convert(object v, Type t, object p, string l)
{
var list = new List<object>();
foreach (var item in (v as Ienumerable<object>))
{
switch (item)
{
case NavItemEx dto:
list.Add(ToItem(dto));
break;
case NavItemHeaderEx dto:
list.Add(ToItem(dto));
break;
case NavItemSeparatorEx dto:
list.Add(ToItem(dto));
break;
}
}
return list;
}
object IvalueConverter.ConvertBack(object v, Type t, object p, string l)
throw new NotImplementedException();
NavigationViewItem ToItem(NavItemEx item)
new NavigationViewItem
{
Content = item.Text,
Icon = ToFontIcon(item.Icon),
};
FontIcon ToFontIcon(string glyph)
new FontIcon { Glyph = glyph, };
NavigationViewItemHeader ToItem(NavItemHeaderEx item)
new NavigationViewItemHeader { Content = item.Text, };
NavigationViewItemSeparator ToItem(NavItemSeparatorEx item)
new NavigationViewItemSeparator { };
}
After referencing this converter as an app-wide or page-level resource, the syntax is as simple as any other converter. I want to take a moment to reiterate how crazy I think it would be to data bind a top-level navigation, but this extensible solution works seamlessly, as shown here:
MenuItemsSource=”{x:Bind ViewModel.Items, Converter={StaticResource NavConverter}}”
Navigating Navigation in the Universal Windows Platform (UWP) starts with the XAML Frame. But, the NavigationView doesn’t have a Frame. In addition, there’s no way to declare my intent with a menu button, which is to say, the page I want it to open. This is easily solved with the XAML attached properties shown here:
public partial class NavProperties : DependencyObject
{
public static Type GetPageType(NavigationViewItem obj)
=> (Type)obj.GetValue(PageTypeProperty);
public static void SetPageType(NavigationViewItem obj, Type value)
=> obj.SetValue(PageTypeProperty, value);
public static readonly DependencyProperty PageTypeProperty =
DependencyProperty.RegisterAttached("PageType", typeof(Type),
typeof(NavProperties), new PropertyMetadata(null));
}
Once I have PageType on NavigationViewItem, I can declare the target page in XAML or bind it to my view-model. Note: I could add additional Parameter and TransitionInfo properties if my design required it; this sample focuses on a basic Navigation implementation. Then I let the extended NavigationView handle navigation, as shown in Figure 5.
Figure 5 NavViewEx, an Extended NavigationView
public class NavViewEx : NavigationView
{
Frame _frame;
public Type SettingsPageType { get; set; }
public NavViewEx()
{
Content = _frame = new Frame();
_frame.Navigated += Frame_Navigated;
ItemInvoked += NavViewEx_ItemInvoked;
SystemNavigationManager.GetForCurrentView()
.BackRequested += ShellPage_BackRequested;
}
private void NavViewEx_ItemInvoked(NavigationView sender,
NavigationViewItemInvokedEventArgs args)
{
if (args.IsSettingsInvoked)
SelectedItem = SettingsItem;
else
SelectedItem = Find(args.InvokedItem.ToString());
}
private void Frame_Navigated(object sender, NavigationEventArgs e)
=> SelectedItem = (e.SourcePageType == SettingsPageType)
? SettingsItem : Find(e.SourcePageType) ?? base.SelectedItem;
private void ShellPage_BackRequested(object sender, BackRequestedEventArgs e)
=> _frame.GoBack();
NavigationViewItem Find(string content)
=> MenuItems.OfType<NavigationViewItem>()
.SingleOrDefault(x => x.Content.Equals(content));
NavigationViewItem Find(Type type)
=> MenuItems.OfType<NavigationViewItem>()
.SingleOrDefault(x => type.Equals(x.GetValue(NavProperties.PageTypeProperty)));
public virtual void Navigate(Frame frame, Type type)
=> frame.Navigate(type);
public new object SelectedItem
{
set
{
if (value == SettingsItem)
{
Navigate(_frame, SettingsPageType);
base.SelectedItem = value;
_frame.BackStack.Clear();
}
else if (value is NavigationViewItem i && i != null)
{
Navigate(_frame, i.GetValue(NavProperties.PageTypeProperty) as Type);
base.SelectedItem = value;
_frame.BackStack.Clear();
}
UpdateBackButton();
}
}
private void UpdateBackButton()
{
SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
(_frame.CanGoBack) ? AppViewBackButtonVisibility.Visible
: AppViewBackButtonVisibility.Collapsed;
}
}
Look at Figure 5 and you’ll notice four important enhancements. One, a XAML Frame is injected during control instantiation. Two, handlers have been added for Frame.Navigated, ItemInvoked and BackRequested. Three, SelectedItem has been overridden to add BackStack and BackButton logic. And four, a new SettingsPageType property has been added to the class.
Back Button The new, explicit frame isn’t just a convenience, it gives me the source for Navigation events. This is important. When the NavigationView invokes navigation, I update the visibility of the shell-drawn back button. Should the user navigate another way, however, I can’t know to update the back button without some sort of event. The Frame.Navigated event is an excellent, global choice.
Find An unexpected behavior of the NavigationView’s ItemInvoked event is that the InvokedItem property passed in the custom event arguments is the string content of the NavigationViewItem and not an object reference to the item itself. As a result, the Find methods in this customized control locate the correct NavigationViewItem based on content passed in ItemInvoked or PageType passed in the Frame.Navigated event.
It’s worth noticing the content of NavigationViewItem can change dynamically with localization settings on the device. Handling ItemInvoked with a hardcoded switch statement, as demonstrated in the online documentation (bit.ly/2xQodCM) would work for English-speakers only or require the switch to exponentially expand as languages are added to support a UWP app. Try to avoid magic numbers and magic strings anywhere in your code. They’re not compatible with significant code bases.
Settings The settings button is the only button in the lower menu pane that participates in the selection logic of the NavigationView. Invoking it, users navigate to the settings page. To simplify that implementation, notice the custom SettingsPageType property, which holds the desired target page type for settings. The overridden SelectedItem setter tests for the settings button and consequently navigates as declared.
What isn’t handled in either the NavigationViewItem’s PageType property or the SettingsPageType property is a way to indicate custom TransitionInfo to the Frame’s Navigate method to coerce the transition information during navigation. This can be an important customization to any app, and additional custom or attached properties could be added to allow for this additional instruction. The code to accomplish this looks like this:
<local:NavViewEx SettingsPageType="views:SettingsPage">
<NavigationView.MenuItems>
<NavigationViewItem Content="Item 01"
local:NavProperties.PageType="views:Page01" />
<NavigationViewItem Content="Item 02"
local:NavProperties.PageType="views:Page02" />
<NavigationViewItem Content="Item 03"
local:NavProperties.PageType="views:Page03" />
</NavigationView.MenuItems>
</local:NavViewEx>
This kind of extensibility allows developers to aggressively extend the behavior of controls and classes without altering their fundamental, underlying implementations. It’s a capability of C# and XAML that has been there for years and makes the coding syntax terse and the XAML declaration plain. It’s an intuitive approach that translates to other developers clearly with little instruction.
Start Page When an app loads, no menu item is initially invoked. Adding another attached property, as shown below, lets me declare my intent in XAML so the extended NavigationView can initialize the first page in its Frame. Here’s the property:
public partial class NavProperties : DependencyObject
{
public static bool GetIsStartPage(NavigationViewItem obj)
=> (bool)obj.GetValue(IsStartPageProperty);
public static void SetIsStartPage(NavigationViewItem obj, bool value)
=> obj.SetValue(IsStartPageProperty, value);
public static readonly DependencyProperty IsStartPageProperty =
DependencyProperty.RegisterAttached("IsStartPage", typeof(bool),
typeof(NavProperties), new PropertyMetadata(false));
}
Using this new property in the NavigationView is a matter of locating the NavigationViewItem within MenuItems with the Start property set, then navigating to it when the control has successfully loaded. This logic is optional, supporting the setting but not requiring it, as shown here:
Loaded += (s, e) =>
{
if (FindStart() is NavigationViewItem i && i != null)
Navigate(_frame, i.GetValue(NavProperties.PageTypeProperty) as Type);
};
NavigationViewItem FindStart()
=> MenuItems.OfType<NavigationViewItem>()
.SingleOrDefault(x => (bool)x.GetValue(NavProperties.IsStartPageProperty));
Notice the use of the LINQ SingleOrDefault selector in my FindStart method, as opposed to its selector sibling, First. Where FirstOrDefault returns the first found, SingleOrDefault throws an exception should more than one be discovered by its predicate. This helps guide and even enforce developer usage of the property, because only one initial page should ever be declared.
Page Header As shown in Figure 2, the NavigationView Header isn’t optional. This area above the Page, with a fixed height of 48px, is intended for global content. Implementing a simple title is as easy as the snippet here, which attached a Header property to the Page object:
public partial class NavProperties : DependencyObject
{
public static string GetHeader(Page obj)
=> (string)obj.GetValue(HeaderProperty);
public static void SetHeader(Page obj, string value)
=> obj.SetValue(HeaderProperty, value);
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.RegisterAttached("Header", typeof(string),
typeof(NavProperties), new PropertyMetadata(null));
}
With the Frame’s Navigated event, NavViewEx looks for the property in the resulting page, injecting the optional value into the NavigationView’s Header. The new attached Page property can be scoped to individual pages and localized through the UWP x:Uid localization subsystem. The code in Figure 6 shows how updating the header effectively introduces only two new lines of code to the extended control.
Figure 6 Updating the Header
private void Frame_Navigated(object sender,
Windows.UI.Xaml.Navigation.NavigationEventArgs e)
{
SelectedItem = Find(e.SourcePageType);
UpdateHeader();
}
private void UpdateHeader()
{
if (_frame.Content is Page p
&& p.GetValue(NavProperties.HeaderProperty) is string s
&& !string.IsNullOrEmpty(s))
{
Header = s;
}
}
In this simple example the default TextBlock in the Header is accepted. In my experience, and corroborated by Microsoft’s first-party, in-box apps, a CommandBar control typically takes up this valuable screen real estate. If I wanted the same in my app, I could update the HeaderTemplate property with this simple markup:
<NavigationView.HeaderTemplate>
<DataTemplate>
<CommandBar>
<CommandBar.Content>
<Grid Margin="12,5,0,11" VerticalAlignment="Stretch">
<TextBlock Text="{Binding}"
Style="{StaticResource TitleTextBlockStyle}"
TextWrapping="NoWrap" VerticalAlignment="Bottom"/>
</Grid>
</CommandBar.Content>
</CommandBar>
</DataTemplate>
</NavigationView.HeaderTemplate>
That TextBlock styling mimics the control’s default Header, placing it inside a globally available CommandBar, which can programmatically be implemented by an app on a page-by-page or global context. As a result, the basic design is visually the same, but its functional potential is significantly expanded.
The Narrow Item Header Issue
One problem remains. As described early in this article, the NavigationView has different display modes that vary based on view width. It also can explicitly open and close the menu pane. When the menu pane is open, its width is determined by the value of the OpenPaneLength property. Don’t get me started on that property name using Length instead of Width. Anyway, here’s the important part: That property value doesn’t impact the width of the menu pane when it’s closed; in the closed state, the pane width is hardcoded to 48px wide.
Here, NavigationViewItems look great with their icons set to 48px wide, but NavigationViewItemHeaders have only one Content property, and it’s the same if the pane is open or closed. Attractive when open, the text is truncated when closed, as shown in Figure 7.
Figure 7 NavigationViewHeader in Open and (Narrow) Closed States
What to do? I first thought of adding an Icon to headers, but when the pane is closed it would look like a NavigationViewItem, but with the bizarre and possibly frustrating behavior of not responding to taps. I thought about alternate text, but inside 48px there’s barely room for three characters. I finally landed on hiding headers when the pane is closed, as shown in the following code snippet:
RegisterPropertyChangedCallback(IsPaneOpenProperty, IsPaneOpenChanged);
private void IsPaneOpenChanged(DependencyObject sender,
DependencyProperty dp)
{
foreach (var item in MenuItems.OfType<NavigationViewItemHeader>())
{
item.Opacity = IsPaneOpen ? 1: 0;
}
}
In this case, changing its visibility prevented any sudden movement of the items in the list. This is not only the easiest to implement, it’s also visually pleasant, and somewhat intuitive as to why it’s occurring. Because NavigationView doesn’t expose an Opened or Closed event, you register for a dependency property change on the IsPaneOpenProperty using RegisterPropertyChangedCallback, a handy utility introduced with Windows 8. I’ll identify the callback and toggle every header. If I wanted to, I could treat different headers in different ways; this example handles all headers the same.
Wrapping Up
What’s beautiful about the Universal Windows Platform and XAML is the abundance of solutions to problems. No control meets the needs of every developer. No API meets the needs of every design. Building on a rich platform with extensive love for its developers turns snags into solutions with just a drop of code and a little effort. It lets you create your own signature experiences with unique value propositions that set your app apart in the ecosystem. Now, even the hamburger menu is a simple addition to your look-and-feel with opportunities for extensions around every corner.
Jerry Nixon is an author, speaker, developer and evangelist in Colorado. He trains and inspires developers around the world to build better apps with crafted code. Nixon spends the bulk of his free time teaching his three daughters Star Trek character backstories and episode plots.
Thanks to the following technical expert for reviewing this article: Daren May
Daren May is a four-year Windows Development MVP and runs CustomMayd, a developer training and custom development company.