Shell-navigering och djuplänkning i .NET MAUI: Universal Links och App Links 2026

Så konfigurerar du Shell-navigering och djuplänkar i .NET MAUI 10 med App Links på Android och Universal Links på iOS. Kompletta kodexempel för rutregistrering, IQueryAttributable och testning.

Djuplänkar i .NET MAUI: Guide 2026

Uppdaterad: 18 juli 2026

Shell-navigering i .NET MAUI är ett URI-baserat navigeringssystem där du flyttar användaren mellan sidor med strängar som //home/products?sku=42 istället för att manuellt hantera en NavigationPage-stack. Från och med .NET 10 är Shell den rekommenderade grunden för djuplänkning, och App Links på Android och Universal Links på iOS mappas direkt mot dina registrerade rutter. Så, låt oss gå igenom rutregistrering, parameteröverföring, plattformskonfiguration och de fallgropar jag har snubblat på i tre produktionsappar.

  • Shell använder URI-baserad navigering (//main/details?id=42) som skalar bättre än Navigation.PushAsync för appar med fler än tre sidor.
  • Registrera dolda sidor med Routing.RegisterRoute och navigera med Shell.Current.GoToAsync. Absoluta rutter börjar med //, relativa utan.
  • Query-parametrar levereras till mottagarsidor via IQueryAttributable.ApplyQueryAttributes. Undvik statiska singleton-hack för att skicka data.
  • App Links på Android kräver en assetlinks.json-fil som serveras från https://din-domän/.well-known/assetlinks.json med rätt SHA-256-fingeravtryck.
  • Universal Links på iOS kräver en apple-app-site-association-fil med rätt Team ID plus bundle-identifierare och com.apple.developer.associated-domains-entitlement.
  • Testa lokalt med adb shell am start -a android.intent.action.VIEW -d "https://din-domän/product/42" och xcrun simctl openurl booted på iOS-simulatorn.

Vad är Shell-navigering i .NET MAUI?

Shell är en toppnivåcontainer i .NET MAUI som samlar tre saker som annars kräver mycket boilerplate: en flikbaserad eller flyout-baserad navigeringsstruktur, en URI-router som mappar strängar till sidtyper, och en integrerad livscykelhantering för sidparametrar. Om du kommer från Xamarin.Forms har du förmodligen skrivit await Navigation.PushAsync(new DetailsPage(product)) hundra gånger. Shell-motsvarigheten är await Shell.Current.GoToAsync($"details?sku={product.Sku}"), och sidan konstrueras av DI-containern med parametrar hydrerade automatiskt.

Den stora vinsten är inte kortare kod utan att URI-modellen ger dig djuplänkning "gratis". När en användare klickar på https://minshop.com/product/42 i ett e-postmeddelande vill operativsystemet öppna din app och landa på just den produktvyn. Med en traditionell navigeringsstack måste du plocka isär URL:en, återskapa den logiska stacken från roten och pusha varje mellanliggande sida. Med Shell mappas hela URL:en till ett enda GoToAsync-anrop, och rätt fliknav, mellansida och detaljvy poppas upp åt dig.

Shell är också där MAUI-teamet lägger sin utveckling. Nyare funktioner (som stödet för ShellContent-baserad Blazor Hybrid-hosting och den nya Shell.NavigationPageProperty-slotten som kom i .NET 10) kommer först till Shell och sist, om alls, till det gamla NavigationPage-API:t.

Registrera rutter och navigera med GoToAsync

Rutter i Shell delas in i två kategorier: visuella rutter som deklareras i XAML som ShellContent-element och representerar toppnivåflikar/flyout-poster, och dolda rutter som registreras i kod och representerar sidor du navigerar in i från en annan sida. En typisk AppShell.xaml ser ut så här:

<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:views="clr-namespace:MinApp.Views"
       x:Class="MinApp.AppShell"
       FlyoutBehavior="Disabled">

    <TabBar>
        <ShellContent Title="Hem"
                      Route="home"
                      Icon="tab_home.png"
                      ContentTemplate="{DataTemplate views:HomePage}" />
        <ShellContent Title="Konto"
                      Route="account"
                      Icon="tab_account.png"
                      ContentTemplate="{DataTemplate views:AccountPage}" />
    </TabBar>
</Shell>

Dolda rutter registrerar du i AppShell.xaml.cs-konstruktorn, inte i MauiProgram.cs, eftersom Shell inte har initialiserats där än:

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();

        // Dolda rutter, navigerbara från vilken sida som helst
        Routing.RegisterRoute("product", typeof(ProductDetailsPage));
        Routing.RegisterRoute("product/reviews", typeof(ProductReviewsPage));
        Routing.RegisterRoute("checkout", typeof(CheckoutPage));
        Routing.RegisterRoute("checkout/payment", typeof(PaymentPage));
    }
}

Nu kan du navigera från vilken vymodell som helst. Skillnaden mellan absoluta och relativa rutter är, ärligt talat, den vanligaste källan till "varför landar jag på fel sida"-buggar:

// Absolut rutt: nollställer navigeringsstacken och landar på home > product
await Shell.Current.GoToAsync("//home/product?sku=42");

// Relativ rutt: pushar ovanpå aktuell sida
await Shell.Current.GoToAsync("product?sku=42");

// Gå tillbaka en nivå
await Shell.Current.GoToAsync("..");

// Gå tillbaka två nivåer och skicka data till mottagaren
await Shell.Current.GoToAsync("../..?refresh=true");

Skicka data mellan sidor med IQueryAttributable

Ett av de vanligaste misstagen jag ser hos team som migrerar från Xamarin är att de fortsätter skicka objekt via konstruktorer eller statiska singletons. I Shell är den kanoniska vägen IQueryAttributable. Låt navigeringsmotorn hydrera din vymodell med query-parametrar:

public partial class ProductDetailsViewModel : ObservableObject, IQueryAttributable
{
    private readonly IProductService _productService;

    [ObservableProperty]
    private Product? _product;

    [ObservableProperty]
    private bool _isLoading;

    public ProductDetailsViewModel(IProductService productService)
    {
        _productService = productService;
    }

    public async void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.TryGetValue("sku", out var skuObj) &&
            skuObj is string sku)
        {
            IsLoading = true;
            Product = await _productService.GetBySkuAsync(sku);
            IsLoading = false;
        }
    }
}

Två saker som är lätta att missa. Värden i query-ordboken är alltid strängar när de kommer från URI:er (så du måste parsa själv för int/Guid), och ApplyQueryAttributes anropas innan OnAppearing. Det innebär att du inte kan förlita dig på att UI:et redan är monterat. För komplexa objekt som inte kan serialiseras till en URI, använd en Query-parameter som är en identifierare och slå upp objektet från ett DI-injicerat repository, precis som exemplet ovan gör med SKU.

Shell stöder båda navigeringsstilarna, men det är inte alltid uppenbart vilken som passar. Push-navigering (standard) lägger den nya sidan ovanpå stacken och visar en tillbaka-pil. Modal-navigering visar sidan som en helskärmsdialog utan tillbaka-pil, så användaren måste explicit stänga den. Du utlöser modal-läge genom prefixet Modal i ruttdefinitionen eller genom PresentationMode-attributet:

// Push (standard)
await Shell.Current.GoToAsync("product?sku=42");

// Modal: öppnas som overlay
await Shell.Current.GoToAsync("product?sku=42", new Dictionary<string, object>
{
    { "PresentationMode", PresentationMode.ModalAnimated }
});
KriteriumPush-navigeringModal-navigering
Använd närAnvändaren utforskar hierarkiskt (lista, detalj)Kortlivad uppgift som måste slutföras eller avbrytas
Tillbaka-pilJa, automatisktNej. Du måste rendera egen "Klar"/"Avbryt"-knapp
Systemets tillbaka-gestFungerarFungerar inte på iOS, fungerar på Android
Flik-nav synligJaNej, helskärm
Djuplänknings-vänligJaNej. Modal-rutter är svåra att öppna från extern URL
Typiska exempelProduktvy, meddelandetråd, inställningskategoriKassaflöde, kameravy, redigerarens fullskärmsläge

Regeln jag följer i praktiken är enkel. Om användaren kan tänkas landa här via en djuplänk från e-post eller en push-notis, ska det vara push. Om det är ett tillfälligt flöde som avslutas när användaren är klar (bekräfta beställning, ta bild, redigera avatar), är modal rätt val. Ett vanligt fel är att bygga hela kassaflödet som modaler. Det gör att du inte kan djuplänka in i "steg 3" från en avbruten-beställning-e-post, vilket är precis det du vill kunna göra.

Android App Links är verifierade djuplänkar. Android kontrollerar en signerad JSON-fil på din domän för att bekräfta att din app har rätt att hantera dess URL:er. Utan denna verifiering visas den vanliga "Öppna med"-dialogen där användaren måste välja mellan din app och webbläsaren. Konfigurationen har tre delar: manifest-intent-filter, en assetlinks.json-fil på servern, och kodhantering av inkommande intent.

Steg 1: Lägg till intent-filter i Platforms/Android/MainActivity.cs:

[Activity(Theme = "@style/Maui.SplashTheme",
          MainLauncher = true,
          LaunchMode = LaunchMode.SingleTop,
          ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
[IntentFilter(new[] { Intent.ActionView },
    Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
    DataScheme = "https",
    DataHost = "minshop.com",
    DataPathPrefix = "/product/",
    AutoVerify = true)]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnNewIntent(Intent? intent)
    {
        base.OnNewIntent(intent);
        HandleAppLink(intent);
    }

    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        HandleAppLink(Intent);
    }

    private void HandleAppLink(Intent? intent)
    {
        if (intent?.Action != Intent.ActionView || intent.Data is null)
            return;

        var uri = intent.Data.ToString();
        // "https://minshop.com/product/42" -> "//home/product?sku=42"
        var segments = intent.Data.PathSegments;
        if (segments?.Count == 2 && segments[0] == "product")
        {
            var sku = segments[1];
            Shell.Current.GoToAsync($"//home/product?sku={sku}");
        }
    }
}

Steg 2: Generera och publicera assetlinks.json:

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.minforetag.minshop",
      "sha256_cert_fingerprints": [
        "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
      ]
    }
  }
]

Filen måste serveras från https://minshop.com/.well-known/assetlinks.json med HTTPS, ha MIME-typ application/json, och SHA-256-fingeravtrycket måste matcha din signeringsnyckel (både debug- och release-keystore om du vill att App Links ska fungera i utveckling). Hämta ditt fingeravtryck med keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android. Läs Androids officiella App Links-verifieringsguide för fullständiga krav.

Universal Links är Apples motsvarighet till App Links och kräver även de en verifieringsfil på servern samt en app-side entitlement. Systemet är strängare på iOS. Om något är fel öppnas länken bara i Safari utan felmeddelande, vilket gör felsökning frustrerande (jag har suttit uppe till 02:00 med precis det scenariot).

Steg 1: Aktivera Associated Domains i Platforms/iOS/Entitlements.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:minshop.com</string>
        <string>applinks:www.minshop.com</string>
    </array>
</dict>
</plist>

Steg 2: Publicera apple-app-site-association-filen (utan filändelse):

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCDE12345.com.minforetag.minshop"],
        "components": [
          {
            "/": "/product/*",
            "comment": "Matcha alla produktsidor"
          },
          {
            "/": "/order/*",
            "comment": "Matcha orderbekräftelser"
          }
        ]
      }
    ]
  }
}

Filen måste serveras från https://minshop.com/.well-known/apple-app-site-association, ha MIME-typ application/json, och servern får inte göra omdirigeringar. Strängen ABCDE12345 är ditt Team ID (finns i Apple Developer-portalen). Testa att filen är giltig med Apples officiella dokumentation för associated domains.

Steg 3: Hantera inkommande länk i AppDelegate.cs:

public override bool ContinueUserActivity(UIApplication application,
    NSUserActivity userActivity,
    UIApplicationRestorationHandler completionHandler)
{
    if (userActivity.ActivityType == NSUserActivityType.BrowsingWeb &&
        userActivity.WebPageUrl is NSUrl url)
    {
        var path = url.Path?.TrimStart('/').Split('/');
        if (path?.Length == 2 && path[0] == "product")
        {
            var sku = path[1];
            MainThread.BeginInvokeOnMainThread(() =>
                Shell.Current.GoToAsync($"//home/product?sku={sku}"));
        }
    }
    return true;
}

Så testar du djuplänkar under utveckling

Att vänta på att någon skickar dig ett e-post och sedan klicka på länken är, ärligt talat, det långsammaste sättet att felsöka djuplänkning. Här är kommandona jag har på min klippbok:

Android via ADB:

# Testa https-länk (kräver att App Links är verifierat)
adb shell am start -a android.intent.action.VIEW \
    -d "https://minshop.com/product/42" \
    com.minforetag.minshop

# Verifiera att din app är kopplad till domänen
adb shell pm get-app-links com.minforetag.minshop

# Om verifieringen misslyckas, tvinga en ny check
adb shell pm verify-app-links --re-verify com.minforetag.minshop

iOS via simulator:

# Öppna länk i simulatorn
xcrun simctl openurl booted "https://minshop.com/product/42"

# Kontrollera att entitlement är korrekt
codesign -d --entitlements :- MinApp.app

På en fysisk iOS-enhet kan du testa genom att skicka länken via Meddelanden till dig själv och klicka på den. Om Safari öppnas i stället för din app är det nästan alltid ett problem med apple-app-site-association. Läs Microsofts officiella Shell-navigeringsdokumentation för det senaste om URI-hanteringen.

Vanliga fallgropar och felsökning

Efter att ha byggt djuplänkningsflöden i tre produktionsappar har jag sett samma buggar dyka upp gång på gång. Här är de fem vanligaste och hur du undviker dem.

1. Rutter registreras för sent

Om du registrerar rutter i MauiProgram.cs eller i en sidas konstruktor får du ArgumentException: Route ... not registered. Registrera alltid i AppShell-konstruktorn, den körs innan någon navigering initieras.

2. Absolut kontra relativ rutt-förvirring

Ett vanligt fel är GoToAsync("home/product") när du menar GoToAsync("//home/product"). Utan //-prefixet försöker Shell hitta "home/product" som en dold rutt i aktuellt sammanhang och kastar ett undantag. Om djuplänken landar på fel sida, kolla prefixet först.

3. IQueryAttributable körs innan konstruktor är klar

Om din vymodell använder DI för att hämta tjänster och du också anropar dem i ApplyQueryAttributes, se till att DI-registreringen är transient eller scoped, inte singleton. Annars återanvänds instansen mellan navigeringar och gammal data läcker in. Detta är också viktigt när du kombinerar Shell med Xamarin-till-MAUI-migreringens vymodell-omstruktureringssteg.

4. App Links verifieras inte i debug-läge

Ditt debug-fingeravtryck är inte samma som ditt release-fingeravtryck. Om App Links fungerar i debug men inte i release (eller tvärtom), lägg till båda fingeravtrycken i assetlinks.json. Google-verifieringsservern kör en check varje gång appen installeras, så en enda fil med båda fingeravtrycken täcker båda byggena.

5. Tillbaka-gest lämnar användaren strandad

När en användare öppnar din app via en djuplänk och trycker på tillbaka, förväntar de sig ofta att komma till startsidan, inte att appen ska stängas. Använd en absolut rutt i GoToAsync så att stacken innehåller //home som rot, och lyssna på Shells Navigated-händelse för att logga navigeringsflöden i telemetrin. Detta liknar mönstret för att göra applikationen tillgänglig via SemanticProperties, där du också måste tänka på hur assistiv teknik navigerar tillbaka.

För appar med aggressiv startprestandaoptimering, särskilt när du kör Native AOT i .NET MAUI 10, bör du också verifiera att djuplänkningskoden inte trimas bort. Ruttregistrering via reflektion kräver ofta DynamicallyAccessedMembers-annoteringar eller explicita TrimmerRootAssembly-poster i projektfilen för att fungera efter AOT-kompilering.

Vanliga frågor

Kan jag använda Shell utan flikar eller flyout?

Ja. Sätt FlyoutBehavior="Disabled" och använd bara ShellContent-element utan TabBar. Du får ändå URI-navigering, ruttregistrering och djuplänkning, utan visuell chrome.

Vad är skillnaden mellan Shell-navigering och NavigationPage?

NavigationPage är stackbaserat och kräver att du manuellt hanterar push/pop mellan sidor. Shell är URI-baserat, hanterar stacken åt dig och integrerar djuplänkning, flikar och flyout i ett enhetligt API. Nya MAUI-appar bör börja med Shell.

Varför fungerar min djuplänk i utveckling men inte i produktion?

Nästan alltid ett fingeravtryck som saknas i assetlinks.json (Android) eller ett fel Team ID i apple-app-site-association (iOS). Kontrollera att release-nyckelns SHA-256 finns i JSON-filen och att MIME-typen är application/json utan omdirigeringar.

Hur skickar jag komplexa objekt mellan sidor i Shell?

Det gör du inte. Skicka en identifierare (ID, SKU, GUID) som query-parameter och låt mottagarsidans vymodell slå upp objektet från ett DI-injicerat repository. Detta gör flödet djuplänkningsvänligt eftersom en URL bara kan bära serialiserbara värden.

Fungerar Shell-navigering med .NET MAUI Blazor Hybrid?

Ja, du kan hosta en BlazorWebView inuti en Shell-sida och blanda XAML-sidor med Blazor-komponenter. Från och med .NET 10 stöds även routning från Shell direkt till Blazor-rutter via ShellContent med BlazorRoute-attribut.

Marcus Chen
Om Författaren Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.