Dataczwartek, 13 sierpnia 2026 Czas18:15:46
← Aplikacje webowe

Lekcja 29. Komponenty Blazor — parametry, zdarzenia własne i cykl życia

Trudny / egzaminacyjny

Po 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

  1. Dodaj do KartaProduktu dodatkowy parametr [Parameter] public bool CzyPromocja { get; set; } i warunkowo (@if) pokazuj czerwoną etykietę "PROMOCJA" na kartach, gdzie ta wartość jest true.

  2. Zbuduj komponent "Zegar" wyświetlający aktualną godzinę, aktualizowany co sekundę przez PeriodicTimer uruchomiony w OnInitializedAsync, poprawnie ZATRZYMYWANY przy usunięciu komponentu (interfejs IDisposable, metoda Dispose() zatrzymująca timer) — analogicznie do zegara z lekcji o MAUI, tylko jako komponent webowy.

  3. Zbuduj komponent "OknoModalne" przyjmujący [Parameter] public RenderFragment? ChildContent { get; set; } (dowolna treść między znacznikami) oraz parametr Tytul i zdarzenie OnZamknieto — 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.