From uno-platform
This skill demonstrates how to use the Uno Navigation Extensions `NavigationView` and MVVM to create a data-bound, hierarchical hamburger menu with dynamic navigation. Use when implementing a data-bound hamburger menu.
How this skill is triggered — by the user, by Claude, or both
Slash command
/uno-platform:uno-hamburgermenu-databindingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill demonstrates how to use the Uno Navigation Extensions with a `NavigationView` to create a data-bound, hierarchical hamburger menu with dynamic navigation.
This skill demonstrates how to use the Uno Navigation Extensions with a NavigationView to create a data-bound, hierarchical hamburger menu with dynamic navigation.
Author: https://github.com/VincentH-Net
The pattern uses:
NavigationView with uen:Region.Attached="true" for Uno Navigation integrationMenuItemsSource bound to an ObservableCollection<NavMenuItem>Children property on menu itemsFooterMenuItemsSource for settings/utility linksSelectedItem binding to track and control the selected menu itemuen:Region.Name and uen:Navigation.Data on each NavigationViewItem for route-based navigation with data passingDefine a simple model to represent each menu entry, including support for child items:
using System.Collections.ObjectModel;
public sealed class NavMenuItem
{
public string Route { get; init; } = "";
public object? Data { get; init; }
public string Title { get; init; } = "";
public string Subtitle { get; init; } = "";
public string IconGlyph { get; init; } = "";
public string? ToolTip { get; init; }
public ObservableCollection<NavMenuItem>? Children { get; init; }
}
Key points:
Route maps to the Uno Navigation region name (e.g. "Main/Home")Data carries navigation data passed to the target page's viewmodelChildren enables nested/expandable menu items (set to [] for a parent that has children, null for a leaf item)<Page x:Class="MyApp.Presentation.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyApp.Presentation"
xmlns:uen="using:Uno.Extensions.Navigation.UI">
<Grid uen:Region.Attached="True">
<NavigationView PaneDisplayMode="LeftCompact"
IsSettingsVisible="False"
uen:Region.Attached="true"
x:Name="NavView"
IsBackEnabled="False"
IsBackButtonVisible="Collapsed"
MenuItemsSource="{Binding MenuItems}"
FooterMenuItemsSource="{Binding FooterMenuItems}"
SelectedItem="{Binding SelectedMenuItem, Mode=TwoWay}">
<!-- Optional: Pane header branding -->
<NavigationView.PaneHeader>
<StackPanel>
<TextBlock Text="App Name" />
<TextBlock Text="App Subtitle" />
</StackPanel>
</NavigationView.PaneHeader>
<!-- Optional: Header with page top bar for e.g. page title and icons for flyouts -->
<NavigationView.Header>
</NavigationView.Header>
<!-- Template for data-bound menu items -->
<NavigationView.MenuItemTemplate>
<DataTemplate x:DataType="local:NavMenuItem">
<NavigationViewItem uen:Region.Name="{x:Bind Route}"
uen:Navigation.Data="{x:Bind Data}"
Tag="{x:Bind Title}"
ToolTipService.ToolTip="{x:Bind ToolTip}"
MenuItemsSource="{Binding Children}">
<NavigationViewItem.Icon>
<FontIcon Glyph="{x:Bind IconGlyph}"/>
</NavigationViewItem.Icon>
<NavigationViewItem.Content>
<StackPanel>
<TextBlock Text="{x:Bind Title}"/>
<TextBlock Text="{x:Bind Subtitle}"/>
</StackPanel>
</NavigationViewItem.Content>
</NavigationViewItem>
</DataTemplate>
</NavigationView.MenuItemTemplate>
<!-- Content area where navigated pages appear -->
<Grid uen:Region.Attached="True"
uen:Region.Name="Main"
uen:Region.Navigator="Visibility" />
</NavigationView>
</Grid>
</Page>
Grid needs uen:Region.Attached="True" to establish the navigation region hierarchy.NavigationView also needs uen:Region.Attached="true" so it participates in routing.MenuItemTemplate must use a NavigationViewItem (not just content) with:
uen:Region.Name="{x:Bind Route}" for navigation routinguen:Navigation.Data="{x:Bind Data}" — REQUIRED when passing typed data. Without it, the clicked NavigationViewItem container becomes the navigation data instead of the intended payload.MenuItemsSource="{Binding Children}" to enable nested child items (note: uses {Binding} not {x:Bind} due to known Uno Platform x:Bind source generator issues in nested DataTemplate contexts — see unoplatform/uno#7279, #18509, #8471)uen:Region.Navigator="Visibility" so pages are shown/hidden rather than recreated.IsDefault: true in their RouteMap constructor. This ensures the landing page displays on initial navigation."Main/Home", "Main/Settings"). Do NOT use relative names. Mixed/relative names cause context-sensitive failures — submenu children can navigate at shell scope instead of within the content region, or header selection changes while content stays on the prior page.NavigationViewNavigator enumerates top-level only: Route-to-selected-item synchronization only walks MenuItems + FooterMenuItems — it does NOT recursively descend into nested submenu children. Keep nesting to one level maximum or accept that selection highlight may desync from the actual navigated page.When adding flyouts (notifications, profile, org switcher) in NavigationView.Header, command bindings fail silently on Uno Skia. Flyout content renders in PopupRoot where DataContext resolves to the shell's ViewModel, not the expected scope. Data bindings work (read-only) but commands resolve to null.
Fix: Wrap the command in a data model so it travels with the data through the Flyout boundary:
public sealed class ProfileInfo
{
public string Name { get; init; } = "";
public string Email { get; init; } = "";
public IRelayCommand LogoutCommand { get; init; } = null!;
}
In the ViewModel, create the model with the command embedded:
ProfileInfo = new ProfileInfo { Name = user.Name, LogoutCommand = LogoutCommand };
In the Flyout XAML, bind through the data model:
<Button Content="Log out" Command="{Binding ProfileInfo.LogoutCommand}"/>
Do NOT use Click handlers as a workaround.
NavigateViewModelAsync resolves routes within the current navigator scope (child routes only). To navigate to a sibling route (e.g. from the main shell to a login page), use NavigateRouteAsync with the -/ qualifier:
await navigator.NavigateRouteAsync(this, "-/Login");
WHY: NavigateViewModelAsync to a sibling ViewModel fails silently.
.UseThemeSwitching() host builder requirementThe dotnet new unoapp template does NOT include .UseThemeSwitching(). Add it to the host builder chain for IThemeService to resolve:
.Configure(host => host
.UseThemeSwitching() // required for IThemeService injection
.UseNavigation(RegisterRoutes)
)
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
[Bindable]
public partial class MainViewModel : ObservableObject
{
const int ReportsCount = 3;
[ObservableProperty]
string pageTitle = "";
[ObservableProperty]
ObservableCollection<NavMenuItem> menuItems = [];
[ObservableProperty]
ObservableCollection<NavMenuItem> footerMenuItems = [];
// CRITICAL: Must be object?, NOT NavMenuItem? — Uno Navigation's SelectorNavigator
// sets SelectedItem to a NavigationViewItem container, not your NavMenuItem model.
// Using NavMenuItem? causes runtime TypeConverter errors.
[ObservableProperty]
object? selectedMenuItem;
partial void OnSelectedMenuItemChanged(object? value)
{
// Must handle BOTH NavMenuItem (from programmatic selection) and
// NavigationViewItem (from Uno Navigation's SelectorNavigator.Show)
if (value is NavMenuItem { Children: null or { Count: 0 } } menuItem)
PageTitle = menuItem.Title;
else if (value is NavigationViewItem { Tag: string tag })
PageTitle = tag;
}
public MainViewModel()
{
BuildMenuItems();
BuildFooterMenuItems();
}
void BuildMenuItems()
{
ObservableCollection<NavMenuItem> items = [];
// Simple leaf menu item (no children)
items.Add(new NavMenuItem
{
Route = "Main/Home",
Title = "Home",
Subtitle = "Default app startup page",
IconGlyph = "\u2302",
ToolTip = "Home - Default app startup page"
});
// Parent menu item with children (multi-level)
// Add children dynamically (e.g. from API data)
// Smart collapsing: if only one child, add it directly instead of nesting
if (ReportsCount > 0)
{
var reports = new NavMenuItem
{
Title = "Reports",
Subtitle = "All available reports",
IconGlyph = "\u2399",
ToolTip = "Reports - All available reports",
Children = []
};
var peers = ReportsCount > 1 ? reports.Children : items;
for (int reportId = 0; reportId < ReportsCount; reportId++)
{
string description = "Report {reportid} description";
peers.Add(new NavMenuItem
{
Route = "Main/Report",
Data = reportId, // passed to destination viewmodel
Title = "Report {reportid}",
Subtitle = description,
IconGlyph = "\u2399",
ToolTip = $"Report {reportid} - {description}"
});
}
if (reports.Children.Count > 0) items.Add(reports);
}
MenuItems = items;
SelectedMenuItem ??= MenuItems.FirstOrDefault();
}
void BuildFooterMenuItems()
{
FooterMenuItems = [
new () {
Route = "Main/Settings",
Title = "Settings",
Subtitle = "User preferences",
IconGlyph = "\uE713",
ToolTip = "Settings - User preferences"
}
];
}
}
ObservableCollection (assign a new one to MenuItems) rather than clearing and re-adding items. This ensures the NavigationView properly re-renders.SelectedMenuItem after building the menu to auto-navigate to the default page.var peers = children.Count > 1 ? parent.Children : items;
foreach (var child in dataSource)
peers.Add(new NavMenuItem { ... });
if (parent.Children.Count > 0) items.Add(parent);
OnSelectedMenuItemChanged: Use the generated partial method from [ObservableProperty] to react to selection changes (e.g. update a page title, skip parent items).Data on NavMenuItem to pass objects (e.g. an entity from an API) to the destination page's viewmodel via Uno Navigation.Routes follow the pattern "RegionName/PageName", e.g.:
"Main/Home" navigates to HomePage in the region named "Main""Main/Monitoring" navigates to MonitoringPage in the "Main" regionThe region name in the route must match the uen:Region.Name on the content Grid in XAML. Always use the fully qualified form — see "Critical routing details" above.
The dotnet new unoapp template shows flat sibling routes. The hamburger menu pattern typically requires nested routes under a shell page, with a login page as a sibling:
static void RegisterRoutes(IViewRegistry views, IRouteRegistry routes)
{
_ = views.Register(
new ViewMap(ViewModel: typeof(ShellViewModel)),
new ViewMap<LoginPage, LoginViewModel>(),
new ViewMap<MainPage, MainViewModel>(),
new ViewMap<HomePage, HomeViewModel>(),
new ViewMap<SettingsPage, SettingsViewModel>(),
new DataViewMap<DetailPage, DetailViewModel, DetailInfo>() // typed navigation data
);
_ = routes.Register(
new RouteMap("", View: views.FindByViewModel<ShellViewModel>(),
Nested: [
new("Login", View: views.FindByViewModel<LoginViewModel>(), IsDefault: true),
new("Main", View: views.FindByViewModel<MainViewModel>(),
Nested: [
new("Home", View: views.FindByViewModel<HomeViewModel>(), IsDefault: true),
new("Settings", View: views.FindByViewModel<SettingsViewModel>()),
new("Detail", View: views.FindByViewModel<DetailViewModel>()),
]
),
]
)
);
}
Key differences from template:
IsDefault: true sibling — shown first on app startIsDefault: true on the landing pageDataViewMap for pages receiving typed navigation dataUno.Extensions.Navigation.UI (for uen:Region.* and uen:Navigation.* attached properties)CommunityToolkit.Mvvm (for [ObservableProperty], [RelayCommand], ObservableObject)Microsoft.UI.Xaml / WinUI 3 (for NavigationView, NavigationViewItem)npx claudepluginhub vincenth-net/dotnet-agentic-engineering --plugin uno-platformProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.