Aplikacje mobilne · Architektura

Oddzielamy logikę od interfejsu: droga do MVVM w MAUI

Dotąd cała logika siedziała w code-behind strony, wymieszana z obsługą przycisków. To działa w małych przykładach, ale w większym projekcie robi się bałagan. Wydzielisz logikę do osobnej klasy i zobaczysz, jak ta zasada rozwija się w MVVM, standardowy wzorzec .NET MAUI.

  • około 50 minut
  • .NET MAUI · klasa logiki · MVVM
  • INF.04.6.2

Po tej lekcji: wydzielić logikę do klasy niezależnej od strony, zostawić w code-behind tylko „klej” oraz wyjaśnić role Model, View i ViewModel i działanie wiązania danych.

Ta sama funkcja bez zależności od przycisku

Jeżeli zasady programu są zamknięte w kliknięciu, trudno ich użyć na innym ekranie. Wyjmiemy formatowanie do klasy, a dopiero po działającej wersji pokażemy MVVM.

Najpierw uruchomisz mały ekran. Potem połączysz go z kodem i sprawdzisz konkretne wyniki. Dodatki wykonuj dopiero, gdy wersja podstawowa działa. Quiz i oceniane zadania pozostają w biurku po zalogowaniu.

Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji

1 · Zobacz przepływ

Trzy warstwy, trzy zadania

Wpisz dane (celowo z zapasowymi spacjami) i kliknij „Wyślij”. Zobacz, która warstwa kiedy pracuje. Spróbuj zostawić jedno pole puste.

Najpierw przewidź, potem sprawdź: W modelu warstw prześledź dane: pola → metoda logiki → wynik → etykieta. Wskaż, która warstwa powinna znać Entry, a która powinna przyjąć zwykły string.

Ta interakcja wymaga włączonego JavaScriptu.

Widok pokazuje, code-behind przekazuje, logika decyduje. Każdą z trzech części zmienisz osobno. Model pomaga zobaczyć mechanizm; właściwą aplikację uruchomisz z plików poniżej.

1. Przygotuj miejsce pracy

Co dodajesz, a co podmieniasz?

Stronę dodaj przez Projekt → Dodaj → Nowy element → .NET MAUI ContentPage (XAML). Powstaje para .xaml i .xaml.cs. Dla ContentView wybierz szablon ContentView; dla modeli i serwisów zwykłą klasę. Jeśli plik już istnieje, podmień wskazaną zawartość — nie twórz drugiej klasy o tej samej nazwie.

PlikRola w ćwiczeniu
Logika/Formater.csLogika i obsługa danych
MainPage.xaml.csLogika i obsługa danych
MainPage.xamlWygląd strony lub kontrolki
FormularzViewModel.csLogika i obsługa danych
projekt.csproj (fragment)Fragment w istniejącym pliku
MvvmPage.xamlWygląd strony lub kontrolki

Zanim uruchomisz: trzy połączenia

x:Class="MojaAplikacja.NazwaStrony" w XAML musi zgadzać się z namespace MojaAplikacja i nazwą klasy C#. InitializeComponent() tworzy kontrolki opisane w XAML.

x:Name="PoleImie" daje kodowi C# dostęp przez PoleImie.Text. Z kolei Clicked="Zapisz_Clicked" wskazuje metodę o tej nazwie w pliku .xaml.cs. Sam napis przycisku w Text niczego nie podpina.

Gdy dodajesz XAML z obsługą zdarzeń, od razu uzupełnij też odpowiadający C#. Uruchamiaj dopiero po skończeniu oznaczonej pary plików; brakujący handler nie jest błędem środowiska.

2. Uruchom sam wygląd — jeszcze bez logiki

W świeżym projekcie zastąp MainPage.xaml poniższym ekranem. W MainPage.xaml.cs zachowaj konstruktor z InitializeComponent(); usuń szablonowy licznik i OnCounterClicked, bo nie ma ich już w XAML. Na tym etapie App i AppShell pozostają z szablonu. To pełny, tymczasowy XAML — później świadomie go podmienisz, nie dokleisz do drugiego ContentPage.

MainPage.xaml — etap tymczasowyxaml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MojaAplikacja.MainPage">
    <VerticalStackLayout Padding="24" Spacing="14">
        <Label Text="Oddzielamy logikę" FontSize="24" />
        <Entry Placeholder="Imię" />
        <Entry Placeholder="Nazwisko" />
    </VerticalStackLayout>
</ContentPage>
  1. Pierwsza próba Uruchom. Zobaczysz kontrolki, ale przyciski bez Clicked jeszcze nic nie robią. Sprawdź rozmiar i kolejność elementów. Zatrzymaj aplikację przed następnym etapem.

Zacznijmy od telefonu

Gdy wszystko jest w jednej metodzie

Formularz „Imię + Nazwisko → Wyślij → wynik” na telefonie. Gdy cała logika łączenia napisów leży w metodzie kliknięcia, trudno znaleźć, gdzie zmienić regułę, a gdzie wygląd, i da się ją sprawdzić tylko po uruchomieniu aplikacji.

Zasada: **logika (obliczenia, reguły, formatowanie) żyje w osobnej klasie, która nie wie nic o stronach, kontrolkach ani DisplayAlert**. Przyjmuje dane i zwraca wynik.

5 · Krok dalej

MVVM: ta sama idea w pełnym wzorcu

MVVM to standardowy wzorzec w .NET MAUI (i WPF). Nie jest wymagany w pełnej formie na egzaminie, ale zasada oddzielenia logiki od interfejsu jest oceniana jako dobra praktyka.

Modeldane i reguły

Czyste dane i reguły biznesowe, bez wiedzy o stronie. Tu odpowiada mu klasa Formater.

Na przykład klasa Produkt z nazwą i ceną

Viewwygląd

Plik XAML (ContentPage) bez logiki.

Na przykład MvvmPage.xaml

ViewModelpośrednik

Udostępnia dane w formie gotowej do wyświetlenia (wiązanie danych, ustawiane jako BindingContext) i komendy zamiast Clicked.

Na przykład FormularzViewModel

3. Wskaż stronę startową ćwiczenia

Po dodaniu docelowych stron podmień App.xaml.cs na poniższy. App.xaml zostaje z szablonu. Od tej chwili startuje MainPage, a nie wcześniejszy tymczasowy MainPage z AppShell. Uzupełnij jej pliki w kolejnych etapach, zanim ponownie uruchomisz projekt.

App.xaml.cscsharp
namespace MojaAplikacja;

public partial class App : Application
{
    public App() => InitializeComponent();
    protected override Window CreateWindow(IActivationState? activationState)
        => new Window(new NavigationPage(new MainPage()));
}
  1. CreateWindow Tworzy okno aplikacji. NavigationPage jest kontenerem zwykłych stron i daje przycisk powrotu; nie łączymy go z Shell.
  2. new MainPage() Tworzy instancję docelowej strony. Aby testować drugi niezależny przykład, zatrzymaj program, zmień tylko ten typ i uruchom ponownie.

4. Połącz wygląd z działaniem

Teraz zbuduj docelowy przykład. Pełne pliki poniżej zastępują swoje wcześniejsze wersje. Przeczytaj opis nad kodem i objaśnienia pod nim; potem sprawdź przepływ danych, nie tylko brak czerwonych podkreśleń.

Gdy przykład ma kilka plików zależnych od siebie, najpierw dodaj wszystkie wskazane typy, a potem uruchom całość. Ustaw breakpoint w obsłudze głównego przycisku i prześledź jedną próbę.

Budowa krok po kroku

Formater.cs

Dodaj zwykłą klasę Formater w folderze Logika. Nie dziedziczy po ContentPage i nie zna żadnego Entry. Przyjmuje napisy, zwraca napis.

Klasa nie używa ContentPage, Entry ani DisplayAlert. Można ją przetestować osobno i użyć w innej aplikacji.

Logika/Formater.cscsharp
namespace MojaAplikacja.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}";
    }
}
  1. string? imie Znak ? oznacza, że wartość może być null. Metoda to uwzględnia.
  2. string.IsNullOrWhiteSpace Wykrywa null, pusty napis i same spacje.
  3. Trzy if i return Trzy przypadki: oba pola puste, brak imienia, brak nazwiska. Pozostałe dane składamy w jeden napis.

Budowa krok po kroku

MainPage.xaml

Na początek użyj MainPage z Clicked. Imię i nazwisko mają własne nazwy, a wynik osobną etykietę.

Widok to opis wyglądu: dwa pola, przycisk i etykieta. Bez reguł działania.

MainPage.xamlxaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MojaAplikacja.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>
  1. Przeczytaj powiązanie x:Name identyfikuje kontrolkę, a Binding odczytuje właściwość obiektu ustawionego jako BindingContext. To dwa różne mechanizmy.

Budowa krok po kroku

MainPage.xaml.cs

Handler jest łącznikiem: pobiera dane, wywołuje Formater i pokazuje wynik. Zasady dotyczące brakującego imienia są w Formater, nie w handlerze.

Metoda kliknięcia robi trzy rzeczy: odczytuje pola, woła logikę, pokazuje wynik. Obiekt logiki tworzysz raz, jako pole klasy.

MainPage.xaml.cscsharp
using MojaAplikacja.Logika;

namespace MojaAplikacja;

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;
    }
}
  1. private readonly Formater _formater = new(); Jeden obiekt na stronę zamiast new Formater() przy każdym kliknięciu.
  2. _formater.PolaczImieNazwisko(imie, nazwisko) Cała robota dzieje się w klasie logiki.
  3. WynikLabel.Text = wynik Jedyne, co strona robi z wynikiem.

Zatrzymaj się przed kolejnym wariantem

Nie zastępuj jeszcze działającej wersji. Wykonaj tę próbę i dopiero potem przejdź do dodatku.

Dopiero teraz: drugi wariant lub rozszerzenie

To następny etap tej lekcji, nie warunek pierwszego uruchomienia. Porównaj jego rolę z działającą bazą. Jeśli brakuje czasu, wróć do niego po sprawdzeniu podstawowej wersji.

Dodatek do działającej wersji

Zależność w pliku projektu

To fragment do istniejącego projekt.csproj (fragment), nie cały plik. Zachowaj pozostałe ustawienia szablonu i dodaj podaną sekcję w odpowiednim miejscu.

Pakiet instalujesz przez Menedżera pakietów NuGet (wyszukaj CommunityToolkit.Mvvm), co dopisuje takie wpisy do .csproj.

projekt.csproj (fragment)xml
<ItemGroup>
  <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
</ItemGroup>

Dodatek do działającej wersji

FormularzViewModel.cs: to samo, bez code-behind

Dodatek MVVM po działającej wersji podstawowej. partial umożliwia generatorowi dodanie Imie/Nazwisko/Wynik i WyslijCommand z pól oraz metody oznaczonych atrybutami.

W MAUI pełne MVVM zwykle korzysta z biblioteki CommunityToolkit.Mvvm (pakiet NuGet od Microsoftu), która sama generuje powiadamianie o zmianach i komendy. Ręczne pisanie tego od zera jest rozwlekłe.

FormularzViewModel.cscsharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MojaAplikacja.Logika;

namespace MojaAplikacja;

// ObservableObject i atrybuty generują powiadamianie o zmianie (INotifyPropertyChanged) i komendy
public partial class FormularzViewModel : ObservableObject
{
    private readonly Formater _formater = new();

    [ObservableProperty]
    private string imie = "";

    [ObservableProperty]
    private string nazwisko = "";

    [ObservableProperty]
    private string wynik = "Tu pojawi się wynik";

    [RelayCommand]
    private void Wyslij()
    {
        Wynik = _formater.PolaczImieNazwisko(Imie, Nazwisko);
    }
}
  1. ObservableObject Klasa bazowa z gotowym INotifyPropertyChanged.
  2. [ObservableProperty] private string imie Generator tworzy publiczną właściwość Imie z powiadamianiem o zmianie.
  3. [RelayCommand] private void Wyslij() Generator tworzy komendę WyslijCommand, którą wiążesz z przyciskiem.
  4. partial class Wymagane, bo generator dopisuje drugą część klasy.

Dodatek do działającej wersji

MvvmPage.xaml: pola powiązane z właściwościami

Dodaj osobną MvvmPage z konstruktorem szablonu. BindingContext tworzy FormularzViewModel, a Command zastępuje handler Clicked.

Strona nie ma ani jednej metody obsługi zdarzeń. Pola są wiązane ({Binding}) z właściwościami ViewModelu, a przycisk z komendą.

MvvmPage.xamlxaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MojaAplikacja"
             x:Class="MojaAplikacja.MvvmPage"
             x:DataType="local:FormularzViewModel"
             Title="Formularz MVVM">

    <ContentPage.BindingContext>
        <local:FormularzViewModel />
    </ContentPage.BindingContext>

    <VerticalStackLayout Padding="20" Spacing="12">
        <Entry Text="{Binding Imie}" Placeholder="Imię" />
        <Entry Text="{Binding Nazwisko}" Placeholder="Nazwisko" />
        <Button Text="Wyślij" Command="{Binding WyslijCommand}" />
        <Label Text="{Binding Wynik}" FontAttributes="Bold" />
    </VerticalStackLayout>
</ContentPage>
  1. <ContentPage.BindingContext> Mówi stronie, który obiekt jest jej ViewModelem.
  2. x:DataType="local:FormularzViewModel" Włącza skompilowane wiązania: błędy w nazwach właściwości wykryje kompilator.
  3. {Binding Imie}, {Binding WyslijCommand} Dwukierunkowe wiązanie pola i komenda przycisku.

Plik .xaml.cs też musi istnieć

Szablon strony lub ContentView generuje poniższe klasy. Nie dodawaj ich drugi raz, jeśli Visual Studio już je utworzyło. W każdej parze musi pozostać InitializeComponent().

MvvmPage — minimalny kod za widokiem

MvvmPage.xaml.cscsharp
namespace MojaAplikacja;

public partial class MvvmPage : ContentPage
{
    public MvvmPage() => InitializeComponent();
}
  1. partial Druga część klasy powstaje z XAML. Nie twórz ręcznie pól kontrolek ani drugiej metody InitializeComponent.

5. Odbiór: czy naprawdę działa?

Wykonaj próby w swojej aplikacji, nie tylko w modelu na stronie. Zaznaczaj dopiero po sprawdzeniu wyniku. Funkcje urządzenia wymagają testu na właściwej platformie.

9 · Czego unikać

Trzy błędy z oddzielaniem logiki

Logika zna `DisplayAlert` lub kontrolki
Tak nie
public class Formater
{
    public void Pokaz(Entry pole)
    {
        Application.Current.MainPage.DisplayAlertAsync("Witaj", pole.Text, "OK");
    }
}

Klasy nie da się przetestować ani użyć poza tą aplikacją.

Tak
public string Zloz(string imie) => "Witaj, " + imie;

Logika przyjmuje dane i zwraca wynik. Pokazanie go to zadanie strony.

Gruby code-behind mimo klasy logiki
Tak nie
if (PoleImie.Text.Length > 0 && PoleNazwisko.Text.Length > 0)
    WynikLabel.Text = _formater.Zloz(PoleImie.Text);
else
    WynikLabel.Text = "Błąd";

Reguła „oba pola muszą być wypełnione” nadal siedzi na stronie.

Tak
WynikLabel.Text = _formater.PolaczImieNazwisko(PoleImie.Text, PoleNazwisko.Text);

Warunki i reguły należą do klasy logiki.

Nowy obiekt logiki przy każdym kliknięciu
Tak nie
var formater = new Formater();   // w metodzie obsługi

Niepotrzebne tworzenie obiektu. Stan logiki zniknąłby po każdym kliknięciu.

Tak
private readonly Formater _formater = new();

Obiekt logiki tworzysz raz, jako pole strony.

Ćwiczenie z pomocą · samodzielna praca

6. Mała zmiana, którą zrobisz samodzielnie

Dodaj do Formater metodę zwracającą inicjały.

Oczekiwany rezultat
Dodaj do Formater metodę zwracającą inicjały.
Dotychczasowe działanie ma pozostać poprawne.
Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź

Metoda przyjmuje dwa napisy, nie Entry.

Krok 2 — pokaż podpowiedź

Sprawdź puste wartości przed pobraniem pierwszej litery.

Krok 3 — pokaż podpowiedź

Wywołaj ją z formularza i sprawdź przypadek z brakującym nazwiskiem.

Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.

10 · Sprawdź się

Która warstwa za to odpowiada?

Cztery zadania. Wskaż warstwę.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Logika żyje w osobnej klasie, która nie zna stron, kontrolek ani DisplayAlert.
  2. Code-behind to cienki „klej”: odczytaj dane, wywołaj logikę, pokaż wynik. Obiekt logiki jest polem strony.
  3. MVVM = Model (dane i reguły) + View (XAML) + ViewModel (dane do wyświetlenia i komendy).
  4. W MAUI MVVM ułatwia pakiet CommunityToolkit.Mvvm (ObservableObject, [ObservableProperty], [RelayCommand]).
  5. Widok łączy się z ViewModelem przez BindingContext i {Binding}.

Dalej na swoim koncie

Rozgrzewki, quiz i zadania są w biurku ucznia.

Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.

Zaloguj się →
Źródła i dalsza nauka