Lekcja 13. Oddzielenie logiki od UI — wprowadzenie do wzorca MVVM
Trudny / egzaminacyjnyPo co się tego uczymy?
Do tej pory cała logika (sprawdzanie danych, obliczenia, formatowanie tekstu) siedziała bezpośrednio w code-behind strony, wymieszana z obsługą przycisków. To działa dla małych przykładów, ale w większym projekcie mobilnym szybko robi się bałagan — trudno znaleźć, gdzie coś zmienić, i nie da się przetestować logiki bez uruchamiania całej aplikacji na emulatorze. Ta lekcja pokazuje, jak to uporządkować, wprowadzając zasadę leżącą u podstaw wzorca architektonicznego znanego jako MVVM.
Teoria
Zanim przejdziemy do pełnego MVVM, zacznijmy od prostszej, fundamentalnej zasady: logika biznesowa (obliczenia, reguły, formatowanie) powinna żyć w osobnej klasie, niezależnej od strony. Klasa logiki nie powinna nic wiedzieć o przyciskach, stronach czy DisplayAlert — przyjmuje dane, zwraca wynik, i tyle. Code-behind strony (MainPage.xaml.cs) staje się wtedy cienką warstwą "kleju": odczytuje dane z kontrolek (Entry, Picker...), przekazuje je do klasy logiki, i wypisuje wynik z powrotem na ekranie (np. do Label).
Korzyści z takiego podziału: porządek (UI nie miesza się z regułami działania), czytelność (łatwiej znaleźć, gdzie zmienić wygląd strony, a gdzie regułę biznesową), testowalność (logikę można sprawdzić w osobnym teście jednostkowym, bez uruchamiania emulatora — co w MAUI jest szczególnie cenne, bo start emulatora trwa długo), skalowalność (projekt rośnie, a Ty dodajesz kolejne metody do klasy logiki, nie zaśmiecając kodu strony), wielokrotne użycie (ta sama klasa logiki może obsłużyć wiele stron albo nawet zostać przeniesiona do innego projektu — np. z aplikacji mobilnej do konsolowej).
MVVM (Model-View-ViewModel) to rozwinięcie tej samej idei do pełnego wzorca architektonicznego, standardowego w świecie .NET MAUI (i WPF):
- Model — czyste dane i reguły biznesowe (np. klasa
Produktz polami Nazwa/Cena); - View — plik XAML (
ContentPage), czyli sam wygląd strony, bez żadnej logiki; - ViewModel — pośrednik między Modelem a Widokiem: udostępnia dane z Modelu w formie gotowej do wyświetlenia (przez data binding, ustawiany zwykle jako
BindingContextstrony) i komendy (ICommand) reagujące na akcje użytkownika, zamiast zwykłych zdarzeńClickedw code-behind.
W MAUI pełne MVVM zwykle korzysta z gotowej biblioteki CommunityToolkit.Mvvm (pakiet NuGet od Microsoftu), która automatycznie generuje powiadamianie o zmianie danych (INotifyPropertyChanged) i komendy — ręczne pisanie tego od zera jest dość rozwlekłe. Pełne MVVM z bindingiem dwukierunkowym i komendami to jednak temat na cały, osobny kurs — w tej lekcji poznajemy PODSTAWOWĄ zasadę oddzielenia logiki od interfejsu, która jest fundamentem MVVM i którą już teraz możesz stosować w swoich aplikacjach mobilnych, bez żadnych dodatkowych bibliotek.
Mechanika łączenia widoku z logiką w tym prostszym podejściu (bez pełnego MVVM): x:Name w XAML nadaje kontrolce nazwę (np. PoleImie) widoczną jako pole w code-behind. Clicked="PrzyciskWyslij_Click" podłącza zdarzenie przycisku do metody w MainPage.xaml.cs. InitializeComponent() w konstruktorze strony "wczytuje" XAML i tworzy kontrolki. using MojaApp.Logika; pozwala użyć klasy z osobnego folderu/pliku. Tworzymy obiekt logiki (new Formater()) i wołamy jego metody z poziomu obsługi zdarzenia.
Schemat
Bez rozdzielenia (bałagan): Z rozdzieleniem logiki:
MainPage.xaml.cs MainPage.xaml.cs (code-behind)
┌─────────────────────┐ ┌─────────────────────┐
│ obsluga przyciskow │ │ obsluga przyciskow │
│ + walidacja │ │ (tylko odczyt/zapis │
│ + obliczenia │ ----> │ danych z kontrolek) │
│ + logika biznesowa │ └──────────┬───────────┘
│ WSZYSTKO w 1 pliku! │ │ woła metody
└─────────────────────┘ ▼
Logika/Formater.cs
(czysta logika C#,
NIE zna kontrolek MAUI,
mozna testowac osobno)
Przykład z życia
Wyobraź sobie formularz "Imię + Nazwisko → Wyślij → wynik" na ekranie telefonu. Bez rozdzielenia cała logika łączenia napisów siedziałaby w metodzie obsługi kliknięcia przycisku. Z rozdzieleniem — tworzysz osobną klasę Formater z metodą PolaczImieNazwisko(string imie, string nazwisko), a code-behind strony tylko odczytuje tekst z pól Entry, woła tę metodę, i wypisuje wynik na etykiecie Label. Gdyby ktoś chciał potem zmienić regułę łączenia (np. dodać automatyczne wielkie litery albo inny format), zmienia się TYLKO klasę Formater, nie dotykając w ogóle kodu strony ani XAML.
MainPage.xaml
<!-- MainPage.xaml (UI - widok) -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MojaApp.MainPage"
Title="Formularz">
<VerticalStackLayout Padding="20" Spacing="12">
<Entry x:Name="PoleImie" Placeholder="Imię" />
<Entry x:Name="PoleNazwisko" Placeholder="Nazwisko" />
<Button Text="Wyślij" Clicked="PrzyciskWyslij_Click" />
<Label x:Name="WynikLabel" Text="Tu pojawi się wynik" FontAttributes="Bold" />
</VerticalStackLayout>
</ContentPage>
MainPage.xaml.cs + Logika/Formater.cs
// MainPage.xaml.cs - "klej" łączący UI z logiką
using MojaApp.Logika;
namespace MojaApp;
public partial class MainPage : ContentPage
{
private readonly Formater _formater = new();
public MainPage()
{
InitializeComponent();
}
private void PrzyciskWyslij_Click(object sender, EventArgs e)
{
// Code-behind TYLKO odczytuje dane z UI i woła logikę
string imie = PoleImie.Text;
string nazwisko = PoleNazwisko.Text;
string wynik = _formater.PolaczImieNazwisko(imie, nazwisko);
WynikLabel.Text = wynik;
}
}
// ---------------------------------------------------------------------
// Logika/Formater.cs - czysta logika, NIE zna kontrolek MAUI
namespace MojaApp.Logika;
public class Formater
{
public string PolaczImieNazwisko(string imie, string nazwisko)
{
if (string.IsNullOrWhiteSpace(imie) && string.IsNullOrWhiteSpace(nazwisko))
return "Proszę podać imię i nazwisko.";
if (string.IsNullOrWhiteSpace(imie))
return $"Nazwisko: {nazwisko}";
if (string.IsNullOrWhiteSpace(nazwisko))
return $"Imię: {imie}";
return $"{imie} {nazwisko}";
}
}
Komentarz i wyjaśnienie kodu
Klasa Formater w folderze Logika nie ma ŻADNEGO odwołania do ContentPage, Entry czy czegokolwiek z MAUI — przyjmuje dwa stringi, zwraca jeden string. Dzięki temu można ją przetestować niezależnie (np. wywołując z prostego testu jednostkowego, bez uruchamiania emulatora), a w przyszłości użyć jej też w zupełnie innym miejscu (np. w aplikacji konsolowej albo webowej), bo nie jest przywiązana do interfejsu.
MainPage.xaml.cs ma pole _formater (instancję klasy logiki utworzoną RAZ, jako pole klasy) i w metodzie obsługi kliknięcia robi TYLKO trzy rzeczy: odczytuje dane z pól Entry, przekazuje je do metody logiki, wypisuje zwrócony wynik do Label. Żadnych obliczeń ani reguł bezpośrednio w tej metodzie — to jest właśnie ta "cienka warstwa kleju", o której mowa w teorii. Zwróć uwagę na strukturę folderów: Logika/Formater.cs jest osobnym plikiem w osobnym folderze projektu (PPM na projekt → Dodaj → Nowy folder → Logika, potem PPM na Logika → Dodaj → Klasa).
Ćwiczenie samodzielne
Weź dowolną aplikację MAUI, którą pisałeś we wcześniejszych lekcjach tego działu (np. formularz z kontrolek). Wydziel logikę obliczeniową do osobnej klasy w nowym folderze "Logika", tak żeby code-behind strony tylko odczytywał dane z pól i wołał metody tej klasy.
Zadania do pracy własnej
Stwórz klasę
Walidatorz metodąCzyEmailWygladaPoprawnie(string email)zwracającąbool(sprawdzającą np. czy zawiera znak "@" i kropkę po nim) i użyj jej w formularzu z polemEntryna email.Wydziel logikę kalkulatora BMI (wzrost, waga → wynik + kategoria: niedowaga/norma/nadwaga) do osobnej klasy
KalkulatorBmiz metodamiObliczBmi(double waga, double wzrostM)iOkreslKategorie(double bmi), a code-behind strony niech tylko odczytuje dane z pólEntry, woła te metody i wyświetla wynik wLabel.Zaprojektuj aplikację "Konwerter walut" z klasą logiki
KonwerterWalutobsługującą kilka kursów wymiany (PLN/EUR/USD, kursy na sztywno w słowniku), gdzie cała logika przeliczeń i walidacji kwoty (musi być liczbą dodatnią) jest w osobnej klasie, a strona MAUI (zPickerdo wyboru walut iEntryna kwotę) jest tylko cienką warstwą UI.
Typowe błędy
Dodawanie do klasy logiki odwołań do DisplayAlert lub kontrolek — to niweczy cały sens rozdzielenia; jeśli logika pokazuje okna dialogowe albo zna nazwy kontrolek, nie da się jej użyć nigdzie indziej ani łatwo przetestować.
Zbyt "gruby" code-behind mimo posiadania klasy logiki — jeśli w metodzie obsługi zdarzenia nadal jest dużo warunków i obliczeń OBOK wywołania metody logiki, oddzielenie jest tylko połowiczne.
Tworzenie osobnej instancji klasy logiki przy każdym kliknięciu — zwykle lepiej stworzyć ją raz (jako pole klasy strony, jak _formater w przykładzie) i używać wielokrotnie, zamiast new Formater() za każdym razem wewnątrz metody obsługi.
Nawiązanie do egzaminu zawodowego
To wprowadzenie do koncepcji leżącej u podstaw wzorca MVVM, standardowego w profesjonalnych aplikacjach .NET MAUI — choć podstawa programowa INF.04.6 nie wymaga pełnego MVVM z bindingiem i komendami, sama zasada oddzielenia logiki od interfejsu jest oceniana w zadaniach egzaminacyjnych jako dobra praktyka programistyczna i przydaje się już w następnej lekcji o modelach danych i walidacji.