diff --git a/docs/fundamentals/shell/navigation.md b/docs/fundamentals/shell/navigation.md index 5f20801d6a..ba54987871 100644 --- a/docs/fundamentals/shell/navigation.md +++ b/docs/fundamentals/shell/navigation.md @@ -1,7 +1,7 @@ --- title: ".NET MAUI Shell navigation" description: "Learn how .NET MAUI Shell apps can utilize a URI-based navigation experience that permits navigation to any page in the app, without having to follow a set navigation hierarchy." -ms.date: 06/05/2026 +ms.date: 08/11/2026 --- # .NET MAUI Shell navigation @@ -116,6 +116,171 @@ This example enables contextual page navigation, where navigating to the `detail > [!NOTE] > Pages whose routes have been registered with the `Routing.RegisterRoute` method can be deregistered with the `Routing.UnRegisterRoute` method, if required. +::: moniker range=">=net-maui-11.0" + +### Route templates + +Starting in .NET 11, a route that's registered with the `Routing.RegisterRoute` method can include *path parameters*. A route that contains one or more path parameters is known as a *route template*. Path parameters capture part of the navigation URI and deliver it to the destination page as navigation data, which removes the need to encode every value as a query string: + +```csharp +Routing.RegisterRoute("trip/{tripId}", typeof(TripDetailPage)); +``` + +With this registration, navigating to `//routes/trip/SEA-204`, where `routes` is a route in the Shell visual hierarchy, displays `TripDetailPage` and delivers `SEA-204` to it as a navigation parameter named `tripId`. + +Route templates are an additive feature. Routes that don't contain a path parameter are unaffected, and no new API is required to use them. + +> [!NOTE] +> For a sample that exercises each supported route template form, see the [Shell route templates sample](/samples/dotnet/maui-samples/navigation-shell-route-templates). + +#### Route template syntax + +A path parameter is written as a name surrounded by braces, and occupies a single segment of the route unless it's a catch-all parameter. The following table shows the supported forms: + +| Form | Description | Example route | Example URI | +| --- | --- | --- | --- | +| `{name}` | A required parameter that matches exactly one segment. | `trip/{tripId}` | `//routes/trip/SEA-204` | +| `{name?}` | An optional parameter that matches zero or one segment. | `traveler/{name?}` | `//routes/traveler/Ada` or `//routes/traveler` | +| `{name=value}` | A parameter whose default value is delivered when the segment is absent. A default value implies that the parameter is optional. | `rating/{stars=5}` | `//routes/rating/4` or `//routes/rating` | +| `{name:constraint}` | A parameter that only matches when the segment satisfies the constraint. | `reservation/{reservationId:int}` | `//routes/reservation/42` | +| `{*name}` | A catch-all parameter that captures all remaining segments as a single value, separated by `/`. | `files/{*path}` | `//routes/files/trips/SEA-204/receipt.pdf` | +| `prefix{name}suffix` | A mixed segment, where a parameter is matched between literal text within a single segment. | `trip-{tripId}-summary` | `//routes/trip-SEA-204-summary` | + +Forms can be combined. For example, `{reservationId:int?}` declares an optional parameter that must be an integer when it's supplied, and `{stars:int=5}` declares a parameter that must be an integer and that defaults to `5`. + +> [!IMPORTANT] +> A route template is validated when it's registered. If the template is invalid, the `Routing.RegisterRoute` method throws an `ArgumentException` that describes the problem. For more information, see [Route template restrictions](#route-template-restrictions). + +#### Route parameter constraints + +A constraint restricts the values that a path parameter matches, and is appended to the parameter name with a colon. The following constraints are supported: + +| Constraint | Description | Example route | Matches | Doesn't match | +| --- | --- | --- | --- | --- | +| `int` | A 32-bit integer. | `reservation/{reservationId:int}` | `42` | `abc` | +| `long` | A 64-bit integer. | `loyalty/{points:long}` | `9000000000` | `abc` | +| `double` | A number, parsed using the invariant culture. | `budget/{amount:double}` | `1299.50` | `abc` | +| `bool` | `true` or `false`, in any casing. | `toggle/{enabled:bool}` | `true` | `1` | +| `guid` | A GUID. | `booking/{reference:guid}` | `550e8400-e29b-41d4-a716-446655440000` | `12345` | +| `alpha` | One or more letters. | `region/{name:alpha}` | `Pacific` | `Pacific2` | + +No other constraint names are recognized, and a parameter can have at most one constraint. Registering a route with an unrecognized constraint throws an `ArgumentException`. + +A constraint only controls whether a route matches a navigation URI. It doesn't convert the captured value, which is always delivered to the destination page as a `string`. + +> [!NOTE] +> When a URI doesn't satisfy a constraint, the route simply doesn't match. If no other registered route matches the URI, the method throws an `ArgumentException`, in the same way as navigating to any unknown route. + +#### Register route templates + +Route templates are registered with the `Routing.RegisterRoute` method, in the same way as literal routes: + +```csharp +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("booking/{reference:guid}", typeof(GuidConstraintResultPage)); +Routing.RegisterRoute("files/{*path}", typeof(CatchAllResultPage)); +Routing.RegisterRoute("trip-{tripId}-summary", typeof(MixedResultPage)); +``` + +Pages whose routes have been registered as route templates can be deregistered with the `Routing.UnRegisterRoute` method, if required. + +#### Navigate to a route template + +Navigation to a route template is performed with the method, by specifying an absolute URI. As with any global route, the URI starts with a route that's defined in the Shell visual hierarchy, followed by the route template, whose segments supply the values for the path parameters: + +```csharp +await Shell.Current.GoToAsync("//routes/trip/SEA-204"); +``` + +In this example, `routes` is the route of a object in the Shell visual hierarchy, and the `tripId` path parameter of the `trip/{tripId}` template captures the value `SEA-204`. + +> [!IMPORTANT] +> Route templates only work with absolute navigation. Attempting to navigate to a route template with a relative URI, such as `await Shell.Current.GoToAsync("trip/SEA-204")`, throws an `ArgumentException`. + +After navigation completes, the `Shell.Current.CurrentState.Location` property contains the resolved URI, which includes the captured values rather than the template tokens. + +#### Receive path parameter values + +Path parameter values are delivered to the destination page using the same mechanisms as query parameters, so no additional API is required. The class that represents the page being navigated to, or the class for the page's `BindingContext`, can be decorated with a for the parameter: + +```csharp +[QueryProperty(nameof(TripId), "tripId")] +public partial class TripDetailPage : ContentPage +{ + string tripId; + + public string TripId + { + get => tripId; + set + { + tripId = value; + OnPropertyChanged(); + } + } + + public TripDetailPage() + { + InitializeComponent(); + BindingContext = this; + } +} +``` + +Alternatively, the receiving class can implement the interface, where each path parameter appears in the `query` dictionary: + +```csharp +public class TripDetailViewModel : IQueryAttributable +{ + public string TripId { get; private set; } + + public void ApplyQueryAttributes(IDictionary query) + { + if (query.TryGetValue("tripId", out object tripId)) + TripId = tripId.ToString(); + } +} +``` + +The following rules apply to the values that are delivered: + +- The key is the parameter name, without braces, and without any constraint, optional marker, or default value. For example, the key for `{reservationId:int}` is `reservationId`. +- The value is always a `string`, regardless of any constraint that's applied to the parameter. +- Path parameter values are URL decoded before they're delivered, regardless of whether they're received through or . For example, navigating to `//routes/trip/SEA%20204` delivers `SEA 204`. Any constraint is evaluated against the decoded value. This differs from query string values, which are only automatically decoded when they're received through . +- An optional parameter that's absent from the URI, and that has no default value, isn't delivered. +- A parameter that's declared by a route template earlier in the URI is also delivered to pages that are pushed later in the same navigation. + +#### Route matching precedence + +When more than one registered route can match a navigation URI, or when a value is supplied more than once, the following precedence rules apply: + +- A literal route takes precedence over a route template. For example, if both `trip/{tripId}` and `trip/summary` are registered, then navigating to `//routes/trip/summary` displays the page that's registered for `trip/summary`. +- A path parameter takes precedence over a query parameter of the same name. For example, navigating to `//routes/trip/SEA-204?tripId=ignored` delivers `SEA-204`. + +#### Route template restrictions + +The `Routing.RegisterRoute` method throws an `ArgumentException` when a route template violates any of the following rules: + +| Rule | Invalid example | +| --- | --- | +| A parameter must have a name, and the name must be a valid C# identifier. | `trip/{}` | +| A parameter name can only appear once in a route. | `trip/{id}/{id}` | +| A catch-all parameter must be the last segment in a route. | `files/{*path}/details` | +| An optional parameter, or a parameter with a default value, must be the last segment in a route. | `trip/{tripId?}/summary` | +| A constraint must be one of `int`, `long`, `double`, `bool`, `guid`, or `alpha`. | `trip/{tripId:decimal}` | +| A default value must satisfy the constraint that's applied to its parameter. | `reservation/{id:int=abc}` | +| A mixed segment can only contain one parameter, and its braces must be well formed. | `trip-{tripId}-{leg}` | + +In addition, the following limitations apply: + +- A route template can't consist only of a path parameter, such as `{category}`, because such a route can't be matched unambiguously. Navigating to a route that's registered in this way throws an `ArgumentException`. +- A mixed segment supports a constraint, but doesn't support the optional, default value, or catch-all forms. + +::: moniker-end + ## Perform navigation To perform navigation, a reference to the subclass must first be obtained. This reference can be obtained through the `Shell.Current` property. Navigation can then be performed by calling the method on the object. This method navigates to a `ShellNavigationState` and returns a `Task` that will complete once the navigation animation has completed. The `ShellNavigationState` object is constructed by the method, from a `string`, or a `Uri`, and it has its `Location` property set to the `string` or `Uri` argument. @@ -146,9 +311,20 @@ await Shell.Current.GoToAsync("//animals/monkeys"); This example navigates to the page for the `monkeys` route, with the route being defined on a object. The object that represents the `monkeys` route is a child of a object, whose route is `animals`. +::: moniker range="<=net-maui-10.0" + > [!WARNING] > Absolute routes don't work with pages that are registered with the `Routing.RegisterRoute` method. +::: moniker-end + +::: moniker range=">=net-maui-11.0" + +> [!IMPORTANT] +> An absolute URI can't be rooted directly at a page that's registered with the `Routing.RegisterRoute` method. However, it can start with a route in the Shell visual hierarchy and continue into a registered route template. For more information, see [Route templates](#route-templates). + +::: moniker-end + ### Relative routes Navigation can also be performed by specifying a valid relative URI as an argument to the method. The routing system will attempt to match the URI to a object. Therefore, if all the routes in an app are unique, navigation can be performed by only specifying the unique route name as a relative URI. @@ -391,6 +567,12 @@ async void OnCollectionViewSelectionChanged(object sender, SelectionChangedEvent This example retrieves the currently selected elephant in the , and navigates to the `elephantdetails` route, passing `elephantName` as a query parameter. +::: moniker range=">=net-maui-11.0" + +Primitive data can also be passed as part of the navigation URI itself, by registering a route that declares path parameters. For more information, see [Route templates](#route-templates). + +::: moniker-end + ### Pass multiple use object-based navigation data Multiple use object-based navigation data can be passed with a overload that specifies an `IDictionary` argument: diff --git a/docs/migration/custom-renderers.md b/docs/migration/custom-renderers.md index ede9451c0a..8fdd2ae687 100644 --- a/docs/migration/custom-renderers.md +++ b/docs/migration/custom-renderers.md @@ -1,7 +1,7 @@ --- title: "Reuse custom renderers in .NET MAUI" description: "Learn how to adapt Xamarin.Forms custom renderers to work in a .NET MAUI app." -ms.date: 07/08/2026 +ms.date: 08/11/2026 --- # Reuse custom renderers in .NET MAUI @@ -117,6 +117,40 @@ builder.ConfigureMauiHandlers(handlers => }); ``` +### Migrate iOS TabbedPage renderers + +Starting in .NET MAUI 11, uses the `Microsoft.Maui.Handlers.TabbedViewHandler` handler by default on iOS and Mac Catalyst, instead of the `TabbedRenderer` compatibility renderer. Apps with a custom `TabbedRenderer` subclass should migrate those customizations to the handler's property mapper. For more information, see [Customize controls with handlers](~/user-interface/handlers/customize.md). + +If you need the compatibility renderer while migrating, register it explicitly with : + +```csharp +builder.ConfigureMauiHandlers(handlers => +{ +#if IOS || MACCATALYST + handlers.AddHandler(); +#endif +}); +``` + +If you've derived a custom renderer from `TabbedRenderer`, register your own type instead. For more information, see [TabbedPage on iOS and Mac Catalyst](~/user-interface/pages/tabbedpage.md#tabbedpage-on-ios-and-mac-catalyst). + +### Migrate iOS NavigationPage renderers + +Starting in .NET MAUI 11, uses the `Microsoft.Maui.Handlers.NavigationViewHandler` handler by default on iOS and Mac Catalyst, instead of the `NavigationRenderer` compatibility renderer. Apps with a custom `NavigationRenderer` subclass should migrate those customizations to the handler's property mapper. For more information, see [Customize controls with handlers](~/user-interface/handlers/customize.md). + +If you need the compatibility renderer while migrating, register it explicitly with : + +```csharp +builder.ConfigureMauiHandlers(handlers => +{ +#if IOS || MACCATALYST + handlers.AddHandler(); +#endif +}); +``` + +If you've derived a custom renderer from `NavigationRenderer`, register your own type instead. For more information, see [NavigationPage on iOS and Mac Catalyst](~/user-interface/pages/navigationpage.md#navigationpage-on-ios-and-mac-catalyst). + ::: moniker-end ### Consume the custom renderers diff --git a/docs/user-interface/handlers/index.md b/docs/user-interface/handlers/index.md index f3c0da1697..b7952a0238 100644 --- a/docs/user-interface/handlers/index.md +++ b/docs/user-interface/handlers/index.md @@ -115,8 +115,8 @@ The following table lists the types that implement pages in .NET MAUI: | -- | -- | -- | -- | -- | -- | | | | | | | | | | | PhoneFlyoutPageRenderer | | `Mapper` | | -| | | NavigationRenderer | | `Mapper` | | -| | | TabbedRenderer | | `Mapper` | | +| | | | | `Mapper` | | +| | | | | `Mapper` | | | | `ShellHandler` | ShellRenderer | `ShellHandler` | `Mapper` | | > [!NOTE] diff --git a/docs/user-interface/pages/navigationpage.md b/docs/user-interface/pages/navigationpage.md index c3b1b03d47..1244b4d1bc 100644 --- a/docs/user-interface/pages/navigationpage.md +++ b/docs/user-interface/pages/navigationpage.md @@ -1,7 +1,7 @@ --- title: "NavigationPage" description: "The .NET MAUI NavigationPage is used to perform hierarchical navigation through a stack of last-in, first-out (LIFO) pages." -ms.date: 06/05/2026 +ms.date: 08/11/2026 --- # NavigationPage @@ -511,3 +511,44 @@ Alternatively, an extended navigation bar can be suggested by placing some of th > [!TIP] > The `BackButtonTitle`, `Title`, `TitleIconImageSource`, and `TitleView` properties can all define values that occupy space on the navigation bar. While the navigation bar size varies by platform and screen size, setting all of these properties will result in conflicts due to the limited space available. Instead of attempting to use a combination of these properties, you may find that you can better achieve your desired navigation bar design by only setting the `TitleView` property. + +::: moniker range=">=net-maui-11.0" + +## NavigationPage on iOS and Mac Catalyst + +Starting in .NET 11, uses a handler on iOS and Mac Catalyst, rather than the compatibility renderer that was used in previous releases. This aligns iOS and Mac Catalyst with Android, Windows, and Tizen, which already used a handler, and means that uses the same handler architecture as other .NET MAUI controls on every platform. + +This change is enabled by default and isn't gated behind a feature switch. Other platforms are unaffected, because they already used a handler for . + +Existing functionality is preserved by the handler, including the navigation bar, the back button, and pushing and popping pages. + +### Behavior changes + +The handler changes some behavior on iOS and Mac Catalyst, which aligns these platforms with Android and Windows: + +- The event is raised before the page is pushed onto the native navigation stack, rather than after the page becomes visible. Therefore, don't use this event for work that requires the page to be on screen. +- The `NavigatedTo` event is deferred until the page appears. Use this event, rather than , for work that requires the page to be on screen. +- Pushing a page that was previously popped creates a new view controller for the page, rather than reusing the previous one. +- objects whose `Order` property is set to `Secondary` are displayed in an overflow menu in the navigation bar, rather than in a toolbar at the bottom of the page. + +However, apps that subclass the compatibility renderer are affected. A is no longer displayed by `Microsoft.Maui.Controls.Handlers.Compatibility.NavigationRenderer`, and so a custom renderer that derives from it is no longer used unless you register it explicitly. + +### Use the compatibility renderer + +If your app depends on the compatibility renderer, you can continue to use it by registering it for with in your `MauiProgram` class: + +```csharp +builder.ConfigureMauiHandlers(handlers => +{ +#if IOS || MACCATALYST + handlers.AddHandler(); +#endif +}); +``` + +If you've derived a custom renderer from `NavigationRenderer`, register your own type instead. + +> [!IMPORTANT] +> Registering the compatibility renderer is intended as a temporary migration step. You should migrate any custom renderers to the handler architecture. For more information, see [Migrate iOS NavigationPage renderers](~/migration/custom-renderers.md#migrate-ios-navigationpage-renderers). + +::: moniker-end diff --git a/docs/user-interface/pages/tabbedpage.md b/docs/user-interface/pages/tabbedpage.md index 8200bf4678..3e5a3c6144 100644 --- a/docs/user-interface/pages/tabbedpage.md +++ b/docs/user-interface/pages/tabbedpage.md @@ -1,7 +1,7 @@ --- title: "TabbedPage" description: "The .NET MAUI TabbedPage consists of a series of pages that are navigable by tabs across the top or bottom of the page, with each tab loading the page content." -ms.date: 09/30/2024 +ms.date: 08/11/2026 --- # TabbedPage @@ -142,3 +142,43 @@ For more information about performing navigation using the [!WARNING] > While a can be placed in a , it's not recommended to place a into a . + +::: moniker range=">=net-maui-11.0" + +## TabbedPage on iOS and Mac Catalyst + +Starting in .NET 11, uses a handler on iOS and Mac Catalyst, rather than the compatibility renderer that was used in previous releases. This aligns iOS and Mac Catalyst with Android, Windows, and Tizen, which already used a handler, and means that uses the same handler architecture as other .NET MAUI controls on every platform. + +This change is enabled by default and isn't gated behind a feature switch. Other platforms are unaffected, because they already used a handler for . + +> [!NOTE] +> The control also moved from a compatibility renderer to a handler on iOS and Mac Catalyst in .NET 11. For more information, see [NavigationPage on iOS and Mac Catalyst](navigationpage.md#navigationpage-on-ios-and-mac-catalyst). + +Existing functionality is preserved by the handler, including: + +- Tab titles, icons, and selection, and adding or removing child pages at runtime. +- The `BarBackground`, `BarBackgroundColor`, `BarTextColor`, `SelectedTabColor`, and `UnselectedTabColor` properties. +- The **More** tab, which appears when there are more than five tabs. +- The `TranslucencyMode` platform-specific. For more information, see [TabbedPage translucent tab bar on iOS](~/ios/platform-specifics/tabbedpage-translucent-tabbar.md). + +However, apps that subclass the compatibility renderer are affected. A is no longer displayed by `Microsoft.Maui.Controls.Handlers.Compatibility.TabbedRenderer`, and so a custom renderer that derives from it is no longer used unless you register it explicitly. + +### Use the compatibility renderer + +If your app depends on the compatibility renderer, you can continue to use it by registering it for with in your `MauiProgram` class: + +```csharp +builder.ConfigureMauiHandlers(handlers => +{ +#if IOS || MACCATALYST + handlers.AddHandler(); +#endif +}); +``` + +If you've derived a custom renderer from `TabbedRenderer`, register your own type instead. + +> [!IMPORTANT] +> Registering the compatibility renderer is intended as a temporary migration step. You should migrate any custom renderers to the handler architecture. For more information, see [Migrate iOS TabbedPage renderers](~/migration/custom-renderers.md#migrate-ios-tabbedpage-renderers). + +::: moniker-end