.NET MAUI Shell on yhden tiedoston navigointimalli, joka kokoaa sovelluksen koko rakenteen (välilehdet, flyout-valikon, reitit ja URI-pohjaisen navigoinnin) yhden AppShell.xaml-tiedoston alle. Tämä opas näyttää, miten rakennat tuotantokelpoisen Shell-pohjaisen .NET MAUI 9 -sovelluksen vuonna 2026: TabBar- ja Flyout-rakenteet, reittien rekisteröinti, parametrien välitys IQueryAttributable-rajapinnalla, deep linking sekä yleisimmät virhetilanteet ratkaisuineen.
Olen rakentanut viimeisen kahden vuoden aikana kolme tuotantosovellusta Shell-rakenteella, ja honestly, ensimmäinen niistä oli pieni katastrofi. Sekoitin absoluuttiset ja suhteelliset reitit ja vietin yhden iltapäivän miettien, miksi takaisin-painike vei käyttäjän aina väärälle välilehdelle. Tässä oppaassa käyn läpi ne kompastuskivet, joista olisin halunnut kuulla etukäteen.
Shell yhdistää välilehdet, flyout-valikon ja reitityksen yhteen XAML-tiedostoon, ja vähentää navigointikoodin määrää 60–80 % verrattuna NavigationPage-pohjaiseen ratkaisuun.
URI-pohjainen navigointi Shell.Current.GoToAsync("//main/details?id=42") tukee sekä absoluuttisia että suhteellisia polkuja ja toimii myös deep link -lähteenä.
Parametrit välitetään IQueryAttributable-rajapinnan kautta tai [QueryProperty]-attribuutilla. Tietokohtaiset objektit kannattaa lähettää sanakirjana, eivät JSON-merkkijonoina.
Modaali- ja työntövaiheen valinta tehdään Routing.RegisterRoute-kutsussa eikä navigointihetkellä. Tämä on yleisin sudenkuoppa migrointivaiheessa.
Deep linking iOS- ja Android-puolella vaatii AppLinks-konfiguraation ja Shell.Current.GoToAsync-kutsun käsittelyn OnAppLinkRequestReceived-metodissa.
.NET MAUI 9 -version Shell tukee nyt sisäänrakennettua BackButtonBehavior-yliajoa ja parannettua Flyout-renderöintiä Androidilla.
Mikä .NET MAUI Shell on ja milloin sitä kannattaa käyttää?
.NET MAUI Shell on Microsoftin tarjoama korkean tason navigointiabstraktio, joka korvaa perinteisen NavigationPage- ja TabbedPage-yhdistelmän yhdellä deklaratiivisella mallilla. Shell vastaa sovelluksen visuaalisesta rakenteesta (flyout-valikosta, alapalkin välilehdistä ja sivujen pinoamisesta) sekä URI-pohjaisesta navigoinnista, joka mahdollistaa deep linking -toiminnallisuuden ilman erillistä reititysmoottoria.
Käytä Shelliä, kun:
Sovelluksessa on 2–5 päänäkymää, jotka kuuluvat luonnollisesti välilehtien tai flyout-valikon alle.
Tarvitset deep linkingia (esim. push-ilmoituksesta tiettyyn sivuun).
Haluat keskittää navigointilogiikan yhteen tiedostoon sen sijaan, että käsittelet pinoja manuaalisesti.
Älä käytä Shelliä, jos sovelluksesi on yhden ruudun työkalu (esim. laskin), tai jos tarvitset täysin mukautetun navigointianimaation, jota Shellin elinkaari ei salli. Tällöin NavigationPage tai oma INavigation-toteutus on parempi valinta. Lisää tietoa virallisesta Microsoftin .NET MAUI Shell -dokumentaatiosta.
Shell-perusrakenne: AppShell.xaml
Jokainen Shell-pohjainen sovellus alkaa AppShell.xaml-tiedostosta. Tämä on juurielementti, joka määrittää sovelluksen rakenteen, eli välilehdet, flyout-kohteet ja niihin liittyvät sivut. Alla on minimaalinen mutta tuotantokäyttöön sopiva runko, joka käyttää sekä TabBar- että Flyout-rakennetta:
Huomaa kolme avainasiaa. FlyoutBehavior="Flyout" aktivoi sivuvalikon, TabBar rakentaa alapalkin välilehdet, ja jokainen Route-attribuutti määrittää URI-fragmentin, jota käytetään myöhemmin GoToAsync-kutsuissa. Jos jätät Route-arvon pois, Shell luo sen automaattisesti tyypin nimestä, mutta älä luota tähän tuotannossa. Refaktorointi rikkoo navigoinnin hiljaisesti.
TabBar ja välilehtinavigointi käytännössä
TabBar määrittää sovelluksen alapalkin välilehdet, jotka ovat aina näkyvissä. Toisin kuin FlyoutItem, TabBar ei näy flyout-valikossa. Se on erillinen rakenne, joka käytännössä piilottaa flyoutin sen alaisilta sivuilta. Tämä on ideaali rakenne sovelluksille, joissa on 3–5 päänäkymää ja erillinen "asetukset"-valikko.
Voit lisätä välilehdille badgeja (esim. lukemattomien viestien määrä) seuraavasti:
// AppShell.xaml.cs
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("details", typeof(ProductDetailPage));
Routing.RegisterRoute("cart", typeof(CartPage));
}
// ViewModelissa
public void UpdateCartBadge(int count)
{
var cartTab = Shell.Current.Items
.SelectMany(i => i.Items)
.FirstOrDefault(s => s.Route == "cart");
if (cartTab is not null)
{
Shell.SetTabBarIsVisible(cartTab, count > 0);
}
}
Välilehtien välillä navigointi tapahtuu absoluuttisella URI:lla:
// Vaihda toiseen välilehteen (ei pinoa)
await Shell.Current.GoToAsync("//search");
// Avaa yksityiskohtanäkymä haku-välilehden pinon päälle
await Shell.Current.GoToAsync("//search/details?productId=42");
// Palaa edelliseen
await Shell.Current.GoToAsync("..");
Kaksoiskauttaviivat (//) tarkoittavat absoluuttista reittiä, eli tämä pyyhkii nykyisen pinon ja siirtyy juurivälilehteen. Yhden kauttaviivan reitti (/details) työntää sivun nykyisen pinon päälle. Tämä ero on Shellin tärkein konsepti, eikä se ole intuitiivinen. Kirjoita se muistiin, koska kompastuin siihen itse useita kertoja ensimmäisessä projektissani.
Flyout-valikon rakentaminen ja muokkaaminen
Flyout-valikko on perinteinen "hamburger menu" -tyylinen sivuvalikko, joka tukee otsaketta, sisältöä ja alatunnistetta. Yksinkertaisimmillaan riittää FlyoutItem-elementtien lisääminen suoraan Shelliin, mutta tuotantosovellukset käyttävät yleensä mukautettua FlyoutItemTemplate-mallia visuaalista yhtenäisyyttä varten:
Voit myös rakentaa dynaamisen Flyoutin BindableLayout-sidoksella, joka näyttää käyttäjäkohtaisia kohteita (esim. suosikit). Sulje Flyout ohjelmallisesti asettamalla Shell.Current.FlyoutIsPresented = false heti navigoinnin jälkeen, muuten valikko jää auki Androidilla. Tämän opin kantapään kautta, kun testaajat raportoivat "viallisesta" valikosta, joka oikeasti oli oma huolimattomuuteni.
Reittien rekisteröinti ja Routing.RegisterRoute
Kaikki XAML:ssa näkymättömät sivut (yksityiskohtanäkymät, modaalit ja sekundaarisivut) täytyy rekisteröidä Routing.RegisterRoute-kutsulla ennen ensimmäistä navigointia. Tämä tehdään tyypillisesti AppShell-luokan konstruktorissa:
public AppShell()
{
InitializeComponent();
// Perussivu (työntyy pinon päälle)
Routing.RegisterRoute("product-details", typeof(ProductDetailPage));
// Sisäkkäinen reitti, käytä kun haluat ryhmitellä
Routing.RegisterRoute("order/confirmation", typeof(OrderConfirmationPage));
Routing.RegisterRoute("order/payment", typeof(PaymentPage));
// Modaaliesitys (esim. lomake): huom, ei "modal"-prefiksiä reitin nimessä,
// vaan käytetään PresentationMode-attribuuttia XAML:ssa
}
Reittien nimet ovat globaaleja sovelluksen sisällä, eli kaksi eri sivua eivät voi käyttää samaa reittinimeä. Iso projekti hyötyy reittien organisoinnista staattisiin vakioihin:
public static class Routes
{
public const string ProductDetails = "product-details";
public const string OrderConfirmation = "order/confirmation";
public const string Payment = "order/payment";
}
// Käyttö:
await Shell.Current.GoToAsync(Routes.ProductDetails);
Tämä estää kirjoitusvirheet ja tekee refaktoroinnin Visual Studion "Rename"-toiminnolla turvalliseksi. Jos käytät Dependency Injectionia, kuten esitimme SQLite ja MVVM -oppaassa, rekisteröi sivut myös DI-säiliöön MauiProgram.cs-tiedostossa.
Shellin URI-pohjainen navigointi tukee parametrien välitystä kahdella tavalla: kyselymerkkijonoilla (?id=42) yksinkertaisille arvoille ja sanakirjoilla monimutkaisemmille objekteille. Suositeltu ja modernein tapa on toteuttaa IQueryAttributable-rajapinta vastaanottavassa ViewModelissa:
public partial class ProductDetailViewModel : ObservableObject, IQueryAttributable
{
[ObservableProperty]
private int productId;
[ObservableProperty]
private Product? product;
private readonly IProductService _service;
public ProductDetailViewModel(IProductService service)
{
_service = service;
}
public async void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("productId", out var idValue)
&& int.TryParse(idValue?.ToString(), out var id))
{
ProductId = id;
Product = await _service.GetByIdAsync(id);
}
}
}
Lähettävältä puolelta parametrit välitetään kyselymerkkijonona tai sanakirjana:
// Yksinkertainen tapa, kyselymerkkijono
await Shell.Current.GoToAsync($"product-details?productId={product.Id}");
// Monimutkainen objekti, sanakirja (suositus)
var navigationParameter = new Dictionary<string, object>
{
{ "product", product },
{ "openedFrom", "search-results" }
};
await Shell.Current.GoToAsync("product-details", navigationParameter);
Sanakirjamenetelmä on parempi, koska se välittää oikeat oliotyypit eikä vain merkkijonoja, ja vältät JSON-sarjallistamisen sivujen välillä. Vältä myös [QueryProperty]-attribuuttia uusissa projekteissa. Se toimii edelleen, mutta IQueryAttributable on testattavampi ja tukee paremmin riippuvuusinjektiota.
Deep linking iOS- ja Android-sovelluksissa
Deep linking tarkoittaa sitä, että käyttäjä voi avata tietyn sivun sovelluksessasi suoraan ulkoisesta lähteestä, kuten verkkosivulta, push-ilmoituksesta tai sähköpostilinkistä. Shell tekee tästä yksinkertaista, koska sen URI-rakenne on jo valmis reititysmuoto.
Androidilla deep linking konfiguroidaan AndroidManifest.xml-tiedostossa lisäämällä intent-filterMainActivity-luokkaan:
iOS:llä lisätään Entitlements.plist-tiedostoon com.apple.developer.associated-domains -avain ja serveröidään apple-app-site-association-tiedosto sivustosi juuresta.
Linkin saapuessa Shell ohjaa sen automaattisesti, jos käsittelet App.xaml.cs-tiedostossa OnAppLinkRequestReceived-metodin:
Suora Shell.Current.GoToAsync-kutsu ViewModelin sisältä rikkoo MVVM-mallin testattavuuden, koska Shell.Current on staattinen riippuvuus, jota et voi mockata. Ratkaisu on tuotantosovelluksissa käytännössä pakollinen: abstrahoi navigointi INavigationService-rajapinnan taakse ja injektoi se ViewModeleihin Dependency Injection -säiliön kautta.
public interface INavigationService
{
Task GoToAsync(string route);
Task GoToAsync(string route, IDictionary<string, object> parameters);
Task GoBackAsync();
}
public class ShellNavigationService : INavigationService
{
public Task GoToAsync(string route)
=> Shell.Current.GoToAsync(route);
public Task GoToAsync(string route, IDictionary<string, object> parameters)
=> Shell.Current.GoToAsync(route, parameters);
public Task GoBackAsync()
=> Shell.Current.GoToAsync("..");
}
// MauiProgram.cs
builder.Services.AddSingleton<INavigationService, ShellNavigationService>();
builder.Services.AddTransient<ProductListViewModel>();
ViewModelissa käytetään nyt vain rajapintaa:
public partial class ProductListViewModel : ObservableObject
{
private readonly INavigationService _navigation;
public ProductListViewModel(INavigationService navigation)
{
_navigation = navigation;
}
[RelayCommand]
private async Task OpenDetailsAsync(Product product)
{
await _navigation.GoToAsync("product-details",
new Dictionary<string, object> { { "product", product } });
}
}
Yksikkötestissä voit nyt korvata INavigationService-toteutuksen Moq- tai NSubstitute-mockilla ja varmistaa, että ViewModel kutsuu navigointia oikealla reitillä ja parametreilla, ilman että XAML-sivuja tarvitsee renderöidä. Tämä on suora jatko ViewModelin elinkaarelle, jota käsittelimme aiemmin REST API ja MVVM -oppaassa.
Yleisimmät virheet ja niiden korjaaminen
Shell-navigoinnissa on muutamia toistuvia sudenkuoppia, jotka iskevät erityisesti Xamarin.Forms-pohjaisten projektien migroinnissa. Tunnista nämä etukäteen, niin säästät tunteja debuggaukselta. Itse törmäsin näistä neljään ensimmäisen viikon aikana, kun siirsin vanhan Xamarin-sovelluksen MAUI:in.
Virhe
Syy
Korjaus
System.ArgumentException: Route ... not found
Sivua ei ole rekisteröity
Lisää Routing.RegisterRoute AppShell-konstruktoriin
Flyout aukeaa väärin Androidilla
Vanhentunut MAUI Essentials -versio
Päivitä .NET MAUI 9.0.30+ -versioon
Parametrit eivät päivity uudelleen samalle sivulle
Sivu cachetetaan, ApplyQueryAttributes ei laukea uudelleen
Käytä OnNavigatedTo-tapahtumaa tai pakota uusi instanssi
Yliaja OnBackButtonPressed tai käytä BackButtonBehavior-elementtiä
GoToAsync ei tee mitään
Reitti on suhteellinen, mutta tarkoititit absoluuttista
Lisää // reitin eteen
Jos käytät MVVM-mallia ja navigointi ei toimi testeissä, abstrahoi Shell.Current.GoToAsyncINavigationService-rajapinnan taakse. Silloin voit mockata sen yksikkötesteissä. Tämä on myös Microsoftin suositus virallisessa MAUI Enterprise Application Patterns -oppaassa.
Usein kysytyt kysymykset
Mikä on ero Shellin ja NavigationPagen välillä?
Shell on korkeamman tason abstraktio, joka sisältää sisäänrakennetun flyout-valikon, välilehdet ja URI-pohjaisen reitityksen. NavigationPage on matalan tason pino, jolla on vain Push/Pop-operaatiot. Shell tuottaa 60–80 % vähemmän koodia keskikokoisessa sovelluksessa ja on Microsoftin suositus uusiin .NET MAUI -projekteihin.
Voiko Shell-sovelluksessa käyttää modaaleja sivuja?
Kyllä. Rekisteröi sivu normaalisti Routing.RegisterRoute-kutsulla ja aseta XAML:ssa Shell.PresentationMode="ModalAnimated". Navigointi tapahtuu samalla GoToAsync-kutsulla, mutta sivu liukuu ylhäältä alas modaalina natiivin alustan tyyliin.
Miten siirrän Xamarin.Forms-sovelluksen Shell-rakenteeseen?
Korvaa App.xaml:n juuri NavigationPage uudella AppShell-luokalla, siirrä TabbedPage-välilehdet TabBar-elementin sisään ja rekisteröi yksityiskohtanäkymät Routing.RegisterRoute-kutsuilla. Vaihda kaikki Navigation.PushAsync-kutsut Shell.Current.GoToAsync-kutsuiksi käyttäen rekisteröityjä reittinimiä.
Tukeeko Shell deep linkingia ilman lisäkirjastoja?
Kyllä, Shellin URI-rakenne toimii suoraan deep link -lähteenä. Käsittele saapuva linkki App.OnAppLinkRequestReceived-metodissa ja ohjaa se Shell.Current.GoToAsync-kutsuun. Alusta-spesifiset konfiguraatiot (Android intent-filter, iOS associated-domains) ovat silti pakollisia.
Miksi sivuni ei avaudu, kun kutsun GoToAsync-metodia?
Yleisin syy on rekisteröimätön reitti. Varmista, että Routing.RegisterRoute on kutsuttu AppShell-konstruktorissa. Toinen yleinen syy on absoluuttisen ja suhteellisen reitin sekoittaminen: //home vaihtaa juurivälilehteä, kun taas home yrittää työntää sivun nykyisen pinon päälle.
Push-ilmoitukset .NET MAUI -sovelluksessa vaativat sekä Firebase FCM:n Androidille että APNs:n iOS:lle. Tässä käytännön oppaassa käydään läpi HTTP v1 -rajapinta, token-rekisteröinti, notification channels, deep linking Shell-reitille ja tuotannon kompastuskivet.