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.
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.
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}!";
}
}
string? imieZnak?oznacza, że wartość może byćnull. Metoda sobie z tym radzi.imie?.Trim() ?? ""Jeśliimiejestnull, wynikiem jest pusty napis, w przeciwnym razie napis bez spacji na krańcach.string.IsNullOrEmpty(...)Reguła biznesowa: bez imienia lub nazwiska zwracamy komunikat zamiast powitania.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ń.
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;
}
}
private readonly Formater _formater = new Formater();Obiekt logiki tworzysz raz, jako pole klasy, a nie przy każdym kliknięciu._formater.PolaczImieNazwisko(...)Cała robota dzieje się w klasie logiki.EtykietaWynik.Text = wynikJedyne, 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.
<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>
x:NameNazwy mają tylko te kontrolki, które czyta lub zmienia code-behind.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ę.
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));
}
}
INotifyPropertyChangedInterfejs, dzięki któremu ViewModel powiadamia widok, że wartość się zmieniła.Zmieniono()Metoda wywołująca zdarzeniePropertyChanged. AtrybutCallerMemberNamesam podstawia nazwę właściwości.WyslijCommandKomenda w miejsce metodyClick. UżywaRelayCommandz lekcji o zdarzeniach._formaterTa 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ą.
<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>
<Window.DataContext>Mówi oknu, który obiekt jest jego ViewModelem.{Binding Imie, UpdateSourceTrigger=PropertyChanged}Wartość pola trafia do właściwościImieprzy każdym znaku.Command="{Binding WyslijCommand}"Przycisk uruchamia komendę zamiast metody w code-behind.
8 · Czego unikać
Trzy błędy z oddzielaniem logiki
Logika zna kontrolki
public class Formater
{
public void Pokaz(TextBox pole)
{
MessageBox.Show("Witaj, " + pole.Text);
}
}Klasy nie da się przetestować ani użyć poza tym oknem.
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
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.
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
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.
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ę.
Podsumowanie
Co warto zapamiętać
- Logika (obliczenia, reguły) żyje w osobnej klasie, która nie zna kontrolek ani
MessageBox. - Code-behind to cienki „klej”: odczytaj dane, wywołaj logikę, pokaż wynik.
- Obiekt logiki tworzysz raz, jako pole klasy.
- MVVM = Model (dane i reguły) + View (XAML) + ViewModel (dane do wyświetlenia i komendy).
- 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ę →