.NET MAUI Shell navigáció: Útmutató az útvonalkezeléstől a deep linkingig

Teljes útmutató a .NET MAUI Shell navigációhoz: útvonalak regisztrálása, GoToAsync, paraméterátadás, navigációs események kezelése, MVVM integráció és .NET 10 újdonságok kódpéldákkal.

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 MessagingCenter belső (internal) lett. Helyette a CommunityToolkit.Mvvm csomag WeakReferenceMessenger-ét kell használni.
  • DisplayAlertAsync és DisplayActionSheetAsync: A korábbi DisplayAlert és DisplayActionSheet metódusok elavulttá váltak — az Async utó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.cs fájl segítségével megszabadulhatsz a XAML fájlok tetején lévő rengeteg xmlns: 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 SafeAreaEdges enum 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 ContentTemplate tulajdonságot a ShellContent-ben ahelyett, hogy közvetlenül adnád meg a Content-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 Dictionary helyett, 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 ApplyQueryAttributes metódusban kerüld a szinkron, hosszú futású műveleteket. Használj async void metó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.

A Szerzőről Editorial Team

Our team of expert writers and editors.