🏗️ Паттерны MVP и MVVM

Архитектура современных приложений

Вы узнаете:

  • ✅ Что такое архитектурные паттерны
  • ✅ Зачем разделять логику и интерфейс
  • ✅ MVP против MVVM — плюсы и минусы
  • ✅ Живые примеры на C# и WPF

❌ Проблема — "Божественная форма"

public partial class Form1 : Form
{
    // Логика работы с базой данных
    // Обработка нажатий кнопок
    // Валидация данных
    // Работа с API
    // Графика и анимация
    // ВСЕ В ОДНОМ МЕСТЕ!
    
    private void button1_Click(object sender, EventArgs e)
    {
        // Сотни строк кода...
    }
}
Проблемы:
  • 📦 Код невозможно тестировать
  • 🔧 Сложно менять интерфейс
  • 🐛 Ошибки трудно найти
  • 👥 Нельзя работать в команде
  • 🔄 Сложно переносить на другую платформу

Решение — Архитектурные паттерны

Архитектурный паттерн — это проверенное решение для организации кода в приложении.

Главный принцип: РАЗДЕЛЕНИЕ ОТВЕТСТВЕННОСТИ
┌─────────────────────────────────────────┐ │ ПРЕДСТАВЛЕНИЕ │ │ (View - интерфейс) │ ├─────────────────────────────────────────┤ │ ЛОГИКА / МОДЕЛЬ │ │ (Business Logic / Data) │ └─────────────────────────────────────────┘

Популярные паттерны:

  • MVC (Model-View-Controller) — классика веба
  • MVP (Model-View-Presenter) — WinForms, Android
  • MVVM (Model-View-ViewModel) — WPF, Xamarin

MVP — Model-View-Presenter

┌──────────┐ ┌──────────────┐ ┌─────────┐ │ VIEW │◄────►│ PRESENTER │◄────►│ MODEL │ │ (Форма) │ │ (Весь ум здесь)│ │(Данные) │ └──────────┘ └──────────────┘ └─────────┘ ▲ │ │ ▼ └────────────────────┘ (События от View)
КомпонентОбязанности
ModelДанные, бизнес-логика, база данных
ViewОтображение интерфейса, передача действий
PresenterПосредник: получает данные → обновляет View
Ключевая особенность: View НЕ ЗНАЕТ о Model! Presenter знает и о View, и о Model

MVP — пример на WinForms

Интерфейс для View

public interface ICalculatorView
{
    string FirstNumber { get; set; }
    string SecondNumber { get; set; }
    string Result { get; set; }
    
    event EventHandler AddClicked;
    event EventHandler SubtractClicked;
    
    void ShowError(string message);
}

Модель

public class CalculatorModel
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;
}

MVP — Presenter

public class CalculatorPresenter
{
    private readonly ICalculatorView _view;
    private readonly CalculatorModel _model;
    
    public CalculatorPresenter(ICalculatorView view, CalculatorModel model)
    {
        _view = view;
        _model = model;
        
        _view.AddClicked += OnAddClicked;
        _view.SubtractClicked += OnSubtractClicked;
    }
    
    private void OnAddClicked(object sender, EventArgs e)
    {
        try
        {
            int a = int.Parse(_view.FirstNumber);
            int b = int.Parse(_view.SecondNumber);
            int result = _model.Add(a, b);
            _view.Result = result.ToString();
        }
        catch (Exception ex)
        {
            _view.ShowError("Ошибка: " + ex.Message);
        }
    }
}

Плюсы и минусы MVP

✅ Плюсы

  • Тестируемость — Presenter можно тестировать без формы
  • Разделение — Логика отделена от интерфейса
  • Замена View — Легко сменить WinForms на WPF
  • Читаемость — Код становится понятнее
  • Командная работа — Дизайнер и программист работают параллельно

❌ Минусы

  • Больше кода — Нужно писать интерфейсы и связывать
  • Сложность — Для маленьких проектов избыточно
  • Presenter может раздуться — Весь "ум" в одном месте
Когда использовать: Средние и большие проекты, сложная логика, необходимость тестирования.

MVVM — Model-View-ViewModel

┌──────────┐ ┌──────────────┐ ┌─────────┐ │ VIEW │────►│ VIEWMODEL │◄────┤ MODEL │ │ (XAML) │ │ (Посредник) │ │(Данные) │ └──────────┘ └──────────────┘ └─────────┘ ▲ │ │ │ └──────────────────┘ (Data Binding)

Ключевые технологии:

  • 🔗 Data Binding — автоматическая синхронизация
  • ⚡ Commands — обработка действий
  • 📢 INotifyPropertyChanged — уведомления об изменениях

MVVM — главные магические слова

1. INotifyPropertyChanged

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

2. Data Binding в XAML

<TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="{Binding UserName}" />

3. RelayCommand

public class RelayCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func _canExecute;
    
    public RelayCommand(Action execute, Func canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }
    
    public bool CanExecute(object parameter) => _canExecute == null || _canExecute();
    public void Execute(object parameter) => _execute();
}

MVVM — ViewModel пример

public class MainViewModel : ViewModelBase
{
    private string _userName;
    private List<Person> _people;
    
    public MainViewModel()
    {
        LoadPeopleCommand = new RelayCommand(LoadPeople);
        SaveCommand = new RelayCommand(Save, CanSave);
    }
    
    public string UserName
    {
        get => _userName;
        set 
        { 
            _userName = value; 
            OnPropertyChanged();
        }
    }
    
    public List<Person> People
    {
        get => _people;
        set { _people = value; OnPropertyChanged(); }
    }
    
    public ICommand LoadPeopleCommand { get; }
    public ICommand SaveCommand { get; }
    
    private void LoadPeople()
    {
        People = Database.GetAllPersons();
    }
    
    private void Save()
    {
        Database.SavePerson(SelectedPerson);
    }
}

MVVM — XAML View

<Window x:Class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    
    <Grid>
        <!-- Ввод имени -->
        <TextBox Text="{Binding UserName}" />
        
        <!-- Список людей -->
        <DataGrid ItemsSource="{Binding People}"
                  SelectedItem="{Binding SelectedPerson}" />
        
        <!-- Кнопки -->
        <Button Content="Загрузить" Command="{Binding LoadPeopleCommand}" />
        <Button Content="Сохранить" Command="{Binding SaveCommand}" />
    </Grid>
</Window>
Обратите внимание: В XAML почти нет кода! Только привязки!

Сравнение MVP и MVVM

ХарактеристикаMVPMVVM
СложностьСредняяВысокая
Связь View-PresenterView знает о PresenterView не знает о ViewModel
БиндингРучнойАвтоматический
ПлатформыWinForms, AndroidWPF, Xamarin, MAUI
ТестированиеЛегкоЛегко
Код в ViewЕсть (события)Почти отсутствует
Порог входаНизкийВысокий

Когда что выбирать?

✅ MVP подходит для:

  • WinForms приложения
  • Проекты без сложного биндинга
  • Команда новичков
  • Быстрая разработка

✅ MVVM подходит для:

  • WPF / MAUI приложения
  • Проекты с частым обновлением UI
  • Большая команда
  • Требуется переиспользование логики
Простой совет:
WinForms → MVP
WPF/MAUI → MVVM
Маленький проект → можно без паттернов

Тестирование MVP

Presenter легко тестировать без формы!
[TestClass]
public class CalculatorPresenterTests
{
    [TestMethod]
    public void Add_ValidNumbers_ShowsSum()
    {
        // Arrange
        var mockView = new Mock<ICalculatorView>();
        mockView.Setup(v => v.FirstNumber).Returns("5");
        mockView.Setup(v => v.SecondNumber).Returns("3");
        
        var presenter = new CalculatorPresenter(mockView.Object, new CalculatorModel());
        
        // Act
        mockView.Raise(v => v.AddClicked += null, EventArgs.Empty);
        
        // Assert
        mockView.VerifySet(v => v.Result = "8", Times.Once);
    }
    
    [TestMethod]
    public void Add_InvalidNumbers_ShowsError()
    {
        var mockView = new Mock<ICalculatorView>();
        mockView.Setup(v => v.FirstNumber).Returns("abc");
        
        var presenter = new CalculatorPresenter(mockView.Object, new CalculatorModel());
        
        mockView.Raise(v => v.AddClicked += null, EventArgs.Empty);
        
        mockView.Verify(v => v.ShowError(It.IsAny<string>()), Times.Once);
    }
}
🚀 Тесты работают без запуска формы!

Распространенные ошибки

❌ MVP

  • Presenter напрямую работает с контролами → используйте интерфейс View
  • View знает о Model → модель только в Presenter
  • Логика в коде формы → весь ум в Presenter

❌ MVVM

  • Код в code-behind → переносите в ViewModel
  • Не реализован INotifyPropertyChanged → наследуйте от базового класса
  • Прямой доступ к контролам → используйте биндинг

Dependency Injection в MVP/MVVM

Плохо: Presenter сам создает зависимости
public class MainPresenter
{
    private IDataBase _db = new SqlDatabase(); // Жесткая связь!
}
Хорошо: зависимости передаются извне
public class MainPresenter
{
    private readonly IDataBase _db;
    private readonly ILogger _logger;
    
    public MainPresenter(IDataBase db, ILogger logger)
    {
        _db = db;
        _logger = logger;
    }
}

// Настройка DI-контейнера
var services = new ServiceCollection();
services.AddSingleton<IDataBase, SqlDatabase>();
services.AddSingleton<ILogger, FileLogger>();
services.AddTransient<MainPresenter>();

Современные фреймворки

Для MVP:

  • WinForms — классика
  • Xamarin.Android — мобильная разработка

Для MVVM:

  • WPF — золотой стандарт
  • MAUI — современный кроссплатформенный
  • Avalonia UI — кроссплатформенный аналог WPF
  • UWP / WinUI — приложения для Windows

Библиотеки для упрощения:

  • Prism — модульность, навигация, DI
  • Caliburn.Micro — конвенции вместо конфигурации
  • MVVM Light — легковесный MVVM
  • ReactiveUI — реактивный MVVM

Резюме и шпаргалка

🎯 MVP

  • WinForms приложения
  • Android приложения
  • Команда не знакома с биндингом
View → Presenter → Model View знает Presenter Ручное обновление

🎯 MVVM

  • WPF / MAUI / Avalonia
  • Приложения с богатым UI
  • Требуется тестируемость
View → ViewModel → Model View не знает ViewModel Авто-обновление (биндинг)
Простая памятка:
MVP: View знает Presenter, ручное обновление
MVVM: View не знает ViewModel, авто-обновление

Часто задаваемые вопросы

Q: Можно ли использовать MVP в WPF?
A: Можно, но теряются преимущества биндинга. Лучше MVVM.
Q: А MVVM в WinForms?
A: Технически можно, но очень сложно (нет биндинга). Не рекомендуется.
Q: Как выбрать между MVP и MVVM?
A: WinForms → MVP, WPF/MAUI → MVVM
Q: Может ли ViewModel знать о View?
A: НЕТ! Это нарушает паттерн. ViewModel должна быть независимой.
Q: Где хранить бизнес-логику?
A: В Model. ViewModel только подготавливает данные для отображения.

Заключение

🎯 Что мы узнали:

  • ✅ Проблемы "божественной формы"
  • ✅ MVP — Model-View-Presenter для WinForms
  • ✅ MVVM — Model-View-ViewModel для WPF
  • ✅ INotifyPropertyChanged, Commands, Bindings
  • ✅ Тестирование без UI
  • ✅ Когда и что выбирать

🙏 Спасибо за внимание!

Вопросы?