Bevezetés
A navigáció minden mobil alkalmazás gerincét képezi — ezt gondolom nem kell senkinek bizonygatni. Akár egy egyszerű kétoldalas alkalmazást készítesz, akár egy összetett, több tucatnyi képernyőből álló enterprise rendszert, a felhasználók zökkenőmentes navigációs élménye alapvetően meghatározza az alkalmazásod sikerét. A .NET MAUI Shell pontosan erre a kihívásra kínál átfogó megoldást: egy URI-alapú, deklaratív navigációs rendszert, amely egyszerűsíti az oldalak közötti átmenetet, támogatja a paraméterátadást, és natívan integrálja a flyout menüket, alsó füleket és felső lapozókat.
A .NET 10 megjelenésével a Shell számos finomhangolást kapott. A navigációs sáv láthatósági animációjának vezérlésétől kezdve a MessagingCenter végleges eltávolításáig és a WeakReferenceMessenger ajánlásáig — elég széles a változások palettája. Ebben az útmutatóban végigmegyünk a Shell navigáció minden fontosabb aspektusán: az útvonalak regisztrálásától a paraméterátadás különböző módjain át egészen a navigációs események kezeléséig és az MVVM mintával való integrációig.
Ha az előző cikkünket olvastad a CollectionView-ról, már ismered a .NET MAUI UI-rendszerének alapjait. Most a navigációs réteget vesszük górcső alá — azt a réteget, amely összeköti ezeket a felületi elemeket.
A Shell alapjai: struktúra és útvonalak
A Shell egy konténer-vezérlőelem, amely az alkalmazásod teljes navigációs struktúráját definiálja. Az AppShell.xaml fájlban deklaratívan leírhatod, hogy milyen oldalak, fülek és flyout menüpontok alkotják az alkalmazásod. Őszintén szólva, ha egyszer megszokod ezt a megközelítést, nehéz visszamenni a manuális navigációs kezeléshez.
A Shell vizuális hierarchia
A Shell hierarchia három fő elemből áll: FlyoutItem (vagy TabBar), Tab és ShellContent. Minden egyes elem rendelkezhet egy Route tulajdonsággal, amely egyedi azonosítót ad az adott navigációs célpontnak.
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyApp.Views"
x:Class="MyApp.AppShell"
FlyoutBehavior="Flyout">
<!-- Főoldal flyout menüpontként -->
<FlyoutItem Title="Főoldal"
Icon="home.png"
Route="fooldal">
<ShellContent ContentTemplate="{DataTemplate views:FooldalPage}"
Route="fooldal-tartalom" />
</FlyoutItem>
<!-- Termékek fülekkel -->
<FlyoutItem Title="Termékek"
Icon="products.png"
Route="termekek">
<Tab Title="Kategóriák" Route="kategoriak">
<ShellContent ContentTemplate="{DataTemplate views:KategoriakPage}"
Route="kategoriak-lista" />
</Tab>
<Tab Title="Kedvencek" Route="kedvencek">
<ShellContent ContentTemplate="{DataTemplate views:KedvencekPage}"
Route="kedvencek-lista" />
</Tab>
</FlyoutItem>
<!-- Beállítások -->
<FlyoutItem Title="Beállítások"
Icon="settings.png"
Route="beallitasok">
<ShellContent ContentTemplate="{DataTemplate views:BeallitasokPage}"
Route="beallitasok-tartalom" />
</FlyoutItem>
</Shell>
Ezzel a konfigurációval az alábbi útvonal-hierarchia jön létre:
fooldal
fooldal-tartalom
termekek
kategoriak
kategoriak-lista
kedvencek
kedvencek-lista
beallitasok
beallitasok-tartalom
Fontos tudni: ha nem adsz meg explicit Route értéket, a rendszer automatikusan generál egyet, de ez nem garantáltan konzisztens az alkalmazás különböző futtatásai között. Szóval mindig érdemes explicit útvonalakat definiálni — ezzel sok fejfájástól kíméled meg magad.
Globális útvonalak regisztrálása
Azokat az oldalakat, amelyek nem részei a Shell vizuális hierarchiájának (tipikusan részlet- vagy szerkesztő oldalak), globális útvonalként kell regisztrálni. Ezt a Routing.RegisterRoute metódussal tehetjük meg, jellemzően az AppShell konstruktorában:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// Globális útvonalak regisztrálása
Routing.RegisterRoute("termek-reszletek", typeof(TermekReszletekPage));
Routing.RegisterRoute("termek-szerkesztes", typeof(TermekSzerkesztesPage));
Routing.RegisterRoute("rendeles-osszegzes", typeof(RendelesOsszegzesPage));
Routing.RegisterRoute("profil", typeof(ProfilPage));
}
}
A globális útvonalak az alkalmazás bármely pontjáról elérhetők relatív URI-val. Egy dolgot viszont ne felejts: duplikált útvonalak ArgumentException kivételt váltanak ki az alkalmazás indításakor, ami azért elég kellemetlen hiba lehet élesben.
Kontextuális útvonalak
A .NET MAUI Shell támogatja a kontextuális navigációt is. Ez azt jelenti, hogy ugyanaz az útvonalnév más-más oldalra mutathat attól függően, honnan navigálunk. Nézzünk egy példát:
// Kontextuális útvonalak regisztrálása
Routing.RegisterRoute("kategoriak/reszletek", typeof(KategoriaReszletekPage));
Routing.RegisterRoute("kedvencek/reszletek", typeof(KedvencReszletekPage));
Ebben az esetben ha a Kategóriák oldalról navigálunk a reszletek útvonalra, a KategoriaReszletekPage jelenik meg. Ha viszont a Kedvencek oldalról tesszük ugyanezt, a KedvencReszletekPage töltődik be. Saját tapasztalatom szerint ez rendkívül hasznos, ha hasonló struktúrájú, de eltérő tartalmú oldalakkal dolgozol — elég sokat tud egyszerűsíteni a kódon.
Navigáció a GoToAsync metódussal
Na, akkor nézzük a lényeget. A Shell navigáció központi eleme a GoToAsync metódus, amelyet a Shell.Current objektumon keresztül hívhatunk meg. Ez a metódus egy Task-ot ad vissza, amely a navigációs animáció befejezésekor teljesül.
Abszolút útvonalak
Az abszolút útvonalak a // előtaggal kezdődnek és a Shell vizuális hierarchia gyökerétől indulnak:
// Navigáció a Kedvencek fülre
await Shell.Current.GoToAsync("//termekek/kedvencek");
// Navigáció a Beállítások oldalra
await Shell.Current.GoToAsync("//beallitasok");
Fontos: abszolút útvonalak csak a Shell vizuális hierarchiában definiált oldalakhoz használhatók. A Routing.RegisterRoute metódussal regisztrált globális útvonalakhoz nem működnek — ez egy gyakori buktató, amin sokan meglepődnek először.
Relatív útvonalak
A relatív útvonalak az aktuális pozícióhoz képest navigálnak, és elsősorban a globális útvonalakhoz használatosak:
// Navigáció egy részletoldalra (globális útvonal)
await Shell.Current.GoToAsync("termek-reszletek");
// Navigáció kontextuális útvonalra
await Shell.Current.GoToAsync("reszletek");
Visszafelé navigáció
A visszafelé navigáció a .. szintaxissal valósítható meg. Ez a navigációs veremből eltávolítja az aktuális oldalt:
// Egyszerű visszalépés
await Shell.Current.GoToAsync("..");
// Visszalépés és navigáció egy másik útvonalra
await Shell.Current.GoToAsync("../masik-oldal");
// Több szint visszalépése
await Shell.Current.GoToAsync("../../harmadik-oldal");
// Visszalépés paraméterrel
await Shell.Current.GoToAsync($"..?eredmeny={mentettAdat}");
A visszafelé navigáció különösen hasznos, amikor egy részletoldalról szeretnénk visszatérni a lista nézethez, vagy amikor egy szerkesztő oldalról visszalépünk az eredménnyel. Egyszerű és intuitív — pont, ahogy lennie kell.
Paraméterátadás navigáció során
Az alkalmazások szinte mindig igénylik, hogy adatokat adjunk át az oldalak között navigáció során. A .NET MAUI Shell háromféle megközelítést kínál erre, és érdemes mindegyiket ismerni, mert más-más helyzetben lesz ideális.
Sztring-alapú query paraméterek
A legegyszerűbb módszer primitív adatok átadására az URI query paramétereinek használata:
// Egyetlen paraméter átadása
await Shell.Current.GoToAsync($"termek-reszletek?termekId={kivalasztottTermek.Id}");
// Több paraméter átadása
await Shell.Current.GoToAsync(
$"termek-reszletek?termekId={termek.Id}&kategoriaId={kategoria.Id}&megpipirazva=true");
Objektum-alapú paraméterek (többszöri használat)
Ha komplex objektumokat szeretnénk átadni, használhatjuk az IDictionary<string, object> overloadot. Fontos tudni, hogy ezek a paraméterek a memóriában maradnak, amíg az oldal a navigációs veremben van:
// Komplex objektum átadása
var navigaciosParameterek = new Dictionary<string, object>
{
{ "KivalasztottTermek", kivalasztottTermek },
{ "KosarElemek", kosarLista }
};
await Shell.Current.GoToAsync("rendeles-osszegzes", navigaciosParameterek);
Figyelmeztetés: Mivel az objektum-alapú paraméterek a memóriában maradnak az oldal teljes élettartama alatt, visszanavigáláskor is újra megkapja az oldal ugyanazokat a paramétereket. Ha ez nem kívánatos (és gyakran nem az), használj egyszeri paramétereket.
Egyszeri használatú paraméterek (ShellNavigationQueryParameters)
A .NET MAUI bevezette a ShellNavigationQueryParameters osztályt, amely automatikusan törli a paramétereket a navigáció befejezése után. Ez gyakorlatilag a legtöbb esetben az, amit használni szeretnél.
// Egyszeri használatú paraméterek
var egyszerParameterek = new ShellNavigationQueryParameters
{
{ "RendelesAdatok", ujRendeles },
{ "FizetesiMod", fizetesiMod }
};
await Shell.Current.GoToAsync("rendeles-osszegzes", egyszerParameterek);
// A paraméterek automatikusan törlődnek a navigáció után
Ez a megközelítés ideális, amikor nem szeretnénk, hogy visszanavigáláskor az oldal újra megkapja az adatokat — például egy fizetési folyamatnál, ahol az dupla rendeléshez vezethetne.
Paraméterek fogadása: IQueryAttributable interfész
A paraméterek fogadásának ajánlott módja az IQueryAttributable interfész implementálása. Ez trim-biztos és NativeAOT-kompatibilis, ellentétben a régebbi QueryPropertyAttribute-tal:
public class TermekReszletekViewModel : IQueryAttributable, INotifyPropertyChanged
{
private Termek _termek;
private bool _pipirazva;
public Termek Termek
{
get => _termek;
set
{
_termek = value;
OnPropertyChanged();
}
}
public bool Pipirazva
{
get => _pipirazva;
set
{
_pipirazva = value;
OnPropertyChanged();
}
}
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
// Objektum-alapú paraméter fogadása
if (query.TryGetValue("KivalasztottTermek", out var termekObj)
&& termekObj is Termek termek)
{
Termek = termek;
}
// Sztring-alapú paraméter fogadása és URL-dekódolás
if (query.TryGetValue("termekId", out var termekIdObj))
{
var termekId = HttpUtility.UrlDecode(termekIdObj?.ToString());
// Termék betöltése az ID alapján
BetoltTermeketAsync(termekId);
}
// Bool paraméter kezelése
if (query.TryGetValue("megpipirazva", out var pipObj))
{
Pipirazva = bool.TryParse(pipObj?.ToString(), out var eredmeny) && eredmeny;
}
}
private async void BetoltTermeketAsync(string termekId)
{
// Aszinkron adatlekérés
Termek = await _termekService.GetTermekByIdAsync(termekId);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Egy apró, de fontos részlet: a sztring-alapú query paraméterek értékeit az IQueryAttributable interfészen keresztül fogadva nem dekódolja automatikusan a rendszer. Ezért mindig használd a HttpUtility.UrlDecode metódust — ezt könnyű elfelejteni, és furcsa bugokhoz vezethet.
Navigációs események és halasztás
A Shell két fontos eseményt biztosít a navigáció életciklusának kezelésére: a Navigating és a Navigated eseményeket. Ezek lehetővé teszik, hogy beavatkozzunk a navigációs folyamatba — például megakadályozzuk a mentetlen adatok elvesztését.
A Navigating esemény
A Navigating esemény még a navigáció végrehajtása előtt következik be. A ShellNavigatingEventArgs tartalmazza az aktuális és a céloldal URI-ját, a navigáció típusát, és lehetőséget ad a navigáció megszakítására:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Navigating += OnShellNavigating;
}
private void OnShellNavigating(object sender, ShellNavigatingEventArgs e)
{
// A navigáció forrásának és céljának naplózása
System.Diagnostics.Debug.WriteLine(
$"Navigáció: {e.Current?.Location} -> {e.Target?.Location} ({e.Source})");
// Visszanavigáció megakadályozása bizonyos esetekben
if (e.Source == ShellNavigationSource.Pop && !MentesElvegezve)
{
e.Cancel();
// Felhasználó értesítése
MentesElottiFigyelmeztetesAsync();
}
}
}
Navigáció halasztása (deferral)
Ha a navigáció megszakításáról aszinkron műveletek alapján szeretnénk dönteni (mondjuk felhasználói megerősítést kérünk), a deferral mechanizmust használhatjuk. Ez az egyik legeleánsabb megoldás, amit a Shell kínál:
public partial class AppShell : Shell
{
protected override async void OnNavigating(ShellNavigatingEventArgs args)
{
base.OnNavigating(args);
// Csak visszanavigáláskor kérdezünk
if (args.Source != ShellNavigationSource.Pop)
return;
// Halasztási token kérése
ShellNavigatingDeferral token = args.GetDeferral();
try
{
// Felhasználói megerősítés kérése
var valasz = await DisplayAlertAsync(
"Megerősítés",
"Biztosan el szeretnéd hagyni ezt az oldalt? A nem mentett módosítások elvesznek.",
"Igen",
"Mégse");
if (!valasz)
{
args.Cancel();
}
}
finally
{
// A token befejezése kötelező!
token.Complete();
}
}
}
Figyelem: Ha egy halasztás van folyamatban és közben egy másik navigáció indul, InvalidOperationException keletkezik. Ezért mindig gondoskodj a token megfelelő befejezéséről — a try-finally blokk itt nem opcionális, hanem kötelező.
A Navigated esemény
A Navigated esemény a navigáció befejezése után következik be. Hasznos például analitikai adatok gyűjtéséhez:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Navigated += OnShellNavigated;
}
private void OnShellNavigated(object sender, ShellNavigatedEventArgs e)
{
// Oldalmegtekintés naplózása analitikai célból
AnalitikaiService.OldalMegtekintes(
e.Current?.Location?.ToString(),
e.Previous?.Location?.ToString());
}
}
MVVM integráció és dependency injection
A modern .NET MAUI alkalmazásokban a navigáció szorosan összefonódik az MVVM mintával és a dependency injection rendszerrel. Ezt a részt érdemes különös figyelemmel olvasni, mert a .NET 10-ben a MessagingCenter végleges eltávolítása után a CommunityToolkit.Mvvm csomag WeakReferenceMessenger-e lett az ajánlott üzenetküldési megoldás.
Oldalak és ViewModel-ek regisztrálása a DI konténerben
A .NET MAUI beépített támogatást nyújt a dependency injection számára a MauiProgram.cs fájlban:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Szolgáltatások regisztrálása
builder.Services.AddSingleton<ITermekService, TermekService>();
builder.Services.AddSingleton<INavigaciosService, NavigaciosService>();
builder.Services.AddSingleton<IKosarService, KosarService>();
// Oldalak regisztrálása (transient, mert minden navigációkor új példány kell)
builder.Services.AddTransient<FooldalPage>();
builder.Services.AddTransient<TermekReszletekPage>();
builder.Services.AddTransient<RendelesOsszegzesPage>();
builder.Services.AddTransient<KosarPage>();
// ViewModel-ek regisztrálása
builder.Services.AddTransient<FooldalViewModel>();
builder.Services.AddTransient<TermekReszletekViewModel>();
builder.Services.AddTransient<RendelesOsszegzesViewModel>();
builder.Services.AddTransient<KosarViewModel>();
return builder.Build();
}
}
Navigációs szolgáltatás készítése
Az MVVM minta egyik legfontosabb előnye, hogy a ViewModel-ek nem függenek közvetlenül a felhasználói felülettől. Ehhez érdemes egy navigációs szolgáltatást készíteni, ami elrejti a Shell-specifikus részleteket:
public interface INavigaciosService
{
Task NavigaljRa(string utvonal);
Task NavigaljRa(string utvonal, IDictionary<string, object> parameterek);
Task NavigaljVissza();
Task NavigaljVisszaEredmennyel(IDictionary<string, object> parameterek);
}
public class NavigaciosService : INavigaciosService
{
public async Task NavigaljRa(string utvonal)
{
await Shell.Current.GoToAsync(utvonal);
}
public async Task NavigaljRa(string utvonal, IDictionary<string, object> parameterek)
{
await Shell.Current.GoToAsync(utvonal, parameterek);
}
public async Task NavigaljVissza()
{
await Shell.Current.GoToAsync("..");
}
public async Task NavigaljVisszaEredmennyel(IDictionary<string, object> parameterek)
{
await Shell.Current.GoToAsync("..", parameterek);
}
}
ViewModel navigáció a CommunityToolkit.Mvvm csomaggal
A CommunityToolkit.Mvvm csomag jelentősen egyszerűsíti a ViewModel-ek kódját a source generátorok segítségével. Komolyan, ha még nem használod, nézz rá — rengeteg boilerplate kódtól szabadulsz meg:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class FooldalViewModel : ObservableObject, IQueryAttributable
{
private readonly INavigaciosService _navigaciosService;
private readonly ITermekService _termekService;
[ObservableProperty]
private ObservableCollection<Termek> _termekek = new();
[ObservableProperty]
private bool _betoltes;
public FooldalViewModel(
INavigaciosService navigaciosService,
ITermekService termekService)
{
_navigaciosService = navigaciosService;
_termekService = termekService;
}
[RelayCommand]
private async Task BetoltTermekeketAsync()
{
Betoltes = true;
try
{
var termekLista = await _termekService.GetOsszesTermekAsync();
Termekek = new ObservableCollection<Termek>(termekLista);
}
finally
{
Betoltes = false;
}
}
[RelayCommand]
private async Task TermekKivalasztva(Termek termek)
{
if (termek == null) return;
var parameterek = new ShellNavigationQueryParameters
{
{ "KivalasztottTermek", termek }
};
await _navigaciosService.NavigaljRa("termek-reszletek", parameterek);
}
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
// Visszanavigáláskor kapott eredmény kezelése
if (query.TryGetValue("frissitendo", out var frissitObj)
&& frissitObj is bool frissit && frissit)
{
BetoltTermekeketAsync();
}
}
}
Vissza gomb testreszabása
A Shell lehetőséget ad a vissza gomb viselkedésének és megjelenésének teljes testreszabására a BackButtonBehavior csatolt tulajdonságon keresztül. Ha valaha is próbáltad már testre szabni a back buttont Xamarin.Forms-ban, tudod, hogy ez nem mindig volt ilyen egyszerű.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Views.TermekSzerkesztesPage">
<Shell.BackButtonBehavior>
<BackButtonBehavior
Command="{Binding VisszaGombCommand}"
CommandParameter="{Binding MentettE}"
IconOverride="custom_back.png"
TextOverride="Vissza"
IsVisible="{Binding VisszaGombLatszik}" />
</Shell.BackButtonBehavior>
<!-- Oldal tartalma -->
<VerticalStackLayout Padding="16" Spacing="12">
<Label Text="Termék szerkesztése"
FontSize="24"
FontAttributes="Bold" />
<Entry Text="{Binding TermekNev}"
Placeholder="Termék neve" />
<Editor Text="{Binding TermekLeiras}"
Placeholder="Termék leírása"
HeightRequest="120" />
<Button Text="Mentés"
Command="{Binding MentesCommand}" />
</VerticalStackLayout>
</ContentPage>
A ViewModel-ben a vissza gomb parancsát így kezelhetjük:
[RelayCommand]
private async Task VisszaGomb(bool mentettE)
{
if (VanMentetlenValtoztatas && !mentettE)
{
var valasz = await Shell.Current.DisplayAlertAsync(
"Figyelmeztetés",
"Vannak mentetlen módosítások. Biztosan kilépsz?",
"Igen",
"Mégse");
if (!valasz) return;
}
await Shell.Current.GoToAsync("..");
}
Modális navigáció és .NET 10 újdonságok
A Shell alapértelmezetten verem-alapú (push/pop) navigációt használ, de szükség esetén modális navigáció is megvalósítható a Navigation tulajdonságon keresztül.
Klasszikus modális megjelenítés
// Modális oldal megnyitása
await Navigation.PushModalAsync(new BejelentkezesPage());
// Modális oldal bezárása
await Navigation.PopModalAsync();
.NET 10 újdonság: popover modális iOS-en és Mac Catalyst-en
A .NET 10 bevezette a modális oldalak popover-ként történő megjelenítésének lehetőségét iOS és Mac Catalyst platformokon. Ez különösen iPaden és Mac-en hasznos, ahol a lebegő ablakok sokkal természetesebb felhasználói élményt nyújtanak, mint egy teljes képernyős modális.
using Microsoft.Maui.Controls.PlatformConfiguration;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
public partial class SzuroPopoverPage : ContentPage
{
public SzuroPopoverPage(View forrasElem, Rectangle terulet)
{
InitializeComponent();
On<iOS>().SetModalPopoverView(forrasElem);
On<iOS>().SetModalPopoverRect(terulet);
On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.Popover);
}
}
// Használat egy gombból:
private async void SzuroGomb_Clicked(object sender, EventArgs e)
{
var popover = new SzuroPopoverPage(sender as View, Rectangle.Empty);
await Navigation.PushModalAsync(popover);
}
Navigációs sáv animáció vezérlése (.NET 10)
A .NET 10-ben új lehetőségként jelent meg a navigációs sáv láthatósági animációjának szabályozása:
<!-- Navigációs sáv animáció kikapcsolása -->
<Shell NavBarVisibilityAnimationEnabled="False"
...>
<!-- Shell tartalom -->
</Shell>
Ez hasznos, ha az animáció zavaró hatású bizonyos oldaltípusoknál, vagy ha teljesítményoptimalizálásra van szükség. Egyébként a legtöbb esetben az alapértelmezett animáció tökéletesen megfelel.
Gyakorlati példa: komplett navigációs folyamat
Rendben, nézzünk meg egy teljes, valószerű navigációs folyamatot egy webáruház alkalmazásban. Ez a példa az eddig tanultakat fogja össze egyetlen koherens rendszerben — szóval ha eddig csak darabokban láttad a képet, itt áll össze az egész.
Az AppShell definiálása
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:WebAruhaz.Views"
x:Class="WebAruhaz.AppShell"
FlyoutBehavior="Disabled">
<TabBar Route="main">
<Tab Title="Bolt" Icon="shop.png" Route="bolt">
<ShellContent ContentTemplate="{DataTemplate views:BoltPage}" />
</Tab>
<Tab Title="Kosár" Icon="cart.png" Route="kosar">
<ShellContent ContentTemplate="{DataTemplate views:KosarPage}" />
</Tab>
<Tab Title="Fiók" Icon="account.png" Route="fiok">
<ShellContent ContentTemplate="{DataTemplate views:FiokPage}" />
</Tab>
</TabBar>
</Shell>
Globális útvonalak és ViewModel-ek
// AppShell.xaml.cs
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("termek", typeof(TermekPage));
Routing.RegisterRoute("fizetes", typeof(FizetesPage));
Routing.RegisterRoute("rendeles-keszult", typeof(RendelesKeszultPage));
}
}
// A Bolt oldal ViewModel-je
public partial class BoltViewModel : ObservableObject
{
private readonly ITermekService _termekService;
[ObservableProperty]
private ObservableCollection<Termek> _termekek = new();
public BoltViewModel(ITermekService termekService)
{
_termekService = termekService;
}
[RelayCommand]
private async Task MegnyitTermek(Termek termek)
{
await Shell.Current.GoToAsync("termek", new ShellNavigationQueryParameters
{
{ "termek", termek }
});
}
}
// A Termék részletek ViewModel-je
public partial class TermekViewModel : ObservableObject, IQueryAttributable
{
private readonly IKosarService _kosarService;
[ObservableProperty]
private Termek _termek;
[ObservableProperty]
private int _mennyiseg = 1;
public TermekViewModel(IKosarService kosarService)
{
_kosarService = kosarService;
}
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
if (query.TryGetValue("termek", out var obj) && obj is Termek t)
{
Termek = t;
}
}
[RelayCommand]
private async Task KosarbaTesz()
{
await _kosarService.HozzaadasAsync(Termek, Mennyiseg);
await Shell.Current.DisplayAlertAsync(
"Siker",
$"{Termek.Nev} hozzáadva a kosárhoz!",
"OK");
}
[RelayCommand]
private async Task AzonalMegvesz()
{
await _kosarService.HozzaadasAsync(Termek, Mennyiseg);
await Shell.Current.GoToAsync("//kosar");
}
}
Hibakeresés és gyakori problémák
A Shell navigáció során számos gyakori probléma felmerülhet. Lássuk a legfontosabbakat — ezek azok, amikbe szinte mindenki belefut előbb-utóbb.
1. Duplikált útvonalak
Ha két azonos nevű útvonalat regisztrálsz, az alkalmazás ArgumentException kivétellel áll le induláskor. Mindig győződj meg az útvonalnevek egyediségéről:
// HIBÁS: duplikált útvonalnév
Routing.RegisterRoute("reszletek", typeof(TermekReszletekPage));
Routing.RegisterRoute("reszletek", typeof(RendelesReszletekPage)); // ArgumentException!
// HELYES: egyedi útvonalnevek
Routing.RegisterRoute("termek-reszletek", typeof(TermekReszletekPage));
Routing.RegisterRoute("rendeles-reszletek", typeof(RendelesReszletekPage));
2. Nem létező útvonalak
Nem létező útvonalra navigálás szintén ArgumentException kivételt eredményez. Érdemes konstansokat vagy egy statikus osztályt használni az útvonalak kezelésére — ezzel futásidejű hibákat alakíthatsz át fordítási idejű hibákká:
public static class Utvonalak
{
public const string TermekReszletek = "termek-reszletek";
public const string RendelesOsszegzes = "rendeles-osszegzes";
public const string Profil = "profil";
public const string Fizetes = "fizetes";
}
// Használat
await Shell.Current.GoToAsync(Utvonalak.TermekReszletek);
3. QueryPropertyAttribute és NativeAOT
A .NET 10-ben fontos figyelmeztetés: a QueryPropertyAttribute nem trim-biztos és nem használható NativeAOT-tal. Ha a jövőre is gondolsz, mindig az IQueryAttributable interfészt használd:
// NE HASZNÁLD NativeAOT esetén:
[QueryProperty(nameof(TermekId), "termekId")]
public partial class ReszletekPage : ContentPage { }
// HASZNÁLD HELYETTE:
public class ReszletekViewModel : IQueryAttributable
{
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
// Trim-biztos paraméter-kezelés
}
}
4. Navigációs verem állapotának ellenőrzése
A Shell aktuális állapotát a Shell.Current.CurrentState tulajdonságon keresztül ellenőrizheted. Hibakeresésnél ez az egyik leghasznosabb eszköz:
// Aktuális útvonal lekérdezése
var aktualisUtvonal = Shell.Current.CurrentState.Location.ToString();
System.Diagnostics.Debug.WriteLine($"Aktuális útvonal: {aktualisUtvonal}");
// A Tab navigációs vermének vizsgálata
var aktualisTab = Shell.Current.CurrentItem?.CurrentItem as Tab;
if (aktualisTab != null)
{
var verem = aktualisTab.GetNavigationStack();
System.Diagnostics.Debug.WriteLine($"Verem mérete: {verem.Count}");
foreach (var oldal in verem)
{
System.Diagnostics.Debug.WriteLine($" - {oldal.GetType().Name}");
}
}
.NET 10 Shell-specifikus változások összefoglalása
A .NET 10 több fontos változást hozott a Shell navigáció és az általános alkalmazásarchitektúra területén. Íme a legfontosabbak egy helyen:
- NavBarVisibilityAnimationEnabled: Új tulajdonság a navigációs sáv megjelenési/eltűnési animációjának vezérlésére.
- MessagingCenter eltávolítása: A .NET 10-ben a
MessagingCenterbelső (internal) lett. Helyette a CommunityToolkit.Mvvm csomagWeakReferenceMessenger-ét kell használni. - DisplayAlertAsync és DisplayActionSheetAsync: A korábbi
DisplayAlertésDisplayActionSheetmetódusok elavulttá váltak — azAsyncutótagú változatokat használd. - XAML Source Generator: Az új XAML forrásgenerátor fordítási időben erősen típusos kódot hoz létre, ami gyorsabb futást és jobb IntelliSense-támogatást eredményez.
- Globális XML névterek: A
GlobalXmlns.csfájl segítségével megszabadulhatsz a XAML fájlok tetején lévő rengetegxmlns:deklarációtól. - iOS popover modális: Modális oldalak mostantól popoverként jeleníthetők meg iPaden és Mac-en.
- SafeAreaEdges: Finomhangolt safe area kezelés a
SafeAreaEdgesenum segítségével. - Aspire integráció: Új projektsablon a .NET Aspire szolgáltatás-alapértelmezések integrálásához MAUI alkalmazásokba.
Teljesítmény-tippek a navigációhoz
A navigáció optimalizálása kulcsfontosságú a jó felhasználói élményhez. Íme néhány gyakorlati tanács, amiket érdemes szem előtt tartani:
- Lazy loading: Használd a
ContentTemplatetulajdonságot aShellContent-ben ahelyett, hogy közvetlenül adnád meg aContent-et. Így az oldalak csak akkor töltődnek be, amikor először navigálsz hozzájuk. - ShellNavigationQueryParameters: Egyszeri adatátadásnál mindig ezt használd a sima
Dictionaryhelyett, hogy elkerüld a memóriaszivárgást. - Transient oldalak: A részletoldalakat mindig
AddTransient-ként regisztráld a DI konténerben, hogy minden navigációkor friss példányt kapj. - Ne blokkolj a navigációs szálon: Az
ApplyQueryAttributesmetódusban kerüld a szinkron, hosszú futású műveleteket. Használjasync voidmetódusokat az adatbetöltéshez (igen, tudom, általában kerülendő, de itt indokolt). - XAML Source Generator: Engedélyezd a .NET 10 XAML forrásgenerátort a gyorsabb indítási időért.
<!-- Projekt fájl (.csproj) kiegészítése a XAML Source Generator-hoz -->
<PropertyGroup>
<MauiXamlInflator>SourceGen</MauiXamlInflator>
</PropertyGroup>
Összefoglalás
A .NET MAUI Shell navigáció egy sokoldalú, URI-alapú rendszer, amely jelentősen egyszerűsíti a mobil alkalmazások oldalak közötti átmeneteit. Ebben az útmutatóban áttekintettük a Shell hierarchia felépítését, az abszolút és relatív útvonalakat, a globális és kontextuális útvonalak regisztrálását, valamint a paraméterátadás három módszerét.
Megismertük a navigációs események kezelését, a halasztás mechanizmusát, a vissza gomb testreszabását és a modális navigáció lehetőségeit — beleértve a .NET 10 által bevezetett popover modálisokat is. Az MVVM integrációs fejezetben pedig láthattad, hogyan építhetsz tiszta, tesztelhető navigációs réteget dependency injection és a CommunityToolkit.Mvvm csomag segítségével.
A .NET 10 változásai (MessagingCenter eltávolítása, XAML Source Generator, globális XML névterek, SafeAreaEdges) együttesen egy érettebb, teljesítményorientáltabb keretrendszert eredményeznek. Ha eddig halogattad a migrációt, most tényleg érdemes lépni.
Következő lépésként próbáld ki a bemutatott mintákat a saját projektedben, és fokozatosan térj át az IQueryAttributable interfész használatára — különösen ha NativeAOT-t vagy full trimming-et tervezel. Hidd el, a befektetett idő megtérül.