Lekcja 31. Routing i layout w Blazor — nawigacja między komponentami
ŚredniPo 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
Dodaj do menu w
MainLayout.razorwizualne wyróżnienie aktywnej strony (sprawdź, że klasa CSSactivegenerowana przezNavLinkfaktycznie zmienia wygląd linku — dodaj odpowiedni styl w CSS).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 przezNavigationManager.NavigateTo("/produkty"), zamiast pokazywać pusty/błędny ekran.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 PustyLayoutna górze tej strony) — sprawdź, że pozostałe strony nadal poprawnie korzystają zMainLayout.
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.