Lekcja 29. Komponenty Blazor — parametry, zdarzenia własne i cykl życia
Trudny / egzaminacyjnyPo co się tego uczymy?
Prawdziwa aplikacja nie mieści się w jednym komponencie — dzieli się na mniejsze, wielokrotnego użytku klocki (np. "karta produktu", "przycisk oceny gwiazdkami"), które przekazują sobie dane i informują się nawzajen o zdarzeniach. To dokładnie ta sama potrzeba, którą w dziale MAUI rozwiązywał DataTemplate w CollectionView — tu poznasz analogiczny mechanizm w Blazor: komponenty potomne, parametry wejściowe i zdarzenia wychodzące.
Teoria
Komponent potomny i parametry. Każdy plik .razor to jednocześnie DEFINICJA komponentu, którego można użyć wewnątrz INNEGO komponentu jak zwykłego znacznika HTML: <KartaProduktu Nazwa="Klawiatura" Cena="89.99" />. Żeby komponent KartaProduktu mógł PRZYJĄĆ takie dane, jego właściwości muszą być oznaczone atrybutem [Parameter] — to bezpośredni odpowiednik przekazywania danych do strony przez konstruktor w lekcji o nawigacji w dziale MAUI, tylko realizowany przez atrybuty znaczników HTML zamiast kodu C#.
Zdarzenia własne — komunikacja OD komponentu potomnego DO rodzica. Parametry przekazują dane W DÓŁ (rodzic → dziecko), ale co, gdy komponent potomny musi POINFORMOWAĆ rodzica o czymś (np. "użytkownik kliknął ten konkretny produkt")? Do tego służy właściwość typu EventCallback<T> oznaczona [Parameter] — rodzic przekazuje METODĘ do wywołania (<KartaProduktu OnKliknieto="ObslugaKlikniecia" />), a komponent potomny wywołuje ją w odpowiednim momencie przez await OnKliknieto.InvokeAsync(wartosc). To bezpośredni odpowiednik zdarzeń Clicked/Click podpinanych w XAML z działów WPF/MAUI, tylko teraz to TY definiujesz WŁASNE, niestandardowe zdarzenie na swoim komponencie.
Cykl życia komponentu — metody wywoływane automatycznie:
| Metoda | Kiedy się wykonuje |
|---|---|
OnInitialized() / OnInitializedAsync() |
RAZ, przy pierwszym utworzeniu komponentu — dobre miejsce na pobranie początkowych danych (np. z bazy) |
OnParametersSet() |
za KAŻDYM razem, gdy zmienią się parametry przekazane przez rodzica |
OnAfterRender(bool pierwszyRaz) |
PO wyrenderowaniu komponentu w przeglądarce — jedyne bezpieczne miejsce na wywołania JavaScript (JS interop) albo operacje wymagające gotowego DOM |
Dispose() (interfejs IDisposable) |
gdy komponent jest usuwany z ekranu — dobre miejsce na sprzątanie (np. zatrzymanie timera z poprzedniej lekcji) |
To bezpośredni odpowiednik konstruktora MainPage() i zdarzenia OnAppearing z lekcji o nawigacji w dziale MAUI — tylko w Blazor cykl życia ma WIĘCEJ, bardziej precyzyjnie rozdzielonych etapów.
Dyrektywa @ChildContent. Komponenty mogą też przyjmować DOWOLNĄ treść HTML/Razor MIĘDZY swoimi znacznikami otwierającym/zamykającym (jak <div>treść</div>) — właściwość [Parameter] public RenderFragment? ChildContent { get; set; } przechwytuje tę treść, a @ChildContent w widoku komponentu wskazuje, gdzie ją wstawić — przydatne przy budowaniu np. reużywalnej "karty" czy "okna dialogowego" o zmiennej zawartości.
Schemat
Komponent RODZICA (np. ListaProduktow.razor)
│
│ <KartaProduktu Nazwa="Klawiatura" Cena="89.99"
│ OnKliknieto="ObsluzKlikniecie" />
▼
Komponent POTOMNY (KartaProduktu.razor)
[Parameter] public string Nazwa { get; set; } ← DANE W DÓŁ
[Parameter] public double Cena { get; set; }
[Parameter] public EventCallback<string> OnKliknieto { get; set; }
│
│ użytkownik klika kartę
▼
await OnKliknieto.InvokeAsync(Nazwa) ← ZDARZENIE W GÓRĘ
│
▼
Komponent RODZICA: ObsluzKlikniecie(string nazwa) { ... }
Cykl życia komponentu (kolejność wywołań):
OnInitialized() → OnParametersSet() → renderowanie → OnAfterRender(true)
│
│ (zmiana parametru od rodzica)
▼
OnParametersSet() → ponowne renderowanie → OnAfterRender(false)
Przykład z życia
Lista produktów w sklepie internetowym renderowana z wielu powtarzających się komponentów "Karta produktu" (nazwa, cena, przycisk "Dodaj do koszyka") — każda karta jest osobnym, samodzielnym komponentem, ale gdy użytkownik kliknie "Dodaj do koszyka" na KONKRETNEJ karcie, komponent rodzica (lista) musi się o tym dowiedzieć, żeby zaktualizować licznik koszyka widoczny gdzie indziej na stronie — dokładnie ten scenariusz pokazuje przykład w tej lekcji.
Components/Shared/KartaProduktu.razor + Components/Pages/ListaProduktow.razor
<!-- Components/Shared/KartaProduktu.razor -->
<div class="karta-produktu" @onclick="Kliknieto">
<h3>@Nazwa</h3>
<p>@Cena.ToString("F2") zł</p>
<button>Dodaj do koszyka</button>
</div>
<!-- ------------------------------------------------------------- -->
<!-- Components/Pages/ListaProduktow.razor - użycie komponentu potomnego -->
@page "/produkty"
@rendermode InteractiveServer
<h1>Produkty (w koszyku: @liczbaWKoszyku)</h1>
@foreach (var p in produkty)
{
<KartaProduktu Nazwa="@p.Nazwa" Cena="@p.Cena" OnKliknieto="DodajDoKoszyka" />
}
KartaProduktu.razor.cs + ListaProduktow.razor.cs
// Components/Shared/KartaProduktu.razor.cs (albo blok @code w tym samym pliku)
using Microsoft.AspNetCore.Components;
namespace MojaStronaApp.Components.Shared;
public partial class KartaProduktu : ComponentBase
{
[Parameter]
public string Nazwa { get; set; } = "";
[Parameter]
public double Cena { get; set; }
[Parameter]
public EventCallback<string> OnKliknieto { get; set; }
private async Task Kliknieto()
{
await OnKliknieto.InvokeAsync(Nazwa);
}
}
// ---------------------------------------------------------------------
// Components/Pages/ListaProduktow.razor.cs
using Microsoft.AspNetCore.Components;
namespace MojaStronaApp.Components.Pages;
public partial class ListaProduktow : ComponentBase
{
private int liczbaWKoszyku = ;
private List<Produkt> produkty = new();
protected override async Task OnInitializedAsync()
{
// Tu docelowo: await _context.Produkty.ToListAsync() (EF Core, lekcja 16)
produkty = new List<Produkt>
{
new Produkt { Nazwa = "Klawiatura", Cena = 89.99 },
new Produkt { Nazwa = "Monitor", Cena = 799.00 }
};
await Task.CompletedTask;
}
private void DodajDoKoszyka(string nazwaProduktu)
{
liczbaWKoszyku++;
// W prawdziwej aplikacji: zapis do sesji/bazy, tu tylko licznik w pamięci
}
}
public class Produkt
{
public string Nazwa { get; set; } = "";
public double Cena { get; set; }
}
Komentarz i wyjaśnienie kodu
Zwróć uwagę na podział pliku komponentu na DWA pliki — KartaProduktu.razor (znaczniki) i KartaProduktu.razor.cs (kod C#, klasa partial) — analogiczny do XAML+code-behind z WPF/MAUI. To podejście jest OPCJONALNE (dla prostych komponentów wygodniej trzymać wszystko w jednym pliku z blokiem @code), ale dla większych, bardziej złożonych komponentów znacznie poprawia czytelność.
OnKliknieto.InvokeAsync(Nazwa) w komponencie potomnym NIE wie NIC o tym, co konkretnie rodzic zrobi z przekazaną nazwą produktu — komponent potomny tylko "zgłasza fakt" (kliknięto tę kartę), a CAŁA logika reakcji (zwiększenie licznika koszyka) żyje w komponencie rodzica. To ważna zasada hermetyzacji — komponenty potomne powinny być jak najbardziej "głupie" i reużywalne, bez wiedzy o kontekście, w którym są używane.
OnInitializedAsync() jest miejscem, gdzie w prawdziwej aplikacji pobierzesz dane z bazy (przez EF Core, wstrzyknięty DbContext dokładnie jak w lekcji 16-17) — tutaj, dla uproszczenia przykładu, dane są wpisane na sztywno.
Ćwiczenie samodzielne
Zbuduj komponent potomny (np. "OcenaGwiazdkowa" z pięcioma klikalnymi gwiazdkami) przyjmujący parametr początkowej oceny i zgłaszający zdarzenie EventCallback<int> po każdej zmianie — użyj go w komponencie rodzica, wyświetlając aktualnie wybraną ocenę.
Zadania do pracy własnej
Dodaj do
KartaProduktudodatkowy parametr[Parameter] public bool CzyPromocja { get; set; }i warunkowo (@if) pokazuj czerwoną etykietę "PROMOCJA" na kartach, gdzie ta wartość jesttrue.Zbuduj komponent "Zegar" wyświetlający aktualną godzinę, aktualizowany co sekundę przez
PeriodicTimeruruchomiony wOnInitializedAsync, poprawnie ZATRZYMYWANY przy usunięciu komponentu (interfejsIDisposable, metodaDispose()zatrzymująca timer) — analogicznie do zegara z lekcji o MAUI, tylko jako komponent webowy.Zbuduj komponent "OknoModalne" przyjmujący
[Parameter] public RenderFragment? ChildContent { get; set; }(dowolna treść między znacznikami) oraz parametrTytuli zdarzenieOnZamknieto— użyj go DWUKROTNIE w różnych miejscach aplikacji z RÓŻNĄ zawartością środka (raz z formularzem, raz z prostym komunikatem), pokazując, jak jeden reużywalny komponent obsługuje różne przypadki użycia.
Typowe błędy
Zapomnienie o atrybucie [Parameter] na właściwości, która ma przyjmować dane od rodzica — bez tego atrybutu Blazor zignoruje próbę przekazania wartości przez znacznik HTML, właściwość pozostanie z wartością domyślną.
Umieszczanie logiki biznesowej w komponencie POTOMNYM zamiast zgłaszania zdarzenia do rodzica — komponenty potomne powinny być reużywalne i "głupie"; jeśli KartaProduktu sama decydowałaby, co się dzieje z koszykiem, nie dałoby się jej użyć w innym kontekście bez zmiany jej kodu.
Brak sprzątania zasobów (timerów, subskrypcji) w Dispose() — komponent, który uruchamia timer w OnInitializedAsync, ale nie zatrzymuje go przy usunięciu, będzie nadal "tykał" w tle nawet po opuszczeniu strony przez użytkownika, marnując zasoby serwera (szczególnie istotne w Blazor Server, gdzie wiele komponentów żyje jednocześnie dla różnych użytkowników).
Nawiązanie do egzaminu zawodowego
To materiał DODATKOWY (poza wymaganą listą frameworków z INF.04.7.2), ale pokazuje architekturę komponentową fundamentalną dla wszystkich nowoczesnych frameworków webowych (Angular, React, Blazor — wszystkie opierają się na tej samej idei: małe, reużywalne komponenty komunikujące się przez parametry i zdarzenia) — jeśli rozumiesz to na przykładzie Blazor, łatwiej przyswoisz analogiczne mechanizmy w Angularze czy React. Kolejna lekcja rozszerza komponenty o formularze i walidację, łącząc je z Data Annotations poznanymi w lekcji 7.