Lekcja 32. Projekt Blazor: kalkulator BMI i kolorowa gra zgadywanka
Trudny / egzaminacyjnyPo co się tego uczymy?
Czas połączyć wszystko z lekcji 9-12 (komponenty, parametry, zdarzenia, binding, formularze, routing) w dwa kompletne, samodzielne projekty — kalkulator BMI i grę „Kolorowy zgadywacz”. To dokładnie te same projekty, które w oryginalnej wersji tego działu były budowane w Angularze — tutaj budujesz identyczną funkcjonalność w 100% w C#, bez jednej linijki TypeScriptu, pokazując, że Blazor daje pełną moc interaktywnych aplikacji webowych w znanym Ci już języku.
Teoria
Projekt 1 — Kalkulator BMI. Formularz z dwoma polami (wzrost w cm, waga w kg), przycisk "Oblicz", i wynik z kategorią (niedowaga/norma/nadwaga/otyłość) wyliczaną na podstawie wzoru BMI = waga / (wzrost_w_metrach)². To praktyczne zastosowanie WSZYSTKICH mechanizmów z tego bloku lekcji: @bind do pól formularza, walidacja (dodatnie wartości), i natychmiastowa aktualizacja wyniku po kliknięciu — bez przeładowania strony.
Projekt 2 — Kolorowy zgadywacz. Gra pokazująca kilka kolorowych przycisków, gdzie tylko JEDEN odpowiada aktualnie wyświetlonemu (opisanemu słownie) kolorowi — kliknięcie poprawnego przycisku zwiększa licznik trafień, błędny — po prostu przechodzi dalej. To świetne ćwiczenie na: generowanie losowych danych (Random), dynamiczne stylowanie elementów (przekazanie koloru jako parametru do stylu style="background-color: @kolor"), oraz zarządzanie stanem gry (aktualne pytanie, wynik, licznik pytań) w bloku @code.
Wzorzec "stan gry w jednym miejscu". W obu projektach cały stan (wpisane dane, wynik, licznik) żyje jako pola w bloku @code GŁÓWNEGO komponentu strony — to ten sam wzorzec "jedno źródło prawdy" (single source of truth), który w Angularze realizowałaby usługa (service) albo stan komponentu, a w MAUI — pola klasy PageModel/ContentPage. Niezależnie od frameworka, zasada jest identyczna: stan aplikacji trzymany w jednym, jasno określonym miejscu, a widok TYLKO go odzwierciedla.
Losowanie w C# — klasa Random. Random.Shared.Next(minimalna, maksymalnaWyłącznie) to nowoczesny, gotowy do użycia generator liczb losowych (współdzielony, bezpieczny wątkowo) — używany do wylosowania, KTÓRY z kolorowych przycisków będzie tym razem poprawną odpowiedzią.
Schemat
PROJEKT 1: Kalkulator BMI
<input @bind="wzrostCm"> <input @bind="wagaKg"> <button @onclick="Oblicz">
│
▼
Oblicz() { bmi = wagaKg / Math.Pow(wzrostCm/100.0, 2); kategoria = ...; }
│
▼
@bmi.ToString("F1") + @kategoria ← natychmiastowa aktualizacja widoku
PROJEKT 2: Kolorowy zgadywacz
┌─────────────────────────────────┐
│ "Kliknij kolor: CZERWONY" │
│ [czerwony] [niebieski] [zielony]│ ← przyciski z @onclick="SprawdzOdpowiedz(kolor)"
└─────────────────────────────────┘
│
poprawny ───┴─── błędny
│ │
▼ ▼
wynik++ (bez zmiany wyniku)
│ │
└──────────┬─────────┘
▼
LosujNastepnePytanie() → Random.Shared.Next(...)
│
▼
@wynik / @liczbaPytan ← na bieżąco widoczny wynik
Przykład z życia
Kalkulator BMI to klasyczne narzędzie zdrowotne dostępne na niezliczonych stronach internetowych, a gra typu "zgadnij kolor" to prosty, ale kompletny przykład mechaniki quizowej (losowe pytanie, sprawdzenie odpowiedzi, zliczanie wyniku) — dokładnie ten sam wzorzec mechaniki quizu, na którym opiera się np. system testów egzaminacyjnych na samej stronie bitedu.pl.
Components/Pages/KalkulatorBmi.razor + KolorowyZgadywacz.razor
<!-- Components/Pages/KalkulatorBmi.razor -->
@page "/bmi"
@rendermode InteractiveServer
<h1>Kalkulator BMI</h1>
<div>
<label>Wzrost (cm)</label>
<input type="number" @bind="wzrostCm" />
</div>
<div>
<label>Waga (kg)</label>
<input type="number" @bind="wagaKg" />
</div>
<button @onclick="Oblicz">Oblicz BMI</button>
@if (bmi is not null)
{
<p>Twoje BMI: <strong>@bmi.Value.ToString("F1")</strong> - @kategoria</p>
}
<!-- ------------------------------------------------------------- -->
<!-- Components/Pages/KolorowyZgadywacz.razor -->
@page "/zgaduj-kolor"
@rendermode InteractiveServer
<h1>Kolorowy zgadywacz</h1>
<p>Kliknij kolor: <strong>@nazwaSzukanegoKoloru</strong></p>
<div style="display:flex; gap:10px;">
@foreach (var (nazwa, kodHex) in dostepneKolory)
{
<button style="background-color:@kodHex; width:60px; height:60px;"
@onclick="() => SprawdzOdpowiedz(nazwa)"></button>
}
</div>
<p>Wynik: @wynik / @liczbaPytan</p>
KalkulatorBmi.razor.cs + KolorowyZgadywacz.razor.cs
// Components/Pages/KalkulatorBmi.razor.cs
using Microsoft.AspNetCore.Components;
namespace MojaStronaApp.Components.Pages;
public partial class KalkulatorBmi : ComponentBase
{
private double wzrostCm;
private double wagaKg;
private double? bmi;
private string kategoria = "";
private void Oblicz()
{
if (wzrostCm <= || wagaKg <= )
{
bmi = null;
kategoria = "Podaj poprawne, dodatnie wartości.";
return;
}
double wzrostM = wzrostCm / 100.0;
bmi = wagaKg / (wzrostM * wzrostM);
kategoria = bmi switch
{
< 18.5 => "niedowaga",
< 25.0 => "waga prawidłowa",
< 30.0 => "nadwaga",
_ => "otyłość"
};
}
}
// ---------------------------------------------------------------------
// Components/Pages/KolorowyZgadywacz.razor.cs
using Microsoft.AspNetCore.Components;
namespace MojaStronaApp.Components.Pages;
public partial class KolorowyZgadywacz : ComponentBase
{
private readonly List<(string Nazwa, string KodHex)> dostepneKolory = new()
{
("czerwony", "#e74c3c"), ("niebieski", "#3498db"),
("zielony", "#2ecc71"), ("żółty", "#f1c40f")
};
private string nazwaSzukanegoKoloru = "";
private int wynik = ;
private int liczbaPytan = ;
protected override void OnInitialized()
{
LosujNastepnePytanie();
}
private void SprawdzOdpowiedz(string wybranaNazwa)
{
liczbaPytan++;
if (wybranaNazwa == nazwaSzukanegoKoloru)
{
wynik++;
}
LosujNastepnePytanie();
}
private void LosujNastepnePytanie()
{
int indeks = Random.Shared.Next(, dostepneKolory.Count);
nazwaSzukanegoKoloru = dostepneKolory[indeks].Nazwa;
}
}
Komentarz i wyjaśnienie kodu
bmi switch { ..., ..., _ => ... } to wyrażenie switch z WZORCAMI RELACYJNYMI (<, wprowadzone w nowszych wersjach C#) — czytelniejszy sposób zapisania serii warunków "w zakresie" niż długi łańcuch if/else if; sprawdzane są PO KOLEI, od góry, i wygrywa PIERWSZY pasujący warunek.
W KolorowyZgadywacz, @onclick="() => SprawdzOdpowiedz(nazwa)" używa WYRAŻENIA LAMBDA zamiast bezpośredniego odwołania do metody — to konieczne, bo trzeba przekazać KONKRETNY argument (nazwa danego przycisku z pętli @foreach), a nie tylko wywołać metodę bez parametrów. Bez lambdy (samo @onclick="SprawdzOdpowiedz") nie dałoby się przekazać, KTÓREGO konkretnie koloru dotyczy kliknięcie.
OnInitialized() wywołuje LosujNastepnePytanie() RAZ, na starcie gry, żeby od razu było jakieś pytanie do wyświetlenia — bez tego pierwsze wejście na stronę pokazałoby pusty tekst przed pierwszym kliknięciem.
Ćwiczenie samodzielne
Zbuduj OBA projekty z tej lekcji w jednym projekcie Blazor (dodaj linki do obu w MainLayout.razor z poprzedniej lekcji). Przetestuj kalkulator BMI z różnymi wartościami (w tym ujemnymi/zerowymi, sprawdzając komunikat błędu), i zagraj w kolorowy zgadywacz co najmniej 10 razy, sprawdzając poprawność liczenia wyniku.
Zadania do pracy własnej
Dodaj do kalkulatora BMI kolorowanie wyniku w zależności od kategorii (np. zielony tekst dla "waga prawidłowa", pomarańczowy dla "nadwaga", czerwony dla "otyłość") przez warunkowe przypisanie klasy CSS.
Rozbuduj kolorowy zgadywacz o TRUDNIEJSZY tryb z większą liczbą kolorów do wyboru (8 zamiast 4) oraz o limit czasu na odpowiedź (np. 5 sekund, licznik odliczający w tle przez
PeriodicTimerz lekcji 10) — jeśli czas minie bez kliknięcia, pytanie liczy się jako błędne i przechodzi dalej automatycznie.Zbuduj TRZECI, własny mini-projekt analogiczny do MediMax z oryginalnej wersji tego działu — prosty system rejestracji pacjentów (formularz z Data Annotations z lekcji 11, lista zarejestrowanych osób renderowana przez komponent potomny "KartaPacjenta" z lekcji 10, i nawigacja do szczegółów konkretnego pacjenta z lekcji 12) — łącząc w jednym projekcie wszystkie mechanizmy poznane w tym bloku lekcji o Blazor.
Typowe błędy
Dzielenie przez zero albo przez wartość ujemną we wzorze BMI bez wcześniejszego sprawdzenia — zawsze waliduj dane wejściowe PRZED wykonaniem obliczeń, dokładnie jak w lekcji o walidacji formularzy.
Zapomnienie o wywołaniu LosujNastepnePytanie() w OnInitialized() — bez tego gra startuje z pustym/domyślnym stanem, dopiero pierwsze kliknięcie (nieprawidłowo) ustawia pierwsze prawdziwe pytanie.
Używanie tego samego generatora Random tworzonego OD NOWA przy każdym losowaniu (new Random() w pętli/przy każdym wywołaniu) zamiast Random.Shared — może prowadzić do powtarzających się "losowych" wyników, jeśli kolejne wywołania następują bardzo szybko po sobie (ten sam ziarno losowości oparte na czasie systemowym).
Nawiązanie do egzaminu zawodowego
Ten cały blok lekcji o Blazor (materiał DODATKOWY, poza wymaganiami INF.04.7.2) miał pokazać, że tę samą wiedzę o interaktywnych, komponentowych aplikacjach webowych, którą zdobywa się ucząc się Angulara czy React (wymaganych na egzaminie), można też zdobyć w pełni w ekosystemie C#/.NET/Visual Studio. Do właściwego przygotowania do egzaminu INF.04.7 wróć do lekcji 1-18 tego działu (Razor Pages, MVC, sesje, logowanie, EF Core, Web API) i/lub przejdź do nauki Angulara — jeśli zdecydujesz się rozszerzyć swoje umiejętności o wymagany na egzaminie framework frontendowy.