Dataczwartek, 13 sierpnia 2026 Czas17:44:19
← Aplikacje webowe

Lekcja 31. Routing i layout w Blazor — nawigacja między komponentami

Średni

Po co się tego uczymy?

Prawdziwa aplikacja Blazor ma wiele stron/komponentów — listę produktów, formularz dodawania, szczegóły. Ta lekcja pokazuje, jak poruszać się między nimi (routing), jak zbudować wspólne menu nawigacyjne widoczne na każdej stronie (layout — dokładnie ta sama potrzeba, co _Layout.cshtml z lekcji 5, tylko w świecie komponentów), i jak przechodzić między stronami z poziomu kodu C#, analogicznie do Navigation.PushAsync z lekcji o nawigacji w dziale MAUI.

Teoria

Routing przez @page na komponencie. W przeciwieństwie do Razor Pages (gdzie adres URL wynika z lokalizacji pliku), w Blazor KAŻDY komponent, który ma być samodzielną, routowalną "stroną", deklaruje swój adres JAWNIE przez dyrektywę @page "/adres" na samej górze pliku — komponent MOŻE mieć NAWET WIĘCEJ niż jeden adres (kilka linii @page pod rząd), np. dla obsługi zarówno /produkty, jak i /produkty/lista. Parametry w adresie: @page "/produkty/{id:int}", odebrane przez [Parameter] public int Id { get; set; } w bloku @code — bardzo podobne do @page "{id:int}" z Razor Pages w lekcji 12.

Nawigacja z kodu — NavigationManager. Wstrzykiwana usługa (@inject NavigationManager Nawigacja) pozwalająca przejść na inną stronę PROGRAMOWO (np. po udanym zapisaniu formularza): Nawigacja.NavigateTo("/produkty") — bezpośredni odpowiednik RedirectToPage(...) z Razor Pages i Navigation.PushAsync(...) z działu MAUI. NavigationManager.Uri zwraca aktualny adres URL, przydatny np. do podświetlenia aktywnego elementu menu.

Layout w Blazor — komponent dziedziczący po LayoutComponentBase. Plik MainLayout.razor (domyślnie w Components/Layout/) to bezpośredni odpowiednik _Layout.cshtml z lekcji 5 — zawiera wspólne menu, nagłówek, stopkę, a w miejscu @Body (właściwość dziedziczona z LayoutComponentBase) wstawiana jest treść AKTUALNIE wyświetlanego komponentu-strony. Komponent-strona wskazuje swój layout przez dyrektywę @layout NazwaLayoutu (albo — częściej — layout ustawiony jest GLOBALNIE dla WSZYSTKICH stron w pliku Routes.razor/App.razor).

Komponent NavLink. Specjalna, wbudowana wersja linku (<NavLink href="/produkty">Produkty</NavLink>) AUTOMATYCZNIE dodająca klasę CSS active, gdy użytkownik znajduje się AKTUALNIE na tej stronie — dzięki temu menu nawigacyjne może wizualnie wyróżnić bieżącą sekcję BEZ ręcznego porównywania adresu URL w kodzie (choć taka ręczna technika też jest możliwa przez NavigationManager.Uri).

Schemat

Components/Layout/MainLayout.razor  (odpowiednik _Layout.cshtml)
┌────────────────────────────────────┐
│ <nav>                               │
│   <NavLink href="/">Strona główna</NavLink>
│   <NavLink href="/produkty">Produkty</NavLink>
│ </nav>                              │
│                                      │
│ @Body  ←──────────────────────────┼─── TU wstawiana jest treść AKTUALNEJ strony
│                                      │
│ <footer>...</footer>                │
└────────────────────────────────────┘

Components/Pages/Produkty.razor          Components/Pages/SzczegolyProduktu.razor
@page "/produkty"                        @page "/produkty/{id:int}"
        │                                        ▲
        │  klik na produkt                        │
        │  Nawigacja.NavigateTo($"/produkty/{p.Id}")
        └────────────────────────────────────────┘

NavLink href="/produkty"
        │  (gdy Uri == "/produkty")
        ▼
automatycznie dostaje klasę CSS "active"

Przykład z życia

Aplikacja sklepowa w Blazor ma wspólne menu (Strona główna, Produkty, Koszyk, Kontakt) widoczne na KAŻDEJ stronie dzięki MainLayout.razor, a kliknięcie konkretnego produktu na liście przenosi na stronę jego szczegółów pod adresem zawierającym Id produktu — dokładnie ta sama koncepcja, którą znasz z _Layout.cshtml i routingu parametrycznego z wcześniejszych lekcji, tylko zrealizowana w świecie interaktywnych komponentów.

Components/Layout/MainLayout.razor + SzczegolyProduktu.razor

<!-- Components/Layout/MainLayout.razor -->
@inherits LayoutComponentBase

<div class="strona">
    <nav>
        <NavLink href="/" Match="NavLinkMatch.All">Strona główna</NavLink>
        <NavLink href="/produkty">Produkty</NavLink>
        <NavLink href="/dodaj-produkt">Dodaj produkt</NavLink>
    </nav>

    <main>
        @Body
    </main>

    <footer>
        <p>© @DateTime.Now.Year Moja Firma</p>
    </footer>
</div>

<!-- ------------------------------------------------------------- -->

<!-- Components/Pages/SzczegolyProduktu.razor -->
@page "/produkty/{Id:int}"
@rendermode InteractiveServer
@inject NavigationManager Nawigacja

<h1>Szczegóły produktu #@Id</h1>
<p>@nazwaProduktu</p>

<button @onclick="WrocDoListy">Wróć do listy</button>

SzczegolyProduktu.razor.cs + Produkty.razor.cs (fragment)

// Components/Pages/SzczegolyProduktu.razor.cs
using Microsoft.AspNetCore.Components;

namespace MojaStronaApp.Components.Pages;

public partial class SzczegolyProduktu : ComponentBase
{
    [Parameter]
    public int Id { get; set; }

    [Inject]
    public NavigationManager Nawigacja { get; set; } = default!;

    private string nazwaProduktu = "";

    protected override void OnParametersSet()
    {
        // Tu docelowo: pobranie produktu z bazy po Id (EF Core, lekcja 16-17)
        nazwaProduktu = $"Przykładowy produkt o Id={Id}";
    }

    private void WrocDoListy()
    {
        Nawigacja.NavigateTo("/produkty");
    }
}

// ---------------------------------------------------------------------

// Components/Pages/Produkty.razor.cs - nawigacja DO szczegółów po kliknięciu
public partial class Produkty : ComponentBase
{
    [Inject]
    public NavigationManager Nawigacja { get; set; } = default!;

    private void PokazSzczegoly(int idProduktu)
    {
        Nawigacja.NavigateTo($"/produkty/{idProduktu}");
    }
}

Komentarz i wyjaśnienie kodu

@inherits LayoutComponentBase na górze MainLayout.razor to KLUCZOWA linia — bez niej ten plik byłby zwykłym komponentem, nie layoutem, i właściwość @Body w ogóle by nie istniała.

Match="NavLinkMatch.All" na linku do strony głównej (/) jest WAŻNE — bez tego NavLink domyślnie dopasowuje adres jako PREFIKS, więc link do / byłby oznaczony jako "aktywny" na KAŻDEJ podstronie (bo każdy adres zaczyna się od /); NavLinkMatch.All wymusza DOKŁADNE dopasowanie całego adresu.

[Parameter] public int Id { get; set; } odbiera wartość z segmentu adresu URL ({Id:int} w @page) — nazwa właściwości C# MUSI dokładnie odpowiadać nazwie w nawiasach klamrowych adresu (wielkość liter ma znaczenie). Metoda OnParametersSet() (a NIE OnInitialized()) jest właściwym miejscem na reakcję na ZMIANĘ parametru Id — gdyby użytkownik przeszedł z /produkty/3 bezpośrednio do /produkty/7 (bez przeładowania całej aplikacji), OnInitialized() NIE wykonałby się ponownie, ale OnParametersSet() — tak.

Ćwiczenie samodzielne

Zbuduj MainLayout.razor z menu jak w przykładzie i sprawdź, że wszystkie strony Twojego projektu Blazor (z poprzednich dwóch lekcji) automatycznie pokazują to samo menu. Dodaj stronę szczegółów produktu z parametrem Id w adresie i przycisk nawigujący do niej z listy produktów.

Zadania do pracy własnej

  1. Dodaj do menu w MainLayout.razor wizualne wyróżnienie aktywnej strony (sprawdź, że klasa CSS active generowana przez NavLink faktycznie zmienia wygląd linku — dodaj odpowiedni styl w CSS).

  2. Zbuduj komponent SzczegolyProduktu, który po nieudanym wyszukaniu produktu o podanym Id (np. Id spoza zakresu 1-100) przekierowuje z powrotem do listy produktów przez NavigationManager.NavigateTo("/produkty"), zamiast pokazywać pusty/błędny ekran.

  3. Zbuduj DRUGI, alternatywny layout (PustyLayout.razor, bez menu nawigacyjnego) przeznaczony dla strony logowania, i skonfiguruj JEDNĄ konkretną stronę tak, żeby używała tego layoutu zamiast domyślnego (przez @layout PustyLayout na górze tej strony) — sprawdź, że pozostałe strony nadal poprawnie korzystają z MainLayout.

Typowe błędy

Brak @inherits LayoutComponentBase w pliku layoutu — bez tego @Body nie istnieje, plik nie może pełnić roli layoutu.

Reagowanie na zmianę parametru w OnInitialized() zamiast OnParametersSet() — przy nawigacji między dwoma wariantami TEGO SAMEGO komponentu (np. /produkty/3/produkty/7) Blazor może ZACHOWAĆ istniejącą instancję komponentu (bez pełnego przetworzenia od nowa), więc OnInitialized() nie wykona się ponownie — trzeba użyć OnParametersSet().

Niedopasowanie nazwy właściwości [Parameter] do nazwy segmentu w @page (wielkość liter, literówka) — Blazor po cichu NIE przypisze wartości, właściwość pozostanie z wartością domyślną, bez żadnego widocznego błędu kompilacji.

Nawiązanie do egzaminu zawodowego

To materiał DODATKOWY — mechanizm layoutów i routingu jest niemal 1:1 koncepcyjnie zgodny z tym, czego nauczyłeś się w Razor Pages (lekcja 5) i w nawigacji MAUI, co pokazuje spójność architektoniczną między różnymi technologiami UI, niezależnie od tego, czy dana technologia jest wymagana na egzaminie. Ostatnia lekcja tego dodatkowego bloku łączy wszystko w praktyczny projekt.