Lekcja 12. Oddzielenie logiki od UI — wprowadzenie do wzorca MVVM
Trudny / egzaminacyjnyPo co się tego uczymy?
Do tej pory cała logika (sprawdzanie danych, obliczenia) siedziała bezpośrednio w code-behind okna, wymieszana z obsługą przycisków. To działa dla małych przykładów, ale w większym projekcie szybko robi się bałagan — trudno znaleźć, gdzie coś zmienić, i nie da się przetestować logiki bez uruchamiania całego okna. Ta lekcja pokazuje, jak to uporządkować, wprowadzając wzorzec architektoniczny znany jako MVVM.
Teoria
Zanim przejdziemy do pełnego MVVM, zacznijmy od prostszej, fundamentalnej zasady: logika biznesowa (obliczenia, reguły) powinna żyć w osobnej klasie, niezależnej od okna. Klasa logiki nie powinna nic wiedzieć o przyciskach, oknach czy MessageBox — przyjmuje dane, zwraca wynik, i tyle. Code-behind okna staje się wtedy cienką warstwą "kleju": odczytuje dane z kontrolek, przekazuje je do klasy logiki, i wypisuje wynik z powrotem na ekranie.
Korzyści z takiego podziału: porządek (UI nie miesza się z regułami działania), czytelność (łatwiej znaleźć, gdzie zmienić wygląd, a gdzie regułę biznesową), testowalność (logikę można sprawdzić w osobnym teście, bez uruchamiania całego okna), skalowalność (projekt rośnie, a Ty dodajesz kolejne metody do klasy logiki, nie zaśmiecając kodu okna).
MVVM (Model-View-ViewModel) to rozwinięcie tej samej idei do pełnego wzorca architektonicznego, standardowego w świecie WPF:
- Model — czyste dane i reguły biznesowe (np. klasa
Produktz polami Nazwa/Cena); - View — plik XAML, czyli sam wygląd okna, 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) i komendy (
ICommand) reagujące na akcje użytkownika, zamiast zwykłych zdarzeńClickw code-behind.
Pełne MVVM z bindingiem dwukierunkowym i komendami to 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.
Schemat
Bez rozdzielenia (bałagan): Z rozdzieleniem logiki:
MainWindow.xaml.cs MainWindow.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 UI,
mozna testowac osobno)
Przykład z życia
Wyobraź sobie formularz "Imię + Nazwisko → Wyślij → wynik". 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 okna tylko odczytuje tekst z pól, woła tę metodę, i wypisuje wynik na etykiecie. Gdyby ktoś chciał potem zmienić regułę łączenia (np. dodać automatyczne wielkie litery), zmienia się TYLKO klasę Formater, nie dotykając w ogóle kodu okna.
Kod (C#)
// Logika/Formater.cs - czysta logika, NIE zna kontrolek WPF
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 imie i nazwisko!";
}
return $"Witaj, {imie} {nazwisko}!";
}
}
}
// MainWindow.xaml.cs - "klej" laczacy UI z logika
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 logike
string wynik = _formater.PolaczImieNazwisko(PoleImie.Text, PoleNazwisko.Text);
EtykietaWynik.Text = wynik;
}
}
}
Komentarz i wyjaśnienie kodu
Klasa Formater w folderze Logika nie ma ŻADNEGO odwołania do Window, TextBox czy czegokolwiek z WPF — 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 okna), 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.
MainWindow.xaml.cs ma pole _formater (instancję klasy logiki) i w metodzie obsługi kliknięcia robi TYLKO trzy rzeczy: odczytuje dane z pól tekstowych, przekazuje je do metody logiki, wypisuje zwrócony wynik. Żadnych obliczeń ani reguł bezpośrednio w tej metodzie — to jest właśnie ta "cienka warstwa kleju", o której mowa w teorii.
Ćwiczenie samodzielne
Weź dowolną aplikację WPF, którą pisałeś we wcześniejszych lekcjach tego działu (np. kalkulator z lekcji o kontrolkach). Wydziel logikę obliczeniową do osobnej klasy w nowym folderze "Logika", tak żeby code-behind okna 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 polem 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 okna niech tylko odczytuje dane, woła te metody i wyświetla wynik.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 okno WPF jest tylko cienką warstwą UI odczytującą wybrane waluty i kwotę.
Typowe błędy
Dodawanie do klasy logiki odwołań do MessageBox 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 okna, 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 WPF — choć podstawa programowa 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ę też w kolejnej lekcji o modelach danych i walidacji.