Nawigacja po stronach w .NET MAUI

1. Co to znaczy „nawigacja między oknami”?

Wyobraź sobie aplikację na telefon lub komputer. Rzadko kiedy mamy tylko jedno okno – zwykle są różne ekrany:

  • ekran logowania,
  • ekran główny,
  • ekran ustawień,
  • ekran szczegółów np. ucznia, produktu czy zdjęcia.

Nawigacja to właśnie mechanizm, który pozwala przejść z jednego ekranu (np. MainPage) do innego (np. MainPage2).


2. Rodzaje nawigacji w MAUI

a) NavigationPage – stos stron

To najprostszy sposób. Działa jak stos kartek:

  • PushAsync() → dokładamy nową kartkę (nową stronę).
  • PopAsync() → wracamy do poprzedniej strony (ściągamy kartkę ze stosu).

Zastosowanie:
– kiedy chcesz mieć klasyczne „przejdź dalej / wróć” w aplikacji, np. lista uczniów → szczegóły ucznia.

Przykład:

// Przejście do nowej strony
await Navigation.PushAsync(new MainPage2());

// Powrót
await Navigation.PopAsync();

b) Modalna nawigacja (okno na wierzchu)

To tak, jakbyś otworzył pełnoekranowe okno dialogowe.
Masz stronę, i na nią „nakładasz” inną stronę. Dopóki jej nie zamkniesz, użytkownik nie wróci do poprzedniej.

Zastosowanie:
– formularze, logowanie, potwierdzenia, okna wyboru.

Przykład:

// Otwórz okno modalne
await Navigation.PushModalAsync(new MainPage2());

// Zamknij
await Navigation.PopModalAsync();

c) Shell (rekomendowany dziś standard w MAUI)

Shell działa jak mapa stron w aplikacji.
Możesz rejestrować trasy (adresy) do różnych ekranów i przechodzić do nich po nazwie.
Daje też menu boczne, dolne zakładki itp.

Zastosowanie:
– większe aplikacje: np. sklep internetowy → zakładki: „Produkty”, „Koszyk”, „Profil”.
– kiedy chcesz łatwo przekazywać dane między stronami.

Przykład rejestracji i przejścia:

// AppShell.xaml.cs
Routing.RegisterRoute(nameof(MainPage2), typeof(MainPage2));

// przejście do strony
await Shell.Current.GoToAsync(nameof(MainPage2));

Można też przekazać dane:

await Shell.Current.GoToAsync(nameof(MainPage2), 
    new Dictionary<string, object>
    {
        ["Imie"] = "Ania",
        ["Wiek"] = 18
    });

3. Przekazywanie danych (np. imię ucznia)

To ważne, bo często nie chodzi tylko o przejście, ale też o to, by druga strona wiedziała co ma wyświetlić.

  • W NavigationPage przekazujemy dane przez konstruktor:
await Navigation.PushAsync(new MainPage2("Ania"));
  • W Shell można przesłać parametry w słowniku i odebrać je na drugiej stronie przez atrybut [QueryProperty].

4. Wiązanie danych (Binding)

Żeby nie pisać kodu „ręcznie” dla każdego pola, używa się Bindingu.
To znaczy: wartość z obiektu w kodzie automatycznie łączy się z kontrolką na ekranie.
Np. obiekt Uczen { Imie="Ania" } jest połączony z Label, który pokazuje to imię.
Jak zmienisz wartość w obiekcie – zmienia się też na ekranie.

Zastosowanie:
– gdy chcesz, by dane w aplikacji aktualizowały się automatycznie.
– przy większych projektach i egzaminie INF.04 to standard (MVVM).

Przykład prosty:

// Code-behind
BindingContext = new Uczen { Imie = "Ania" };
<!-- XAML -->
<Label Text="{Binding Imie}" />

5. Zrozumieć obrazowo

  • NavigationPage: książka, gdzie przewracasz strony do przodu i do tyłu.
  • Modal: wyskakujące okno „na wierzchu”, dopóki go nie zamkniesz, nie wrócisz.
  • Shell: mapa całej aplikacji z adresami i drogowskazami – idealna do dużych projektów.

6. Najprostszy przykład do klasy

Masz dwie strony: MainPage i MainPage2.

MainPage.xaml:

<VerticalStackLayout Padding="20">
    <Button Text="Przejdź do drugiej strony"
            Clicked="OnGoClicked"/>
</VerticalStackLayout>

MainPage.xaml.cs:

private async void OnGoClicked(object sender, EventArgs e)
{
    await Navigation.PushAsync(new MainPage2("Witaj uczniu!"));
}

MainPage2.xaml.cs:

public partial class MainPage2 : ContentPage
{
    public MainPage2(string tekst)
    {
        InitializeComponent();
        LabelWiadomosc.Text = tekst;
    }
}

MainPage2.xaml:

<VerticalStackLayout Padding="20">
    <Label x:Name="LabelWiadomosc"/>
    <Button Text="Wróć" Clicked="OnBackClicked"/>
</VerticalStackLayout>

MainPage2.xaml.cs – powrót:

private async void OnBackClicked(object sender, EventArgs e)
{
    await Navigation.PopAsync();
}


Podsumowanie.

W .NET MAUI można przechodzić między stronami (oknami). Najprostszy sposób to użycie NavigationPage:

  • PushAsync() – przejście do nowej strony,
  • PopAsync() – powrót do poprzedniej.

Dane można przekazać np. przez konstruktor drugiej strony.
To wystarczy na egzaminie INF.04, żeby pokazać nawigację i wymianę danych między oknami.


Zadanie 1

Zadanie:
Utwórz aplikację mobilną w .NET MAUI z dwiema stronami:

  1. MainPage – zawiera pole tekstowe (Entry) do wpisania imienia i przycisk „Przejdź dalej”.
  2. SecondPage – wyświetla wpisane imię oraz przycisk „Wróć”.

Przy przejściu z pierwszej do drugiej strony imię wpisane w polu tekstowym ma zostać przekazane i wyświetlone.


Rozwiązanie krok po kroku

1. Utwórz nową stronę. Dodaj do projektu plik SecondPage.xaml (ContentPage).


2. Ustaw NavigationPage w App.xaml.cs

public App()
{
    InitializeComponent();
    MainPage = new NavigationPage(new MainPage());
}

3. Kod MainPage.xaml

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             x:Class="MauiApp.MainPage">
    <VerticalStackLayout Padding="20">
        <Entry x:Name="poleImie" Placeholder="Podaj imię"/>
        <Button Text="Przejdź dalej" Clicked="OnGoClicked"/>
    </VerticalStackLayout>
</ContentPage>

4. Kod MainPage.xaml.cs

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private async void OnGoClicked(object sender, EventArgs e)
    {
        string imie = poleImie.Text ?? "";
        await Navigation.PushAsync(new SecondPage(imie));
    }
}

5. Kod SecondPage.xaml

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             x:Class="MauiApp.SecondPage">
    <VerticalStackLayout Padding="20">
        <Label x:Name="labelImie" FontSize="24"/>
        <Button Text="Wróć" Clicked="OnBackClicked"/>
    </VerticalStackLayout>
</ContentPage>

6. Kod SecondPage.xaml.cs

public partial class SecondPage : ContentPage
{
    public SecondPage(string imie)
    {
        InitializeComponent();
        labelImie.Text = $"Witaj {imie}!";
    }

    private async void OnBackClicked(object sender, EventArgs e)
    {
        await Navigation.PopAsync();
    }
}

Efekt końcowy

  1. Na pierwszej stronie uczeń wpisuje swoje imię.
  2. Po kliknięciu przycisku otwiera się druga strona, gdzie imię jest wyświetlane w etykiecie.
  3. Kliknięcie przycisku „Wróć” przenosi z powrotem do pierwszej strony.

To jest najprostsze i wystarczające rozwiązanie na INF.04, bo pokazuje:
przekazanie danych między stronami (przez konstruktor).
odanie nowej strony
przejście między stronami (PushAsync, PopAsync),

Przekazanie danych bez konstruktora

Przebuduj program tak by ze strony SecondPage przekazał informacje do strony ThirdPage (Użytkownik na MainPage wpisuje imię, przechodzi do SecondPage, a tam klika przycisk, który otwiera ThirdPage i przekazuje do niej imię w inny sposób)


Sposób 1: Ustawienie właściwości strony (bez konstruktora)

ThirdPage.xaml

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="NawigacjaApp.ThirdPage">

    <VerticalStackLayout Padding="20">
        <Label x:Name="labelDane" FontSize="24"/>
        <Button Text="Wróć" Clicked="OnBackClicked"/>
    </VerticalStackLayout>
</ContentPage>

ThirdPage.xaml.cs

namespace NawigacjaApp;

public partial class ThirdPage : ContentPage
{
    // Właściwość do przekazania danych
    public string? Imie { get; set; }

    public ThirdPage()
    {
        InitializeComponent();
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        labelDane.Text = $"Dane przekazane: {Imie}";
    }

    private async void OnBackClicked(object sender, EventArgs e)
    {
        await Navigation.PopAsync();
    }
}

SecondPage.xaml

Dodaj przycisk, który otworzy ThirdPage:

<VerticalStackLayout Padding="20">
    <Label x:Name="labelImie" FontSize="24"/>
    <Button Text="Przejdź do ThirdPage" Clicked="OnGoThirdPageClicked"/>
    <Button Text="Wróć" Clicked="OnBackClicked"/>
</VerticalStackLayout>

SecondPage.xaml.cs

namespace NawigacjaApp;

public partial class SecondPage : ContentPage
{
    private string _imie;

    public SecondPage(string imie)
    {
        InitializeComponent();
        _imie = imie;
        labelImie.Text = $"Witaj {imie}!";
    }

    private async void OnBackClicked(object sender, EventArgs e)
    {
        await Navigation.PopAsync();
    }

    private async void OnGoThirdPageClicked(object sender, EventArgs e)
    {
        var third = new ThirdPage();
        third.Imie = _imie; // przekazanie danych przez właściwość
        await Navigation.PushAsync(third);
    }
}

Jak to działa?

  1. Wpisujemy imię na MainPage i przechodzi do SecondPage.
  2. Na SecondPage jest przycisk „Przejdź do ThirdPage”.
  3. Po kliknięciu tworzona jest nowa strona ThirdPage, a wartość imienia przekazywana jest do niej poprzez właściwość Imie.
  4. W metodzie OnAppearing() na ThirdPage wartość ta jest wyświetlana w etykiecie.

Porównanie dwóch sposobów

  • Przez konstruktor (SecondPage): dane są przekazywane w momencie tworzenia strony.
  • Przez właściwość (ThirdPage): najpierw tworzysz stronę, a potem ustawiasz jej dane.

Oba sposoby są proste i na egzaminie wystarczające.

***
Innym sposobem Przekazania danych pomiędzy stronami jest Binding opisany szerzej na mojej stronie w zakładce Binding

inny sposób” z użyciem bindingu i oddzielnej klasy modelu Dane.cs. Idea jest prosta: tworzysz jeden obiekt z danymi, ustawiasz go jako BindingContext w ThirdPage, a potem ten sam obiekt przekazujesz do FourthPage. Dzięki temu wszystko, co zmienisz na jednej stronie, jest od razu widoczne na drugiej (bo obie strony „patrzą” na ten sam model).

Poniżej kompletny, prosty przykład w stylu INF.04.

Założenia

Projekt: NawigacjaApp
Strony: ThirdPage.xaml, FourthPage.xaml
Model: Dane.cs (osobny plik)


1) Model z powiadamianiem o zmianach: Dane.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace NawigacjaApp;

public class Dane : INotifyPropertyChanged
{
    private string imie = "";
    private int wiek;

    public string Imie
    {
        get => imie;
        set { imie = value; OnPropertyChanged(); }
    }

    public int Wiek
    {
        get => wiek;
        set { wiek = value; OnPropertyChanged(); }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

Wyjaśnienie: INotifyPropertyChanged sprawia, że gdy zmienisz Imie/Wiek w obiekcie, powiązane kontrolki w XAML automatycznie się odświeżą.


2) ThirdPage: wiążemy pola z modelem i nawigujemy dalej

ThirdPage.xaml

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="NawigacjaApp.ThirdPage"
             Title="ThirdPage">

    <VerticalStackLayout Padding="20" Spacing="12">
        <Label Text="Wpisz dane i przejdź dalej:" FontAttributes="Bold" />
        <Entry Placeholder="Imię" Text="{Binding Imie}" />
        <Entry Placeholder="Wiek" Keyboard="Numeric" Text="{Binding Wiek}" />
        <Button Text="Przejdź do FourthPage"
                Clicked="OnGoFourthClicked" />
    </VerticalStackLayout>
</ContentPage>

ThirdPage.xaml.cs

namespace NawigacjaApp;

public partial class ThirdPage : ContentPage
{
    private readonly Dane _dane;

    public ThirdPage()
    {
        InitializeComponent();

        // 1) Tworzymy obiekt modelu (możesz też go dostać z poprzedniej strony)
        _dane = new Dane { Imie = "Ania", Wiek = 18 };

        // 2) Ustawiamy model jako BindingContext tej strony
        BindingContext = _dane;
    }

    private async void OnGoFourthClicked(object sender, EventArgs e)
    {
        // 3) Przekazujemy TEN SAM obiekt do FourthPage
        await Navigation.PushAsync(new FourthPage(_dane));
    }
}

3) FourthPage: używamy tego samego BindingContext

FourthPage.xaml

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="NawigacjaApp.FourthPage"
             Title="FourthPage">

    <VerticalStackLayout Padding="20" Spacing="12">
        <Label Text="Dane z ThirdPage (ten sam model):" FontAttributes="Bold" />
        <Label Text="Imię:" />
        <Label Text="{Binding Imie}" FontSize="20" />

        <Label Text="Wiek:" />
        <Label Text="{Binding Wiek}" FontSize="20" />

        <!-- pokażmy też, że tu można edytować te same dane -->
        <Entry Placeholder="Zmień imię (zobacz, że działa dwukierunkowo)"
               Text="{Binding Imie}" />

        <Button Text="Wróć"
                Clicked="OnBackClicked" />
    </VerticalStackLayout>
</ContentPage>

FourthPage.xaml.cs

namespace NawigacjaApp;

public partial class FourthPage : ContentPage
{
    // Dostajemy TEN SAM obiekt Dane
    public FourthPage(Dane dane)
    {
        InitializeComponent();
        BindingContext = dane; // kluczowe
    }

    private async void OnBackClicked(object sender, EventArgs e)
    {
        await Navigation.PopAsync();
    }
}

Jak to działa (w skrócie dla ucznia)

  1. ThirdPage tworzy obiekt Dane i ustawia go jako BindingContext.
  2. Pola Entry są połączone z właściwościami Imie i Wiek.
  3. Przycisk przejścia tworzy FourthPage, przekazując ten sam obiekt Dane.
  4. FourthPage też ustawia ten obiekt jako BindingContext, więc widzi te same wartości.
  5. Jeśli zmienisz imię na FourthPage, po powrocie do ThirdPage zobaczysz aktualizację — to zasługa INotifyPropertyChanged i tego, że obie strony „patrzą” na ten sam obiekt.

To jest „inny sposób” niż:
• konstruktor z prostym stringiem,
• ustawienie pojedynczej właściwości strony,
• Shell + QueryProperty.
Tu pracujemy na wspólnym modelu i bindingu, co jest bardzo czytelne i „egzaminowe”.


Bonus: wariant bez przekazywania w konstruktorze (singleton)

Jeśli chcesz nie przekazywać modelu w konstruktorze, możesz zarejestrować Dane jako singleton w MauiProgram.cs i wstrzyknąć go w obu stronach:

MauiProgram.cs

builder.Services.AddSingleton<Dane>();           // model współdzielony
builder.Services.AddTransient<ThirdPage>();
builder.Services.AddTransient<FourthPage>();

ThirdPage.xaml.cs

public ThirdPage(Dane dane)
{
    InitializeComponent();
    BindingContext = dane;
}

FourthPage.xaml.cs

public FourthPage(Dane dane)
{
    InitializeComponent();
    BindingContext = dane; // ten sam singleton
}

Nawigacja wtedy prosto:

// gdzieś: await Navigation.PushAsync(serviceProvider.GetRequiredService<FourthPage>());

Navigacja z AppShell

Założenia projektu

Nazwa: NawigacjaShellApp
Strony: MainPage.xaml, SecondPage.xaml
Nawigacja: Shell (GoToAsync) + przekazywanie parametrów

1) Utworzenie projektu

Visual Studio → Nowy projekt → .NET MAUI App → nazwa NawigacjaShellApp.
Domyślnie projekt ma AppShell.xaml i jest ustawiony jako start (w App.xaml.cs).

2) AppShell – mapowanie stron i tras

AppShell.xaml – dodaj pozycję dla MainPage (start) i zarejestruj SecondPage jako trasę:

<?xml version="1.0" encoding="utf-8" ?>
<Shell
    x:Class="NawigacjaShellApp.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:NawigacjaShellApp">

    <!-- Startowa zakładka/pozycja menu -->
    <ShellContent
        Title="Start"
        ContentTemplate="{DataTemplate local:MainPage}" />
</Shell>

AppShell.xaml.cs – rejestracja trasy do SecondPage:

namespace NawigacjaShellApp;

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
        Routing.RegisterRoute(nameof(SecondPage), typeof(SecondPage));
    }
}

3) MainPage – wejście i przejście do SecondPage z parametrem

MainPage.xaml:

<ContentPage
    x:Class="NawigacjaShellApp.MainPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    Title="MainPage">

    <VerticalStackLayout Padding="20" Spacing="12">
        <Entry x:Name="poleImie" Placeholder="Podaj imię" />
        <Button Text="Idź do SecondPage" Clicked="OnGoClicked" />
    </VerticalStackLayout>
</ContentPage>

MainPage.xaml.cs:

namespace NawigacjaShellApp;

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private async void OnGoClicked(object sender, EventArgs e)
    {
        var imie = poleImie.Text ?? "";

        // Przekazywanie parametru "Imie" do SecondPage
        await Shell.Current.GoToAsync(nameof(SecondPage), new Dictionary<string, object>
        {
            ["Imie"] = imie
        });
    }
}

4) SecondPage – odbiór parametru przez QueryProperty (najprościej)

SecondPage.xaml:

<ContentPage
    x:Class="NawigacjaShellApp.SecondPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    Title="SecondPage">

    <VerticalStackLayout Padding="20" Spacing="12">
        <Label Text="Odebrane dane:" FontAttributes="Bold" />
        <Label Text="{Binding Imie}" FontSize="24" />
        <Button Text="Wróć" Clicked="OnBackClicked" />
    </VerticalStackLayout>
</ContentPage>

SecondPage.xaml.cs:

namespace NawigacjaShellApp;

// Powiedz Shellowi: wstrzyknij wartość parametru "Imie" do właściwości Imie
[QueryProperty(nameof(Imie), "Imie")]
public partial class SecondPage : ContentPage
{
    private string imie = "";
    public string Imie
    {
        get => imie;
        set
        {
            imie = value;
            OnPropertyChanged(); // żeby Binding w Label się odświeżył
        }
    }

    public SecondPage()
    {
        InitializeComponent();
        BindingContext = this; // szybkie podejście na start
    }

    private async void OnBackClicked(object sender, EventArgs e)
    {
        // W Shell wracamy tak samo: ".." = o jeden ekran do tyłu
        await Shell.Current.GoToAsync("..");
    }
}

To wszystko. Masz AppShell, trasę, przejście i odbiór danych.

5) Alternatywny odbiór parametrów: IQueryAttributable

Jeśli wolisz ręcznie obsłużyć słownik parametrów (np. kilka na raz albo obiekt), możesz zamiast [QueryProperty] użyć interfejsu:

public partial class SecondPage : ContentPage, IQueryAttributable
{
    public string Imie { get; set; } = "";

    public SecondPage()
    {
        InitializeComponent();
        BindingContext = this;
    }

    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.TryGetValue("Imie", out var val) && val is string s)
        {
            Imie = s;
            OnPropertyChanged(nameof(Imie));
        }
    }

    private async void OnBackClicked(object sender, EventArgs e)
    {
        await Shell.Current.GoToAsync("..");
    }
}

6) Przekazywanie bardziej złożonych danych (model)

Shell wygodnie przenosi typy proste (string, int itp.). Dla obiektów masz dwa wyjścia:

  1. Słownik w GoToAsync (działa, dopóki nawigujesz w ramach tej samej instancji aplikacji): var model = new Uczen { Imie = "Ania", Klasa = "1TI" }; await Shell.Current.GoToAsync(nameof(SecondPage), new Dictionary<string, object> { ["ModelUczen"] = model }); Odbiór: [QueryProperty(nameof(ModelUczen), "ModelUczen")] public Uczen ModelUczen { get; set; }
  2. Wspólny model przez DI (singleton) – strony korzystają z tej samej instancji (polecane przy większych apkach).

Kiedy wybrać Shell

Gdy:

  • planujesz menu boczne (Flyout) lub dolne zakładki (Tabs),
  • chcesz proste, czytelne trasy i deeplinki,
  • łatwo przekazywać parametry i wracać („..”).

Krótka treść zadania (styl INF.04)

Projekt: NawigacjaShellApp
Wymagania:

  1. W AppShell zarejestruj trasę do SecondPage.
  2. W MainPage wpisz imię i przejdź do SecondPage przyciskiem.
  3. SecondPage ma odebrać parametr Imie i wyświetlić go w etykiecie.
  4. Dodaj przycisk „Wróć” (powrót do MainPage).

Zaliczenie: działa nawigacja Shell i przekazywanie parametru.