Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions 11.0/Navigation/ShellRouteTemplates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: .NET MAUI - Shell route templates
description: Demonstrates absolute Shell navigation with required, optional, defaulted, constrained, catch-all, and mixed route-template parameters.
page_type: sample
languages:
- csharp
- xaml
products:
- dotnet-maui
urlFragment: navigation-shell-route-templates
---

# Shell route templates

This .NET MAUI 11 sample is a small trip-planning route lab. Its route matrix runs every route-template form introduced by [dotnet/maui#35110](https://github.com/dotnet/maui/pull/35110), navigates to a result page, and compares the delivered parameter with the expected value.

> [!IMPORTANT]
> Route templates support **absolute navigation only** in this release. Every example uses a URI beginning with `//routes`. Do not use these templates with relative navigation.

## What you'll learn

- How to register required, optional, defaulted, constrained, catch-all, and mixed route templates.
- Which constraints are implemented by the shipping parser.
- How path parameters flow through both `[QueryProperty]` and `IQueryAttributable`.
- How to verify the resolved value on the destination page.

## Requirements

- .NET SDK `11.0.100-preview.7.26381.103`
- .NET MAUI workload
- .NET MAUI `11.0.0-preview.7.26404.4`, supplied by `11.0/Directory.Build.props`
- Android, iOS, or Mac Catalyst tooling for the target you run

## Route matrix

Optional and default parameters must be the final segment. Catch-all parameters must also be last. The shipping implementation supports one parameter per mixed segment and one constraint per parameter.

| Form | Registered template | Absolute URI used by the sample | Delivered value |
|---|---|---|---|
| Required | `trip/{tripId}` | `//routes/trip/SEA-204` | `tripId = SEA-204` |
| Optional, present | `traveler/{name?}` | `//routes/traveler/Ada` | `name = Ada` |
| Optional, absent | `traveler/{name?}` | `//routes/traveler` | `name` is not supplied |
| Default | `rating/{stars=5}` | `//routes/rating` | `stars = 5` |
| `int` constraint | `reservation/{reservationId:int}` | `//routes/reservation/42` | `reservationId = 42` |
| `long` constraint | `loyalty/{points:long}` | `//routes/loyalty/9000000000` | `points = 9000000000` |
| `double` constraint | `budget/{amount:double}` | `//routes/budget/1299.50` | `amount = 1299.50` |
| `bool` constraint | `toggle/{enabled:bool}` | `//routes/toggle/true` | `enabled = true` |
| `guid` constraint | `booking/{reference:guid}` | `//routes/booking/550e8400-e29b-41d4-a716-446655440000` | `reference` is the GUID |
| `alpha` constraint | `region/{name:alpha}` | `//routes/region/Pacific` | `name = Pacific` |
| Catch-all | `files/{*path}` | `//routes/files/trips/SEA-204/receipt.pdf` | `path = trips/SEA-204/receipt.pdf` |
| Mixed segment | `trip-{tripId}-summary` | `//routes/trip-SEA-204-summary` | `tripId = SEA-204` |

The app appends a `caseId` query string solely to select the expected matrix row. The values shown above come from the path template.

## Key files

| File | Purpose |
|---|---|
| `src/AppShell.xaml.cs` | Registers each route template. |
| `src/Models/RouteCatalog.cs` | Defines the testable route matrix and expected values. |
| `src/ViewModels/MainPageViewModel.cs` | Executes each absolute navigation URI. |
| `src/QueryPropertyResultPage.xaml.cs` | Receives required and mixed parameters through `[QueryProperty]`. |
| `src/AttributableResultPage.xaml.cs` | Receives the other parameters through `IQueryAttributable`. |

## Run the sample

From this directory:

```bash
dotnet build src/ShellRouteTemplates.sln
dotnet build -t:Run -f net11.0-maccatalyst src/ShellRouteTemplates.csproj
```

You can also select the `net11.0-ios` target and an iOS simulator in Visual Studio Code or Visual Studio. On the route matrix, choose **Run** for each row. The destination page displays `PASS` when the actual path parameter matches the expected value.

## Resources

- [Feature PR: Shell route templates with path parameters](https://github.com/dotnet/maui/pull/35110)
- [Shipping route-template parser](https://github.com/dotnet/maui/blob/e45600b065c6636c73fefdc8406bf8881f65e9d4/src/Controls/src/Core/Shell/RouteTemplate.cs)
- [Shipping route-template tests](https://github.com/dotnet/maui/blob/e45600b065c6636c73fefdc8406bf8881f65e9d4/src/Controls/tests/Core.UnitTests/ShellRouteTemplatesTests.cs)
- [.NET MAUI for .NET 11 release notes](https://learn.microsoft.com/dotnet/maui/whats-new/dotnet-11?view=net-maui-11.0)
14 changes: 14 additions & 0 deletions 11.0/Navigation/ShellRouteTemplates/src/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:ShellRouteTemplates"
x:Class="ShellRouteTemplates.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
17 changes: 17 additions & 0 deletions 11.0/Navigation/ShellRouteTemplates/src/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace ShellRouteTemplates;

public partial class App : Application
{
readonly AppShell appShell;

public App(AppShell appShell)
{
InitializeComponent();
this.appShell = appShell;
}

protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(appShell);
}
}
14 changes: 14 additions & 0 deletions 11.0/Navigation/ShellRouteTemplates/src/AppShell.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ShellRouteTemplates.AppShell"
xmlns:local="clr-namespace:ShellRouteTemplates"
Title="Shell Route Templates">

<ShellContent
x:Name="RouteMatrix"
Title="Home"
Route="routes" />

</Shell>
22 changes: 22 additions & 0 deletions 11.0/Navigation/ShellRouteTemplates/src/AppShell.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace ShellRouteTemplates;

public partial class AppShell : Shell
{
public AppShell(MainPage mainPage)
{
InitializeComponent();
RouteMatrix.Content = mainPage;

Routing.RegisterRoute("trip/{tripId}", typeof(RequiredResultPage));
Routing.RegisterRoute("traveler/{name?}", typeof(TravelerResultPage));
Routing.RegisterRoute("rating/{stars=5}", typeof(DefaultValueResultPage));
Routing.RegisterRoute("reservation/{reservationId:int}", typeof(IntConstraintResultPage));
Routing.RegisterRoute("loyalty/{points:long}", typeof(LongConstraintResultPage));
Routing.RegisterRoute("budget/{amount:double}", typeof(DoubleConstraintResultPage));
Routing.RegisterRoute("toggle/{enabled:bool}", typeof(BoolConstraintResultPage));
Routing.RegisterRoute("booking/{reference:guid}", typeof(GuidConstraintResultPage));
Routing.RegisterRoute("region/{name:alpha}", typeof(AlphaConstraintResultPage));
Routing.RegisterRoute("files/{*path}", typeof(CatchAllResultPage));
Routing.RegisterRoute("trip-{tripId}-summary", typeof(MixedResultPage));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ShellRouteTemplates.AttributableResultPage"
Title="Route result">
<ScrollView>
<VerticalStackLayout Padding="24" Spacing="12">
<Label
FontAttributes="Bold"
FontSize="28"
SemanticProperties.HeadingLevel="Level1"
Text="IQueryAttributable result" />
<Label x:Name="FormLabel" FontAttributes="Bold" FontSize="20" />
<Label Text="Template" FontAttributes="Bold" />
<Label x:Name="TemplateLabel" FontFamily="monospace" />
<Label Text="Delivery" FontAttributes="Bold" />
<Label x:Name="DeliveryLabel" />
<Label Text="Expected value" FontAttributes="Bold" />
<Label x:Name="ExpectedLabel" AutomationId="ExpectedValue" />
<Label Text="Actual value" FontAttributes="Bold" />
<ContentView x:Name="ActualValueHost" />
<ContentView x:Name="StatusHost" />
<Button
AutomationId="BackToMatrix"
Clicked="OnBackToMatrix"
Text="Back to route matrix" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using ShellRouteTemplates.Models;

namespace ShellRouteTemplates;

public partial class AttributableResultPage : ContentPage, IQueryAttributable
{
protected AttributableResultPage()
{
InitializeComponent();
}

public void ApplyQueryAttributes(IDictionary<string, object> query)
{
var caseId = query.TryGetValue("caseId", out var caseValue)
? caseValue?.ToString()
: null;
var example = RouteCatalog.Find(caseId);
var actualValue = example is not null
&& query.TryGetValue(example.ParameterName, out var parameterValue)
? parameterValue?.ToString()
: null;
var actualLabel = new Label();
var statusLabel = new Label
{
FontAttributes = FontAttributes.Bold,
FontSize = 24
};

if (example is not null)
{
actualLabel.AutomationId = $"ActualValue-{example.Id}";
statusLabel.AutomationId = $"ResultStatus-{example.Id}";
}

ActualValueHost.Content = actualLabel;
StatusHost.Content = statusLabel;

RouteResultView.Render(
example,
actualValue,
FormLabel,
TemplateLabel,
DeliveryLabel,
ExpectedLabel,
actualLabel,
statusLabel);
}

async void OnBackToMatrix(object? sender, EventArgs e) =>
await Shell.Current.GoToAsync("//routes");
}

public sealed class TravelerResultPage : AttributableResultPage
{
}

public sealed class DefaultValueResultPage : AttributableResultPage
{
}

public sealed class IntConstraintResultPage : AttributableResultPage
{
}

public sealed class LongConstraintResultPage : AttributableResultPage
{
}

public sealed class DoubleConstraintResultPage : AttributableResultPage
{
}

public sealed class BoolConstraintResultPage : AttributableResultPage
{
}

public sealed class GuidConstraintResultPage : AttributableResultPage
{
}

public sealed class AlphaConstraintResultPage : AttributableResultPage
{
}

public sealed class CatchAllResultPage : AttributableResultPage
{
}
80 changes: 80 additions & 0 deletions 11.0/Navigation/ShellRouteTemplates/src/MainPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ShellRouteTemplates.MainPage"
xmlns:models="clr-namespace:ShellRouteTemplates.Models"
xmlns:viewModels="clr-namespace:ShellRouteTemplates.ViewModels"
x:DataType="viewModels:MainPageViewModel"
Title="Route matrix">
<ScrollView>
<VerticalStackLayout Padding="20" Spacing="16">
<Label
FontAttributes="Bold"
FontSize="28"
SemanticProperties.HeadingLevel="Level1"
Text="Trip route lab" />
<Label
FontSize="16"
Text="Run each absolute URI and verify the value delivered by its route template." />
<Border
Background="#FFF4CE"
Padding="12"
Stroke="#9D5D00"
StrokeShape="RoundRectangle 8">
<Label
AutomationId="AbsoluteOnlyNotice"
Text="Absolute navigation only: every example starts with //routes. Relative navigation with template routes is not supported."
TextColor="#5C2D00" />
</Border>

<VerticalStackLayout
BindableLayout.ItemsSource="{Binding Examples}"
Spacing="12">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="models:RouteExample">
<Border
Padding="14"
Stroke="{AppThemeBinding Light=#D0D0D0, Dark=#505050}"
StrokeShape="RoundRectangle 10">
<Grid
ColumnDefinitions="*,Auto"
ColumnSpacing="12"
RowDefinitions="Auto,Auto,Auto,Auto">
<Label
Grid.ColumnSpan="2"
FontAttributes="Bold"
FontSize="18"
Text="{Binding Form}" />
<Label
Grid.Row="1"
Grid.ColumnSpan="2"
FontFamily="monospace"
Text="{Binding Template}" />
<Label
Grid.Row="2"
Grid.ColumnSpan="2"
FontSize="12"
LineBreakMode="CharacterWrap"
Text="{Binding NavigationUri}" />
<Label
Grid.Row="3"
Margin="0,8,0,0"
Text="{Binding Summary}"
VerticalOptions="Center" />
<Button
Grid.Row="3"
Grid.Column="1"
Margin="8,8,0,0"
AutomationId="{Binding AutomationId}"
Command="{Binding BindingContext.NavigateCommand, Source={RelativeSource AncestorType={x:Type ContentPage}}}"
CommandParameter="{Binding .}"
Text="Run" />
</Grid>
</Border>
</DataTemplate>
</BindableLayout.ItemTemplate>
</VerticalStackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
12 changes: 12 additions & 0 deletions 11.0/Navigation/ShellRouteTemplates/src/MainPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using ShellRouteTemplates.ViewModels;

namespace ShellRouteTemplates;

public partial class MainPage : ContentPage
{
public MainPage(MainPageViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}
29 changes: 29 additions & 0 deletions 11.0/Navigation/ShellRouteTemplates/src/MauiProgram.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.Extensions.Logging;
using ShellRouteTemplates.ViewModels;

namespace ShellRouteTemplates;

public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});

builder.Services.AddSingleton<AppShell>();
builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainPageViewModel>();

#if DEBUG
builder.Logging.AddDebug();
#endif

return builder.Build();
}
}
Loading
Loading