Dataniedziela, 9 sierpnia 2026 Czas08:11:00
← Aplikacje mobilne (.NET MAUI)

Lekcja 13. Oddzielenie logiki od UI — wprowadzenie do wzorca MVVM

Trudny / egzaminacyjny

Po 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 Produkt z 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 BindingContext strony) i komendy (ICommand) reagujące na akcje użytkownika, zamiast zwykłych zdarzeń Clicked w 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

  1. Stwórz klasę Walidator z metodą CzyEmailWygladaPoprawnie(string email) zwracającą bool (sprawdzającą np. czy zawiera znak "@" i kropkę po nim) i użyj jej w formularzu z polem Entry na email.

  2. Wydziel logikę kalkulatora BMI (wzrost, waga → wynik + kategoria: niedowaga/norma/nadwaga) do osobnej klasy KalkulatorBmi z metodami ObliczBmi(double waga, double wzrostM) i OkreslKategorie(double bmi), a code-behind strony niech tylko odczytuje dane z pól Entry, woła te metody i wyświetla wynik w Label.

  3. Zaprojektuj aplikację "Konwerter walut" z klasą logiki KonwerterWalut obsł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 (z Picker do wyboru walut i Entry na 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.