Shell Navigation i .NET MAUI: Komplet Guide til Routing, Flyout og TabBar
Mestrer Shell navigation i .NET MAUI 9 med URI-routing, GoToAsync, query-parametre, TabBar, flyout-menuer og deep links. Komplet guide med kodeeksempler og best practices fra produktion.
Shell navigation i .NET MAUI er en URI-baseret navigations-API, der lader dig flytte mellem sider via strenge som //main/products?id=42 i stedet for at instantiere Page-objekter manuelt. Shell håndterer flyout-menuer, TabBar, modale visninger, query-parametre og deep links med ét sammenhængende objekt. I .NET MAUI 9 er Shell den anbefalede navigationsmodel for de fleste apps, fordi den giver dig en konsistent app-skal, automatisk visuelt hierarki og indbygget understøttelse af både telefon- og tablet-layouts uden, at du behøver kode platformsspecifik logik.
Shell bruger URI-baseret routing (//route/subroute?param=value) i stedet for manuel Page-instantiering, hvilket gør dyb navigation og deep links trivielt.
En AppShell.xaml definerer hele app-strukturen: FlyoutItem, TabBar, Tab og ShellContent erstatter NavigationPage, TabbedPage og FlyoutPage.
Query-parametre overføres via [QueryProperty]-attributter eller IQueryAttributable-interfacet, og sidstnævnte er den anbefalede pattern siden .NET 8.
Modale sider åbnes med præfikset // eller via Shell.PresentationMode; husk at registrere ruter med Routing.RegisterRoute.
Deep links fungerer ud af boksen, fordi system-URIs som myapp://products/42 ruter direkte til Shell-ruter med korrekt parameter-binding.
Shell understøtter dependency injection ved konstruktion: registrer dine sider og ViewModels i MauiProgram.cs for automatisk DI på navigation.
Hvad er Shell i .NET MAUI?
Shell er en container-klasse i .NET MAUI, der definerer hele din apps visuelle hierarki og navigationsmodel i én XAML-fil. I stedet for at konfigurere NavigationPage, TabbedPage og FlyoutPage manuelt, beskriver du strukturen deklarativt: hvilke sider er top-niveau, hvilke er flyout-elementer, hvilke er faner, og hvilke er modale. Shell genererer den korrekte platforms-UI på iOS, Android, macOS og Windows uden, at du behøver røre platformskode.
Den vigtigste mentale model er, at Shell ligner en webside med ruter. Du registrerer ruter ved navn, og du navigerer via URI-strenge som //orders/details?id=42. Det betyder, at du kan deep-linke til hvilken som helst side, gemme navigations-stien som en streng i state, og poppe tilbage til en specifik rute uden manuelt at holde styr på sidernes hukommelse. Shell håndterer også sidernes livscyklus: en side instantieres, når du navigerer til den, og frigøres som standard, når du navigerer væk (medmindre du vælger at cache den).
Shell er bygget oven på MAUI's VisualElement-hierarki og integrerer pænt med MVVM-arkitekturen i .NET MAUI, så ViewModels kan injiceres direkte i Shell-sider via dependency injection.
Sådan opretter du din første AppShell
En ny .NET MAUI-projektskabelon genererer allerede en AppShell.xaml-fil. Hvis du opgraderer fra Xamarin.Forms eller starter manuelt, skal du oprette en klasse, der arver fra Shell, og sætte den som MainPage i App.xaml.cs. Den minimale struktur ser sådan ud:
Når Shell instantierer en side, slår den den op i DI-containeren, så constructor-injection bare virker. Det er nøglen til ren, testbar kode. Se også vores guide til dependency injection i .NET MAUI for de fulde detaljer om service-lifetimes.
URI-baseret routing og GoToAsync
Den primære måde at navigere i Shell på er via Shell.Current.GoToAsync(), der tager en URI-streng. Der er tre typer ruter:
Absolutte ruter (//route): nulstiller navigationsstacken og går direkte til ruten. Bruges typisk efter login eller logout.
Relative ruter (route eller route/subroute): pusher en ny side oven på den nuværende.
Tilbage-navigation (..): popper én side; ../.. popper to.
For at navigere til en side, der ikke er top-niveau (altså ikke direkte refereret i AppShell.xaml), skal du først registrere ruten med Routing.RegisterRoute. Det gøres typisk i AppShell's constructor:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("orders/details", typeof(OrderDetailsPage));
Routing.RegisterRoute("products/edit", typeof(ProductEditPage));
Routing.RegisterRoute("settings/profile", typeof(ProfilePage));
}
}
Shell understøtter to mønstre til at sende data mellem sider: [QueryProperty]-attributter (simpel) og IQueryAttributable-interfacet (anbefalet siden .NET 8). Begge fungerer ved at lytte til query-strengen i URI'en.
Mønster 1, QueryProperty:
[QueryProperty(nameof(OrderId), "id")]
public partial class OrderDetailsViewModel : ObservableObject
{
[ObservableProperty]
private int orderId;
partial void OnOrderIdChanged(int value)
{
// Indlæs ordren når id'et er sat
_ = LoadOrderAsync(value);
}
}
Mønster 2, IQueryAttributable (anbefalet):
public partial class OrderDetailsViewModel : ObservableObject, IQueryAttributable
{
[ObservableProperty]
private Order order;
public async void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("id", out var idObj) && int.TryParse(idObj?.ToString(), out var id))
{
Order = await _orderService.GetByIdAsync(id);
}
}
}
Fordelen ved IQueryAttributable er, at alle parametre kommer ind på én gang i en dictionary, så du undgår race conditions, hvor én property sættes før en anden. Honestly, jeg løb ind i præcis den race-bug i mit sidste projekt, og skiftet til IQueryAttributable fjernede en hel kategori af mystiske null-fejl. Det fungerer også med komplekse objekter, hvis du sender dem via GoToAsync's overload, der tager en Dictionary<string, object>:
var navigationParameter = new Dictionary<string, object>
{
{ "order", existingOrder },
{ "mode", "edit" }
};
await Shell.Current.GoToAsync("orders/details", navigationParameter);
Flyout-menu: design, ikoner og betingede elementer
En flyout-menu (også kaldet "hamburger-menu") aktiveres automatisk, hvis din Shell indeholder mindst ét FlyoutItem. Du kan tilpasse udseendet med Shell.FlyoutBehavior (Flyout, Locked eller Disabled), header-template og baggrundsfarve.
For at skjule et flyout-element baseret på brugerens rolle, bind FlyoutItem.IsVisible til en property på en ViewModel, der er sat som Shell's BindingContext. Du kan også vise et flyout-element kun på bestemte enheder ved at bruge OnIdiom-markup.
TabBar og Tab: bund-navigation der virker
Hvis din app primært bruger bundfaner (som de fleste forbruger-apps gør), skal du bruge TabBar i stedet for, eller sammen med, FlyoutItem. En TabBar placeret som rod-element i Shell skjuler flyout'en helt og viser kun bundfaner:
Du kan kombinere FlyoutItem med interne Tab-elementer for at få top-faner inden i en flyout-sektion. Det er typisk for apps med komplekse navigationshierarkier:
TabBar'en kan styles via Shell.TabBarBackgroundColor, Shell.TabBarForegroundColor og Shell.TabBarTitleColor. Sæt dem som attached properties på Shell eller på individuelle Tab-elementer for at få sektion-specifik styling.
Modal-navigation og PresentationMode
For at åbne en side modalt (som overlay over hele appen, typisk uden navigationsbar) bruger du Shell.PresentationMode="ModalAnimated" i XAML eller præfikset // i din navigation. Den anbefalede metode er at sætte PresentationMode på selve siden:
For at lukke modalen, kald GoToAsync(".."). Andre PresentationMode-værdier inkluderer Animated, NotAnimated, Modal (uden animation) og ModalNotAnimated. På iOS respekterer modale sider sheet-stilen, på Android vises de fuldskærm. Detaljerne om hvordan PresentationMode mappes på hver platform er dokumenteret i .NET MAUI's API-reference for PresentationMode.
Deep links og external URI handling
Shell har indbygget understøttelse af deep links. Når dit app-projekt har et URL-scheme konfigureret (f.eks. myapp:// på iOS via Info.plist eller en intent-filter på Android), kan Shell automatisk rute indkommende URIs til registrerede ruter.
Tilføj denne kode i AppShell.xaml.cs for at håndtere indkommende deep links:
protected override async void OnNavigated(ShellNavigatedEventArgs args)
{
base.OnNavigated(args);
// Log navigation til analytics eller analyse
Console.WriteLine($"Naviget til: {args.Current.Location}");
}
public async Task HandleDeepLinkAsync(Uri uri)
{
// myapp://orders/42 → //orders/details?id=42
var path = uri.AbsolutePath.TrimStart('/');
var segments = path.Split('/');
if (segments.Length == 2 && segments[0] == "orders")
{
await Shell.Current.GoToAsync($"//orders/details?id={segments[1]}");
}
}
For at understøtte Universal Links på iOS og App Links på Android skal du konfigurere associerede domæner i platformsspecifikke projektfiler. Microsofts officielle Shell navigation-dokumentation har den fulde opskrift, og .NET MAUI's GitHub-repo har desuden samples, du kan låne fra direkte.
Forskellen på Shell og NavigationPage
Shell og NavigationPage løser begge problemet med side-til-side navigation, men de gør det på fundamentalt forskellige måder. Den vigtigste forskel er, at Shell er en helhedsmodel for hele appens UI-hierarki, mens NavigationPage kun håndterer én navigations-stack.
Funktion
Shell
NavigationPage
Navigationsmodel
URI-baseret (//route?param=value)
Stack-baseret (PushAsync(page))
Flyout-menu
Indbygget via FlyoutItem
Kræver FlyoutPage separat
TabBar
Indbygget via TabBar
Kræver TabbedPage separat
Deep links
Native understøttelse
Manuel implementation
Query-parametre
Indbygget via IQueryAttributable
Manuel parameter-passing
Søgefelt
SearchHandler indbygget
Skal bygges fra bunden
Læringskurve
Stejlere, men mere kraftfuld
Lavere, men mere kode på sigt
Anbefalet til
Apps med flere sektioner eller deep links
Simple apps eller specialiserede flows
I praksis bruger de fleste produktions-MAUI-apps Shell som hovedstruktur, men falder tilbage til NavigationPage indeni Shell for specielle flows (f.eks. en wizard med flere trin), hvor URI-baseret routing er overkill.
Best practices og almindelige fejl
Efter at have shippet flere produktions-MAUI-apps med Shell, er her de mønstre, jeg konsekvent griber til, og fælderne jeg har lært at undgå.
1. Centralisér rute-konstanter
Lav en statisk klasse, der eksponerer alle dine ruter som const string. Det giver dig refactoring-sikkerhed og auto-complete:
public static class Routes
{
public const string Home = "//home";
public const string OrderDetails = "orders/details";
public const string Login = "login";
public static string OrderDetailsWithId(int id) => $"{OrderDetails}?id={id}";
}
2. Brug en NavigationService til ViewModels
Direkte kald til Shell.Current.GoToAsync() i ViewModels kobler dem til UI-laget og gør dem svære at unit-teste. Wrap det i et interface, der injiceres via DI. Jeg har endt med at gøre det her i hver eneste MAUI-app, jeg har bygget, fordi smerten med at mocke Shell.Current i tests bare ikke er værd at lide.
3. Håndter tilbage-knappen korrekt
Brug Shell.BackButtonBehavior til at tilpasse adfærden. Husk at override OnBackButtonPressed, hvis du har usaved changes. Det er en klassisk UX-fejl at miste brugerens arbejde.
4. Optimer side-livscyklus
Shell instantierer som standard en ny side ved hver navigation. For tunge sider (med billeder, CollectionViews eller data-loading) kan du cache sidens content. Se vores guide til ydeevneoptimering i .NET MAUI for konkrete teknikker.
5. Test deep links systematisk
Brug adb shell am start -W -a android.intent.action.VIEW -d "myapp://orders/42" på Android og xcrun simctl openurl booted "myapp://orders/42" på iOS-simulatoren. Hvis du ikke tester de her manuelt, opdager du først bugs i produktion. Jeg ramte præcis det scenarie ved release nummer to af en kunde-app, og det var ingen sjov debug-aften.
Ofte stillede spørgsmål
Hvordan navigerer jeg tilbage flere sider på én gang i Shell?
Brug ".." for hver side, du vil poppe: await Shell.Current.GoToAsync("../..") går to sider tilbage. For at nulstille hele stacken og gå til en bestemt rute, brug en absolut rute med //-præfiks: await Shell.Current.GoToAsync("//home").
Kan jeg bruge Shell sammen med NavigationPage?
Ja. Du kan wrappe en ShellContent i en NavigationPage, hvis du har brug for stack-baseret navigation indenfor en Shell-sektion. Det er nyttigt til wizards eller specielle flows, hvor URI-routing ikke giver mening. De fleste apps har dog ikke brug for det.
Hvorfor virker mine query-parametre ikke i .NET MAUI 9?
Den hyppigste årsag er, at BindingContext ikke er sat til den ViewModel, der implementerer IQueryAttributable. Sørg for, at din side får sin ViewModel via constructor-injection, og at DI er konfigureret korrekt. Tjek også, at parameter-navnene i query-strengen matcher præcist (case-sensitive).
Hvordan skjuler jeg flyout-menuen på en bestemt side?
Sæt Shell.FlyoutBehavior="Disabled" som en attached property direkte på siden. For at skjule navigationsbaren helt, brug Shell.NavBarIsVisible="False". Begge værdier kan også sættes programmatisk fra code-behind eller en ViewModel via Shell.SetFlyoutBehavior.
Understøtter Shell tablet-layouts med master-detail?
Ja. På iPad og Android-tablets viser Shell automatisk flyout-menuen som en sidebar, mens den på telefoner vises som en udtrækkelig menu. Du kan tvinge adfærden med Shell.FlyoutBehavior="Locked" for at have menuen altid synlig, eller bruge OnIdiom-markup til mere granulær kontrol.
Sådan opsætter du type-safe REST API-integration i .NET MAUI med IHttpClientFactory, Refit og Polly. Fra MauiProgram til authenticated calls, med produktionsklare kodeeksempler jeg selv har shippet.
Lær at bygge en hurtig, sikker og vedligeholdbar lokal database i .NET MAUI med enten sqlite-net-pcl eller Entity Framework Core 9. Inkluderer setup, repository-mønster, migrationer, SQLCipher-kryptering og performance-tips med fungerende C#-eksempler.