Aplikacje desktopowe · Architektura

Oddzielamy logikę od interfejsu: droga do MVVM

Do tej pory cała logika siedziała w code-behind okna, wymieszana z obsługą przycisków. W małych przykładach to działa, ale w większym projekcie robi się bałagan. Nauczysz się wydzielać logikę do osobnej klasy i zobaczysz, jak ta zasada rozwija się w MVVM.

  • około 50 minut
  • WPF · klasa logiki · MVVM
  • INF.04.5.3

Po tej lekcji: wydzielić logikę do osobnej klasy niezależnej od okna, zostawić w code-behind tylko „klej” oraz wyjaśnić role Model, View i ViewModel.

Zacznijmy od bałaganu

Gdy wszystko jest w jednej metodzie

Wyobraź sobie formularz: imię, nazwisko, przycisk „Wyślij” i wynik. Jeśli cały kod łączenia napisów leży w metodzie kliknięcia, to później:

- trudno znaleźć, gdzie zmienić regułę, a gdzie wygląd, - logikę da się sprawdzić tylko po uruchomieniu całego okna, - zmiana reguły (np. wielkie litery) wymaga grzebania w kodzie okna.

Zasada, którą poznasz, brzmi: **logika (obliczenia, reguły) żyje w osobnej klasie, która nie wie nic o oknach, przyciskach ani MessageBox**. Przyjmuje dane i zwraca wynik. Koniec.

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 też zostawić jedno pole puste.

Ta interakcja wymaga włączonego JavaScriptu.

Widok pokazuje, code-behind przekazuje, logika decyduje. Dzięki temu każdą z trzech części można zmienić osobno.

2 · Czysta logika

Formater.cs, klasa bez żadnych kontrolek

Ta klasa leży w folderze Logika. Nie używa Window, TextBox ani MessageBox. Dzięki temu możesz ją przetestować osobno i użyć w aplikacji konsolowej lub webowej.

Logika/Formater.cscsharp
namespace MojaAppWpf.Logika;

public class Formater
{
    public string PolaczImieNazwisko(string? imie, string? nazwisko)
    {
        imie = imie?.Trim() ?? "";
        nazwisko = nazwisko?.Trim() ?? "";

        if (string.IsNullOrEmpty(imie) || string.IsNullOrEmpty(nazwisko))
        {
            return "Podaj imię i nazwisko!";
        }

        return $"Witaj, {imie} {nazwisko}!";
    }
}
  1. string? imie Znak ? oznacza, że wartość może być null. Metoda sobie z tym radzi.
  2. imie?.Trim() ?? "" Jeśli imie jest null, wynikiem jest pusty napis, w przeciwnym razie napis bez spacji na krańcach.
  3. string.IsNullOrEmpty(...) Reguła biznesowa: bez imienia lub nazwiska zwracamy komunikat zamiast powitania.
  4. return $"Witaj, {imie} {nazwisko}!" Zwraca wynik. Metoda niczego nie wypisuje na ekranie.

3 · Cienki klej

MainWindow.xaml.cs, który tylko łączy

Metoda kliknięcia robi trzy rzeczy: odczytuje dane z pól, woła logikę, wpisuje wynik w etykietę. Żadnych warunków ani obliczeń.

MainWindow.xaml.cscsharp
using System.Windows;
using MojaAppWpf.Logika;

namespace MojaAppWpf;

public partial class MainWindow : Window
{
    private readonly Formater _formater = new Formater();

    public MainWindow()
    {
        InitializeComponent();
    }

    private void PrzyciskWyslij_Click(object sender, RoutedEventArgs e)
    {
        // Code-behind tylko odczytuje dane z UI i woła logikę
        string wynik = _formater.PolaczImieNazwisko(PoleImie.Text, PoleNazwisko.Text);
        EtykietaWynik.Text = wynik;
    }
}
  1. private readonly Formater _formater = new Formater(); Obiekt logiki tworzysz raz, jako pole klasy, a nie przy każdym kliknięciu.
  2. _formater.PolaczImieNazwisko(...) Cała robota dzieje się w klasie logiki.
  3. EtykietaWynik.Text = wynik Jedyne, co code-behind robi z wynikiem, to pokazanie go.

4 · Sam wygląd

MainWindow.xaml

Widok to tylko opis wyglądu: pola, przycisk i etykieta. Nie zawiera reguł działania.

MainWindow.xamlxaml
<Window x:Class="MojaAppWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Formularz powitania" Height="260" Width="340">
    <StackPanel Margin="20">
        <TextBlock Text="Imię:" />
        <TextBox x:Name="PoleImie" Margin="0,2,0,10" />
        <TextBlock Text="Nazwisko:" />
        <TextBox x:Name="PoleNazwisko" Margin="0,2,0,10" />
        <Button x:Name="PrzyciskWyslij" Content="Wyślij" Click="PrzyciskWyslij_Click" />
        <TextBlock x:Name="EtykietaWynik" FontSize="16" Margin="0,12,0,0" />
    </StackPanel>
</Window>
  1. x:Name Nazwy mają tylko te kontrolki, które czyta lub zmienia code-behind.
  2. Click="PrzyciskWyslij_Click" Jedyne połączenie z kodem.

5 · Krok dalej

MVVM: ta sama idea w pełnym wzorcu

MVVM rozwija pomysł rozdzielenia na standardowy wzorzec WPF. Nie jest wymagany na egzaminie w pełnej formie, ale zasada oddzielenia logiki od interfejsu jest oceniana jako dobra praktyka.

Modeldane i reguły

Czyste dane i reguły biznesowe, bez wiedzy o oknie. U nas odpowiada mu klasa Formater.

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

Viewwygląd

Plik XAML bez logiki. Pokazuje dane i przyjmuje akcje użytkownika.

Na przykład MainWindow.xaml

ViewModelpośrednik

Udostępnia dane w formie gotowej do wyświetlenia (wiązanie danych) i komendy zamiast Click w code-behind.

Na przykład FormularzViewModel

6 · Przedsmak MVVM

FormularzViewModel.cs: to samo, bez code-behind

Ten sam formularz w pełnym MVVM. ViewModel ma właściwości (Imie, Nazwisko, Wynik) oraz komendę, a okno tylko się z nimi wiąże. Szczegóły poznasz w kolejnych lekcjach, tu zwróć uwagę na ideę.

FormularzViewModel.cscsharp
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using MojaAppWpf.Logika;

namespace MojaAppWpf;

public class FormularzViewModel : INotifyPropertyChanged
{
    private readonly Formater _formater = new Formater();
    private string _imie = "";
    private string _nazwisko = "";
    private string _wynik = "";

    public string Imie
    {
        get => _imie;
        set { _imie = value; Zmieniono(); }
    }

    public string Nazwisko
    {
        get => _nazwisko;
        set { _nazwisko = value; Zmieniono(); }
    }

    public string Wynik
    {
        get => _wynik;
        private set { _wynik = value; Zmieniono(); }
    }

    public ICommand WyslijCommand { get; }

    public FormularzViewModel()
    {
        WyslijCommand = new RelayCommand(() => Wynik = _formater.PolaczImieNazwisko(Imie, Nazwisko));
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    private void Zmieniono([CallerMemberName] string? nazwa = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nazwa));
    }
}
  1. INotifyPropertyChanged Interfejs, dzięki któremu ViewModel powiadamia widok, że wartość się zmieniła.
  2. Zmieniono() Metoda wywołująca zdarzenie PropertyChanged. Atrybut CallerMemberName sam podstawia nazwę właściwości.
  3. WyslijCommand Komenda w miejsce metody Click. Używa RelayCommand z lekcji o zdarzeniach.
  4. _formater Ta sama klasa logiki, bez żadnych zmian. Dobrze oddzielona logika pasuje do dowolnej architektury.

7 · Wiązanie danych

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

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

MvvmWindow.xamlxaml
<Window x:Class="MojaAppWpf.MvvmWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MojaAppWpf"
        Title="Formularz MVVM" Height="260" Width="340">
    <Window.DataContext>
        <local:FormularzViewModel />
    </Window.DataContext>
    <StackPanel Margin="20">
        <TextBox Text="{Binding Imie, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10" />
        <TextBox Text="{Binding Nazwisko, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10" />
        <Button Content="Wyślij" Command="{Binding WyslijCommand}" />
        <TextBlock Text="{Binding Wynik}" FontSize="16" Margin="0,12,0,0" />
    </StackPanel>
</Window>
  1. <Window.DataContext> Mówi oknu, który obiekt jest jego ViewModelem.
  2. {Binding Imie, UpdateSourceTrigger=PropertyChanged} Wartość pola trafia do właściwości Imie przy każdym znaku.
  3. Command="{Binding WyslijCommand}" Przycisk uruchamia komendę zamiast metody w code-behind.

8 · Czego unikać

Trzy błędy z oddzielaniem logiki

Logika zna kontrolki
Tak nie
public class Formater
{
    public void Pokaz(TextBox pole)
    {
        MessageBox.Show("Witaj, " + pole.Text);
    }
}

Klasy nie da się przetestować ani użyć poza tym oknem.

Tak
public class Formater
{
    public string Zloz(string imie) => "Witaj, " + imie;
}

Logika przyjmuje zwykłe dane i zwraca wynik. Pokazywanie go to zadanie okna.

Gruby code-behind mimo klasy logiki
Tak nie
private void Wyslij_Click(object sender, RoutedEventArgs e)
{
    if (PoleImie.Text.Length > 0 && PoleNazwisko.Text.Length > 0)
        EtykietaWynik.Text = _formater.Zloz(PoleImie.Text);
    else
        EtykietaWynik.Text = "Błąd";
}

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

Tak
private void Wyslij_Click(object sender, RoutedEventArgs e)
{
    EtykietaWynik.Text = _formater.PolaczImieNazwisko(PoleImie.Text, PoleNazwisko.Text);
}

Warunki i reguły należą do klasy logiki, a code-behind tylko przekazuje dane.

Nowy obiekt logiki przy każdym kliknięciu
Tak nie
private void Wyslij_Click(object sender, RoutedEventArgs e)
{
    var formater = new Formater();
    // ...
}

Niepotrzebne tworzenie obiektu za każdym razem. Gdy logika ma stan, ten stan zniknie.

Tak
private readonly Formater _formater = new Formater();

Obiekt logiki tworzysz raz, jako pole klasy okna.

9 · 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 (obliczenia, reguły) żyje w osobnej klasie, która nie zna kontrolek ani MessageBox.
  2. Code-behind to cienki „klej”: odczytaj dane, wywołaj logikę, pokaż wynik.
  3. Obiekt logiki tworzysz raz, jako pole klasy.
  4. MVVM = Model (dane i reguły) + View (XAML) + ViewModel (dane do wyświetlenia i komendy).
  5. ViewModel powiadamia widok o zmianach przez INotifyPropertyChanged, a widok łączy się z nim przez {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