Lekcja 14. Modele danych i walidacja w .NET MAUI
Trudny / egzaminacyjnyPo co się tego uczymy?
Podczas tworzenia aplikacji bardzo często pracujemy z danymi, które użytkownik wpisuje w formularzach (imię, wzrost, waga, e-mail, wiek...). Trzymanie tych danych w kilkunastu osobnych zmiennych szybko robi się nieczytelne, a co gorsza — nic nie broni aplikacji przed przyjęciem bezsensownych wartości (pusty formularz, wzrost 0 cm, litery zamiast liczby wieku). Ta lekcja pokazuje dwa fundamentalne, wymagane na egzaminie INF.04 narzędzia porządkujące ten problem: modele danych (klasy opisujące dane jako spójną całość) oraz walidację (systematyczne sprawdzanie poprawności danych, zanim aplikacja ich użyje).
Teoria
Model danych to zwykła klasa C#, która opisuje dane — zawiera TYLKO pola/właściwości (properties), bez logiki UI. Przykład modelu z formularza (imię, wzrost, waga, płeć):
public class DaneFormularza
{
public string Imie { get; set; } = "";
public double? WzrostCm { get; set; }
public double? WagaKg { get; set; }
public string Plec { get; set; } = "Nie podano";
}
Zwróć uwagę na typy z znakiem zapytania: double? i int? to typy nullowalne (nullable) — oznaczają "liczba albo brak wartości (null)". Używamy ich zamiast zwykłego double/int właśnie dlatego, że zanim dane zostaną zwalidowane, pole MOŻE być puste — zwykły double zawsze ma jakąś wartość domyślną (0), co byłoby mylące (0 cm wzrostu to nie "brak danych", tylko nieprawdziwa wartość).
Korzyści z modeli: porządek w kodzie (dane są w jednym miejscu, nie rozsypane po formularzu jako osobne zmienne), łatwe przekazywanie (można przesłać cały model między stronami jednym argumentem konstruktora, jak w lekcji o nawigacji), obiektowość (uczymy się myślenia w kategoriach klas i obiektów — wymagane na egzaminie INF.04.4 i INF.04.6), możliwość rozbudowy (w przyszłości można dodać do modelu metody, np. ObliczBmi(), bez zmiany reszty aplikacji).
Walidacja danych to proces sprawdzania poprawności danych wpisanych przez użytkownika, ZANIM aplikacja je wykorzysta (wyświetli, zapisze, wyśle). Dobra praktyka (zgodna z zasadą oddzielenia logiki od UI z poprzedniej lekcji): walidację umieszczamy w osobnej klasie, np. WalidatorFormularza, ze statyczną metodą zwracającą jednocześnie DWIE informacje: czy dane są poprawne (bool) oraz listę konkretnych błędów (List<string>) do pokazania użytkownikowi. W C# do zwrócenia dwóch wartości na raz z jednej metody służy typ krotki (tuple): (bool ok, List<string> bledy) — metoda zwraca jedną wartość, ale zapakowaną w dwa nazwane pola dostępne przez kropkę (wynik.ok, wynik.bledy).
Typowe reguły walidacji spotykane w formularzach mobilnych:
| Reguła | Jak sprawdzić w C# |
|---|---|
| Pole nie może być puste | string.IsNullOrWhiteSpace(tekst) — zwraca true dla null, pustego stringa i samych spacji |
| Tekst musi być liczbą | double.TryParse(tekst, out var wynik) — zwraca false, jeśli tekst nie jest poprawną liczbą (bez rzucania wyjątku) |
| Liczba w dopuszczalnym zakresie | zwykłe porównania: if (wzrost <= 0 || wzrost > 300) |
| E-mail wygląda poprawnie | prosta wersja: sprawdzenie znaku @ i kropki po nim; pełna wersja: System.Text.RegularExpressions.Regex |
| Wybór z listy jest ustawiony | sprawdzenie, czy Picker.SelectedIndex != -1 albo czy wybrana wartość nie jest pusta |
Przepływ typowej aplikacji z modelem i walidacją: użytkownik wypełnia formularz (Entry na Imię/Wzrost/Wagę, Picker na Płeć) → kliknięcie "Wyślij" wywołuje metodę walidującą → jeśli są błędy, pokazujemy DisplayAlert z listą błędów i NIE przechodzimy dalej → jeśli wszystko poprawne, tworzymy obiekt DaneFormularza, wypełniamy go danymi z formularza i przekazujemy na kolejną stronę (np. przez konstruktor, jak w lekcji o nawigacji), gdzie dane z modelu są wyświetlane w etykietach.
Alternatywne podejście — model współdzielony przez wiele stron. Gdy aplikacja ma kilka formularzy zbierających różne fragmenty tych samych danych (np. Formularz 1: imię/nazwisko/e-mail, Formularz 2: wiek/miasto, a na końcu wspólne Podsumowanie), wygodnie jest trzymać JEDEN obiekt modelu dostępny dla wszystkich stron — np. jako statyczną właściwość klasy App: public static Dane Dane { get; } = new();. Każda strona w konstruktorze ustawia BindingContext = App.Dane;, a w XAML pola Entry łączy się z właściwościami modelu przez data binding: Text="{Binding Imie, Mode=TwoWay}". Mode=TwoWay oznacza, że zmiana w polu tekstowym automatycznie aktualizuje właściwość modelu, a zmiana modelu (z kodu) automatycznie aktualizuje pole na ekranie — bez ręcznego przepisywania PoleImie.Text = ... w każdą stronę. To pierwsze zetknięcie z prawdziwym data bindingiem w tym kursie — pełne omówienie INotifyPropertyChanged i wiązań w obie strony wykracza poza podstawę programową, ale sama składnia {Binding Właściwość, Mode=TwoWay} jest bardzo przydatna i warto ją znać.
Schemat
Formularz (Entry x3, Picker)
│
│ kliknięcie "Wyślij"
▼
WalidatorFormularza.Sprawdz(imie, wzrost, waga)
│
├── błędy? ──► DisplayAlert z listą błędów (stop, zostajemy na stronie)
│
└── OK ──► new DaneFormularza { Imie=..., WzrostCm=..., WagaKg=... }
│
▼
Przekazanie modelu do Podsumowanie (konstruktor)
│
▼
Wyświetlenie pól modelu w Label-ach
Przykład z życia
Aplikacja rejestracyjna na wydarzenie sportowe zbiera od uczestnika imię, wzrost, wagę i kategorię wiekową — dane trafiają najpierw do modelu DaneFormularza, a WalidatorFormularza pilnuje, żeby nikt nie zapisał się z pustym imieniem albo wagą "abc". Dopiero gdy wszystkie pola przejdą walidację, aplikacja tworzy zgłoszenie i pokazuje ekran potwierdzenia z podsumowaniem wpisanych danych — dokładnie ten sam wzorzec, co w wielu prawdziwych formularzach rejestracyjnych czy zamówieniowych.
MainPage.xaml
<!-- MainPage.xaml (formularz z walidacją) -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MojaApp.MainPage"
Title="Formularz">
<ScrollView>
<VerticalStackLayout Padding="16" Spacing="12">
<Label Text="Imię" />
<Entry x:Name="PoleImie" Placeholder="Wpisz imię" />
<Label Text="Wzrost (cm)" />
<Entry x:Name="PoleWzrost" Keyboard="Numeric" Placeholder="np. 175" />
<Label Text="Waga (kg)" />
<Entry x:Name="PoleWaga" Keyboard="Numeric" Placeholder="np. 70" />
<Label Text="Płeć" />
<Picker x:Name="PolePlec">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Kobieta</x:String>
<x:String>Mężczyzna</x:String>
<x:String>Nie podano</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>
<Button Text="Wyślij" Clicked="PrzyciskWyslij_Clicked" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
Models/DaneFormularza.cs + Logika/WalidatorFormularza.cs + MainPage.xaml.cs
// Models/DaneFormularza.cs - model danych
namespace MojaApp.Models;
public class DaneFormularza
{
public string Imie { get; set; } = "";
public double? WzrostCm { get; set; }
public double? WagaKg { get; set; }
public string Plec { get; set; } = "Nie podano";
}
// ---------------------------------------------------------------------
// Logika/WalidatorFormularza.cs - walidacja, osobna klasa (bez wiedzy o UI)
namespace MojaApp.Logika;
public static class WalidatorFormularza
{
public static (bool ok, List<string> bledy) Sprawdz(
string? imieTxt, string? wzrostTxt, string? wagaTxt)
{
var bledy = new List<string>();
if (string.IsNullOrWhiteSpace(imieTxt))
bledy.Add("Podaj imię.");
var wzrost = SprobujLiczbe(wzrostTxt);
if (wzrost is null)
bledy.Add("Wzrost musi być liczbą.");
else if (wzrost <= || wzrost > 300)
bledy.Add("Wzrost w zakresie 1-300 cm.");
var waga = SprobujLiczbe(wagaTxt);
if (waga is null)
bledy.Add("Waga musi być liczbą.");
else if (waga <= || waga > 400)
bledy.Add("Waga w zakresie 1-400 kg.");
return (bledy.Count == , bledy);
}
private static double? SprobujLiczbe(string? tekst)
{
if (double.TryParse(tekst, out var wynik))
return wynik;
return null;
}
}
// ---------------------------------------------------------------------
// MainPage.xaml.cs - wywołanie walidacji i utworzenie modelu
using MojaApp.Logika;
using MojaApp.Models;
namespace MojaApp;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private async void PrzyciskWyslij_Clicked(object sender, EventArgs e)
{
var (ok, bledy) = WalidatorFormularza.Sprawdz(
PoleImie.Text, PoleWzrost.Text, PoleWaga.Text);
if (!ok)
{
await DisplayAlert("Popraw dane", string.Join("n", bledy), "OK");
return;
}
var dane = new DaneFormularza
{
Imie = PoleImie.Text,
WzrostCm = double.Parse(PoleWzrost.Text),
WagaKg = double.Parse(PoleWaga.Text),
Plec = PolePlec.SelectedItem?.ToString() ?? "Nie podano"
};
await Navigation.PushAsync(new Podsumowanie(dane));
}
}
Komentarz i wyjaśnienie kodu
DaneFormularza to CZYSTY model — same właściwości, żadnej logiki. WalidatorFormularza jest klasą static (nie musimy tworzyć jej instancji, bo nie przechowuje żadnego stanu — tylko udostępnia metodę). Metoda Sprawdz zwraca krotkę (bool ok, List<string> bledy) — w miejscu wywołania rozpakowujemy ją od razu do dwóch zmiennych: var (ok, bledy) = WalidatorFormularza.Sprawdz(...).
Pomocnicza metoda SprobujLiczbe korzysta z double.TryParse zamiast double.Parse właśnie po to, żeby NIE rzucić wyjątku, gdy użytkownik wpisze coś, co nie jest liczbą — zamiast tego zwraca null, a walidator dopisuje odpowiedni komunikat do listy błędów. Dopiero PO przejściu walidacji, w MainPage.xaml.cs, używamy bezpiecznego już double.Parse(...) (wiemy, że tekst na pewno jest poprawną liczbą, bo inaczej metoda zwróciłaby błąd wcześniej i wykonanie zakończyłoby się na return;).
string.Join("n", bledy) łączy wszystkie komunikaty błędów w jeden tekst z podziałem na linie, żeby pokazać je razem w jednym DisplayAlert.
Ćwiczenie samodzielne
Rozbuduj powyższy przykład o pole "E-mail" (Entry z Keyboard="Email") — dodaj do modelu właściwość Email, a do walidatora regułę sprawdzającą, czy tekst zawiera znak "@" oraz kropkę WYSTĘPUJĄCĄ PO tym znaku.
Zadania do pracy własnej
Stwórz model
DaneLogowania(Login, Haslo) oraz walidator sprawdzający, że oba pola są niepuste, a hasło ma co najmniej 6 znaków.Zbuduj formularz rejestracji zawodnika: model z Imię/Nazwisko/Wiek/Kategoria (Picker), walidator sprawdzający niepustość pól tekstowych, wiek w zakresie 6-99 lat, oraz wybraną kategorię (nie może zostać domyślna wartość "wybierz").
Zaimplementuj wariant z dwoma formularzami i wspólnym modelem (wzorowany na przykładzie z teorii): Formularz 1 (imię, nazwisko, e-mail) i Formularz 2 (wiek, miasto) zapisują dane do JEDNEGO współdzielonego obiektu modelu (
public static Dane Dane { get; } = new();w klasieApp), a strona Podsumowanie wyświetla wszystkie pięć wartości naraz, korzystając zBindingContext = App.Dane;i wiązań{Binding Właściwość}zamiast ręcznego odczytywania pólEntry.
Typowe błędy
Używanie zwykłego double/int zamiast double?/int? w modelu — bez znaku zapytania pole zawsze ma jakąś wartość (domyślnie 0), więc nie da się odróżnić "użytkownik wpisał 0" od "pole jeszcze nie zostało wypełnione".
Wywoływanie double.Parse PRZED walidacją — jeśli tekst nie jest liczbą, Parse rzuci wyjątek i aplikacja się wywali; zawsze najpierw TryParse (albo cała walidacja), dopiero potem bezpieczny Parse.
Umieszczanie walidacji bezpośrednio w code-behind strony zamiast w osobnej klasie — to ten sam błąd, co w poprzedniej lekcji o MVVM: logika walidacji miesza się wtedy z obsługą UI i trudniej ją przetestować czy ponownie użyć w innym formularzu.
Pokazywanie tylko jednego błędu na raz zamiast pełnej listy — użytkownik poprawia jedno pole, klika ponownie i dostaje kolejny, inny błąd, zamiast zobaczyć od razu WSZYSTKIE problemy z formularza.
Nawiązanie do egzaminu zawodowego
To bezpośrednie zastosowanie wymagań INF.04.6.2 (elementy UI, formularze) połączone z INF.04.4.6 (definiowanie klas — pola, właściwości) i INF.04.3.6 (projektowanie aplikacji — projektowanie zestawów danych dla problemu). Modele i walidacja to fundament, na którym opierają się kolejne lekcje tego działu — wyświetlanie list danych, obsługa plików i bazy danych SQLite zawsze zaczynają się od dobrze zaprojektowanego modelu.