Mvvm что это
Перейти к содержимому

Mvvm что это

  • автор:

Краткое руководство по MVVM: ключевые моменты и примеры

MVVM (Model-View-ViewModel) — способ организации кода. Он помогает отделить пользовательский интерфейс от логики.

Цифровий курс від robotdreams: DevOps Engineer.
підходи для створення сучасних і масштабованих застосунків.

Паттерн MVVM

  • View содержит только код пользовательского интерфейса. Он отображает экран (текстовые поля, ярлыки, кнопки) и пользовательский ввод. Например, в Windows Forms это обычно Form или Control . В Windows Presentation Foundation (WPF) — это класс Window: файлы XAML и xaml.cs.
  • Model — это классы. Это «вещи» в вашей программе. Например, в программе ввода заказов вашими классами моделей могут быть: Customer, Order, InventoryItem. Эти классы будут содержать логику, необходимую для выполнения функций.

Как работает паттерн MVVM ?

Как работает паттерн MVVM можно разобрать на примере из реального мира. Представьте, что у нас есть владелец интернет-магазина, веб-мастер и контент-менеджер:

Експертний курс від skvot: Unreal Еngine: від інтерфейсу до запуску гри.
Запустіть свою гру з Unreal.

  • владелец интернет-магазина (Model) — занимается своей работой, ни на что не отвлекаясь. Если начинается важная акция, он передает веб-мастеру, что нужно добавить новость о ней в блог;
  • веб-мастер (ViewModel) – получает информацию от владельца интернет-магазина и передает ее контент-менеджеру;
  • контент-менеджер (View) — пишет новость в блог, основываясь на данных, которые предоставил веб-мастер, и публикует ее.

Примеры использования MVVM

Основным примером использования MVVM является программирование графического интерфейса пользователя (GUI). Он используется для простого событийно-управляемого программирования пользовательских интерфейсов путем отделения View от логики бэкенда, управляющей данными.

В WPF View проектируется с помощью языка разметки XAML. Файлы XAML привязываются к ViewModel. Таким образом View отвечает только за представление, а ViewModel — только за управление состоянием приложения.

MVVM очень широко используется в JavaScript-библиотеке Knockout.js.

Реализация паттерна MVVM

Рассмотрим следующую реализацию MVVM с использованием C#, .NET и WPF.

У нас есть класс Model под названием Animals, класс View, реализованный в XAML, и модель ViewModel под названием AnimalViewModel. Обратите внимание, что Model ни о чем не знает, ViewModel знает только о модели, а View знает только о ViewModel.

Событие OnNotifyPropertyChanged-event позволяет обновлять и Model, и View так, что когда вы вводите что-то в текстовое поле во View, Model обновляется. И если что-то обновляет Model, то обновляется и View:

Ефективний курс від mate.academy: Frontend.
Розблокуйте світ веб-розробки.

/*Model class*/ public class Animal < public string Name < get; set; >public string Gender < get; set; >> /*ViewModel class*/ public class AnimalViewModel : INotifyPropertyChanged < private Animal _model; public AnimalViewModel() < _model = new Animal ; > public string AnimalName < get < return _model.Name; >set < _model.Name = value; OnPropertyChanged("AnimalName"); >> public string AnimalGender < get < return _model.Gender; >set < _model.Gender = value; OnPropertyChanged("AnimalGender"); >> //Event binds view to ViewModel. public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) < if (this.PropertyChanged != null) < var e = new PropertyChangedEventArgs(propertyName); this.PropertyChanged(this, e); >> >     " Width="120" /> " Width="120" /> 

Изменение кода с помощью MVVM

Давайте посмотрим на код, написанный без использования MVVM . Проблема в том, что единственный способ проверить логику — это вручную запустить программу и ввести значения для каждого сценария:

using System.Linq; using System.Windows; namespace WorkbenchWPF.MVVMPattern.NonPatternVersion < public partial class AccountCreationView : Window < public AccountCreationView() < InitializeComponent(); >private void OnClick_ValidatePassword(object sender, RoutedEventArgs e) < if(txtPassword.Text.Trim().Length < 8) < lblErrorMessage.Text = "Password must be at least eight characters long"; >else if(txtPassword.Text.Trim().Length > 20) < lblErrorMessage.Text = "Password cannot be more than twenty characters long"; >else if(!txtPassword.Text.Any(char.IsUpper)) < lblErrorMessage.Text = "Password must contain at least one upper-case character"; >else if(!txtPassword.Text.Any(char.IsLower)) < lblErrorMessage.Text = "Password must contain at least one lower-case character"; >else if(!txtPassword.Text.Any(char.IsNumber)) < lblErrorMessage.Text = "Password must contain at least one number"; >else < lblErrorMessage.Text = "Password is secure"; >> > >

Теперь обратите внимание на код, написанный с использованием паттерна MVVM . Когда код разделен с помощью MVVM, можно написать автоматизированные тесты, которые могут проверить все возможные значения за считанные минуты:

using System.Windows; using Engine.MVVMPattern.PatternVersion.ViewModels; namespace WorkbenchWPF.MVVMPattern.PatternVersion < public partial class AccountCreationView : Window < private readonly AccountCreationViewModel _viewModel = new AccountCreationViewModel(); public AccountCreationView() < InitializeComponent(); DataContext = _viewModel; >private void OnClick_ValidatePassword(object sender, RoutedEventArgs e) < _viewModel.ValidatePassword(); >> >

Ключевые моменты паттерна MVVM

  • Пользователь взаимодействует с View.
  • View имеет ссылку на ViewModel, но View Model не имеет информации о View.

MVVM: полное понимание (+WPF) Часть 1

В настоящей статье задействован мой опыт доведения некоторого числа студентов до полного и окончательного понимания паттерна MVVM и реализации его в WPF. Паттерн описывается на примерах возрастающей сложности. Сначала теоретическая часть, которая может использоваться безотносительно конкретного языка, затем практическая часть, в которой показано несколько вариантов реализации коммуникации между слоями с использованием WPF и, немножко, Prism.

Зачем вообще нужно использовать паттерн MVVM? Это ведь лишний код! Написать тоже самое можно гораздо понятнее и прямолинейнее.

Отвечаю: в маленьких проектах прямолинейный подход срабатывает. Но стоит ему стать чуть больше — и логика программы размазывается в интерфейсе так, что потом весь проект превращается в монолитный клубок, который проще переписать заново, чем пытаться распутать. Для наглядности можно посмотреть на две картинки:

Изображение 1: код без MVVM.

Изображение 2: код с MVVM.

В первом случае программист, со словами «Мне нужно просто соединить вот этот порт и этот, зачем мне все эти хомутики и лейблики?», просто соединяет патчкордом пару слотов. Во втором случае использует некоторый шаблонный подход.

Рассмотрение паттерна на примере №1: Сложение двух чисел с выводом результата

Методика написания программы используя подход «ModelFirst».

  • 1. Разработать модель программы.
  • 2. Нарисовать интерфейс программы.
  • 3. Соединить интерфейс и модель прослойкой VM.

Однако творчество — это довольно затратный по ресурсам мыслительный процесс, и следует избегать излишнее обращение к нему. Во-первых — чтобы не перенапрягаться. Во-вторых — чтобы ваш собрат по программированию (да и вы сами, через пару недель) заглянув к вам в код, не был бы принужден к, излишне увлекательному порой, следованию за творческим полетом мысли. Все места, где вам вдруг удалось применить творчество, — необходимо снабдить подробным комментарием. Кстати, существуют многочисленные паттерны программирования, которые помогают вам это самое творчество — не применять.

Итак, моделью в нашей задаче будет сложение чисел с возвратом результата. Модель, в принципе, может не хранить никакого состояния. Т.е. она может вполне быть реализована статическим методом статического класса. Примерно так:

 static class MathFuncs < public static int GetSumOf(int a, int b) =>a + b; >

Следующий шаг — (см. методику «ModelFirst») — создать View или, проще — нарисовать интерфейс. Это тоже часть, которая может содержать творчество. Но, опять же, не стоить с ним перебарщивать. Пользователь не должен быть шокирован неожиданностями интерфейса. Интерфейс должен быть интуитивен. Наша View будет содержать три текстовых поля, которые можно снабдить лейблами: число номер один, число номер два, сумма.

Заключительный шаг — соединение View и модели через VM. VM — это такое место, которое вообще не должно содержать творческого элемента. Т.е. эта часть паттерна железно обуславливается View и не должна содержать в себе НИКАКОЙ «бизнес логики». Что значит обусловленность от View? Это значит, что если у нас во View есть три текстовых поля, или три места, которые должны вводить/выводить данные — следовательно в VM (своего рода подложке) должны быть минимум три свойства, которые эти данные принимают/предоставляют.

Следовательно два свойства принимают из View число номер один и два, а третье свойство — вызывает нашу модель для выполнения бизнес-логики нашей программы. VM ни в коем случае не выполняет сложение чисел самостоятельно, оно для этого действия только вызывает модель! В этом и состоит функция VM — соединять View (которое тоже ничем иным, кроме как приема ввода от пользователя и предоставления ему вывода не занимается) и Модель, в которой происходит все вычисление. Если нарисовать картинку нашей задачки, то получиться нечто такое:

Изображение 3: Схема Примера №1

Зеленое — это View, три зеленые точки в которой — это наши три текстовые поля. Синее — это VM, к которой эти три зеленых точки железно прибиты (прибиндены), ну а красное облачко — это модель, которая занимается вычислением.

Реализация Примера №1 в WPF

Конкретно в WPF реализована «аппаратная поддержка» паттерна MVVM. View реализуется в XAML. Т.е. зеленый слой (View) будет написана на XAML. Зеленые точки — это будут текстовые поля. А зеленые линии, соединяющиеся с синими — будут реализованы через механизм Binding. Зеленая пунктирная линия — связь всей View и VM осуществляется, когда мы создаем объект VM и присваиванием его свойству DataContext View.

       "> "> " IsReadOnly="True"> 

Теперь выполняем последний пункт методики — реализуем VM. Чтобы наша VM «автоматически» обновляла View, требуется реализовать интерфейс INotifyPropertyChange. Именно посредством него View получает уведомления, что во VM что-то изменилось и требуется обновить данные.

Делается это следующим образом:

public class MainVM : INotifyPropertyChange < public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) < PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); >>

Теперь снабдим VM тремя необходимыми свойствами. (Требования для установления связи VM и View такое, что это должны быть открытые свойства)

private int _number1; public int Number1 < get set < _number1 = value; OnPropertyChanged("Number3"); // уведомление View о том, что изменилась сумма >> private int _number2; public int Number2 < get set < _number1 = value; OnPropertyChanged("Number3"); >>

Последнее свойство — это линия пунктирная синяя линия связи VM и модели:

//свойство только для чтения, оно считывается View каждый раз, когда обновляется Number1 или Number2 public int Number3 < get; >=> MathFuncs.GetSumOf(Number1, Number2);

Мы реализовали полноценное приложение с применением паттерна MVVM.

Рассмотрение паттерна на примере №2:

Теперь усложним наше задание. В программе будет текстовое поле для ввода числа. Будет ListBox с коллекцией значений. Кнопка «Добавить», по нажатию на которую число в текстовом поле будет добавлено в коллекцию значений. Кнопка удалить, по нажатию на которую выделенное в ListBox’е число будет удалено из коллекции. И текстовое поле с суммой всех значений в коллекции.

Изображение 4: Интерфейс для Примера №2

Согласно методике — необходимо сначала разработать модель. Теперь модель не может быть stateless и должна хранить состояние. Значит в модели будет коллекция элементов. Это раз. Затем — операция добавление некоторого числа в коллекцию — это обязанность модели. VM не может залезать во внутренность модели и самостоятельно добавлять в коллекцию модели число, она обязана просить сделать это саму модель. В противном случае это будет нарушение принципа инкапсуляции. Это как если бы водитель не заливал, как положено, топливо в бензобак и т.д. — а лез бы под капот и впрыскивал топливо непосредственно в цилиндр. То есть будет метод «добавить число в коллекцию». Это два. И третье: модель будет предоставлять сумму значений коллекции и точно также уведомлять об ее изменении через интерфейс INotifyPropertyChanged. Не будем разводить споры о чистоте модели, а будем просто использовать уведомления.

Давайте сразу реализуем модель:

Коллекция элементов должна уведомлять подписчиков о своем изменении. И она должна быть только для чтения, чтобы никто, кроме модели, не могли ее как-либо изменить. Ограничение доступа — это выполнение принципа инкапсуляции, оно должно соблюдаться неукоснительно, чтобы: а) самому случайно не создать ситуацию трудноуловимого дебага, б) вселить уверенность, что поле не изменяется извне — опять же, в целях облегчения отладки.

Кроме того, так так мы далее все равно подключим Prism для DelegateCommand, то давайте сразу использовать BindableBase вместо самостоятельной реализации INotifyPropertyChange. Для этого надо подключить через NuGet библиотек Prism.Wpf (на момент написания 6.3.0). Соответственно OnPropertyChanged() измениться на RaisePropertyChanged().

public class MyMathModel : BindableBase < private readonly ObservableCollection_myValues = new ObservableCollection(); public readonly ReadOnlyObservableCollection MyPublicValues; public MyMathModel() < MyPublicValues = new ReadOnlyObservableCollection(_myValues); > //добавление в коллекцию числа и уведомление об изменении суммы public void AddValue(int value) < _myValues.Add(value); RaisePropertyChanged("Sum"); >//проверка на валидность, удаление из коллекции и уведомление об изменении суммы public void RemoveValue(int index) < //проверка на валидность удаления из коллекции - обязанность модели if (index >= 0 && index < _myValues.Count) _myValues.RemoveAt(index); RaisePropertyChanged("Sum"); >public int Sum => MyPublicValues.Sum(); //сумма >

Согласно методике — рисуем View. Перед этим несколько необходимых пояснений. Для того, чтобы создать связь кнопки и VM, необходимо использовать DelegateCommand. Использование для этого событий и кода формы, для чистого MVVM — непозволительно. Используемые события необходимо обрамлять в команды. Но в случае с кнопкой такого обрамления не требуется, т.к. существует специальное ее свойство Command.

Кроме того, число, которое мы будем добавлять, используя DelegateCommand, мы будем не биндить к VM, а будем передавать в качестве параметра этого DelegateCommand, чтобы не загромождать VM и избежать рассинхронизации непосредственного вызова команды и использования параметра. Обратите внимание на получившуюся схему и, особенно, на место, обведенное красной линией.

Изображение 5: Схема для Примера №2

Здесь привязка на View происходит не вида View ViewModel, а вида View View. Для того, чтобы этого добиться используется второй вид биндинга, где указывается имя элемента и его свойства, к которому осуществляться привязка — «».

       " CommandParameter=""/> " Margin="5"/> " CommandParameter=""/> "/> 

Теперь реализуем ViewModel:

public class MainVM : BindableBase < readonly MyMathModel _model = new MyMathModel(); public MainVM() < //таким нехитрым способом мы пробрасываем изменившиеся свойства модели во View _model.PropertyChanged += (s, e) =>< RaisePropertyChanged(e.PropertyName); >; AddCommand = new DelegateCommand(str => < //проверка на валидность ввода - обязанность VM int ival; if (int.TryParse(str, out ival)) _model.AddValue(ival); >); RemoveCommand = new DelegateCommand(i => < if(i.HasValue) _model.RemoveValue(i.Value); >); > public DelegateCommand AddCommand < get; >public DelegateCommand RemoveCommand < get; >public int Sum => _model.Sum; public ReadOnlyObservableCollection MyValues => _model.MyPublicValues; >

Внимание — важно! Касательно проброса уведомлений из модели. Уведомлять об изменении суммы самостоятельно VM не может, т.к. она не должна знать, что именно измениться в модели, после вызова ее методов и измениться ли вообще. Модель для VM должна быть черным ящиком. Т.е. она должна передавать ввод и действия пользователя в модель и если в модели что-то изменилось (о чем должна ее уведомлять сама модель), то только тогда уведомлять далее View.

Мы реализовали второе полноценное приложение с применением паттерна MVVM, познакомились с ObservableCollection, DelegateCommand, привязкой вида View View и пробросом уведомлений во View.

  • .NET
  • Проектирование и рефакторинг
  • C#
  • ООП
  • Промышленное программирование

Что такое MVVM архитектура

MVVM (Model-View-ViewModel) — это архитектурный шаблон, используемый в разработке программного обеспечения для разделения пользовательского интерфейса (UI) от бизнес-логики и данных. MVVM является одним из популярных подходов для проектирования клиентских приложений, особенно в средах, где применяется архитектура Model-View-Controller (MVC) или Model-View-Presenter (MVP).

В этой архитектуре компоненты разделяются следующим образом:

  1. Model (Модель): Отвечает за представление данных и бизнес-логику приложения. Модель может включать в себя операции с данными, хранение информации и управление состоянием приложения.
  2. View (Представление): Отвечает за отображение данных и взаимодействие с пользователем. Это компонент, с которым пользователь взаимодействует, и он визуализирует данные, предоставляемые ViewModel.
  3. ViewModel (Модель-представления): Служит посредником между Model и View. ViewModel преобразует данные из Model в формат, который может быть легко отображен в View, и обрабатывает пользовательские действия, перенаправляя их в Model. ViewModel также позволяет реализовать биндинг (связывание) данных между Model и View.

Этот шаблон способствует улучшению читаемости кода, облегчает тестирование и делает приложения более масштабируемыми, так как разделение бизнес-логики и представления делает каждый компонент более независимым. Этот подход широко применяется в разработке приложений с графическим интерфейсом, таких как мобильные приложения, приложения для настольных компьютеров и веб-приложения.

Отличие MVVM от MVC

MVVM (Model-View-ViewModel) и MVC (Model-View-Controller) — это два разных архитектурных шаблона, которые используются для разработки программного обеспечения, особенно для построения пользовательского интерфейса. Они имеют сходства, но также существенные отличия:

MVC (Model-View-Controller)
  1. Model (Модель): Отвечает за представление данных и бизнес-логику приложения. Он хранит данные, обрабатывает запросы на доступ к данным и обновляет состояние приложения.
  2. View (Представление): Отвечает за отображение данных и взаимодействие с пользователем. В MVC представление может иметь некоторую логику, связанную с отображением данных, но не должно содержать сложной бизнес-логики.
  3. Controller (Контроллер): Отвечает за обработку пользовательских действий и управление взаимодействием между Model и View. Контроллер реагирует на действия пользователя, изменяет состояние Model и обновляет View.
MVVM (Model-View-ViewModel)
  1. Model (Модель): То же самое, что и в MVC, отвечает за представление данных и бизнес-логику.
  2. View (Представление): Отвечает за отображение данных, но в данном случае оно пассивнее, чем в MVC. Взаимодействие с данными и логика отображения вынесены в ViewModel.
  3. ViewModel (Модель-представления): Это ключевая разница между двумя подходами. ViewModel представляет собой посредника между Model и View. Он преобразует данные из Model так, чтобы их можно было легко отображать в View, а также обрабатывает действия пользователя, перенаправляя их в Model.

Главное отличие заключается в том, что в MVVM View более пассивно, оно не содержит бизнес-логики и большая часть взаимодействия с данными и действиями пользователя обрабатывается в ViewModel, что делает код более легко тестируемым и позволяет лучше разделять ответственности между компонентами.

Преимущества MVVM

Архитектура имеет несколько преимуществ, которые делают его популярным в разработке приложений с графическим интерфейсом. Вот некоторые из основных преимуществ MVVM:

  • Разделение ответственности: MVVM четко разделяет бизнес-логику (Model) от представления (View) и управления представлением (ViewModel). Это способствует улучшению читаемости кода и упрощает поддержку и развитие приложения.
  • Тестирование: MVVM упрощает тестирование, так как ViewModel может быть отдельно протестирована, и ее можно тестировать независимо от View. Это делает юнит-тестирование более эффективным и позволяет легче обнаруживать и исправлять ошибки.
  • Биндинг данных: MVVM позволяет использовать биндинг данных (data binding) между Model и View через ViewModel. Это позволяет автоматически обновлять пользовательский интерфейс при изменении данных в Model, что упрощает синхронизацию данных и представления.
  • Масштабируемость: Разделение компонентов MVVM делает приложение более масштабируемым. Разработчики могут работать над разными частями приложения независимо, что позволяет параллельное развитие.
  • Архитектурная гибкость: MVVM дает возможность легко изменять пользовательский интерфейс без необходимости переписывать всю бизнес-логику. Также легче адаптировать приложение под разные платформы, так как ViewModel может оставаться практически неизменной.
  • Улучшенный UX-дизайн: MVVM позволяет разделить дизайн пользовательского интерфейса и логику взаимодействия с данными. Это улучшает процесс совместной работы дизайнеров и разработчиков, позволяя им работать над интерфейсом независимо.
  • Использование реактивных подходов: MVVM хорошо сочетается с реактивным программированием, позволяя более эффективно реагировать на изменения данных и событий в приложении.

В целом, шаблон представляет собой сбалансированный подход к разработке, который упрощает разделение ответственности, обеспечивает хорошую тестируемость и улучшает архитектурную гибкость приложения.

Недостатки

Несмотря на множество преимуществ, у Model-View-ViewModel также есть некоторые недостатки и ограничения, которые следует учитывать при разработке приложений:

  1. Комплексность: Внедрение этого шаблона может потребовать больше времени и усилий на начальном этапе разработки из-за необходимости создания ViewModel для каждого View. Это может добавить сложности, особенно для простых приложений.
  2. Сложность понимания для начинающих: MVVM может показаться сложным для новичков в разработке, особенно если они только начинают изучать архитектурные паттерны. Необходимо понимание связей между Model, View и ViewModel.
  3. Увеличение количества классов: Использование шаблона может привести к увеличению количества классов в проекте, особенно если приложение имеет много экранов и компонентов. Это может сделать проект более громоздким и усложнить его обслуживание.
  4. Повышенное потребление памяти: Иногда, из-за дополнительных объектов ViewModel, MVVM может потреблять больше памяти, особенно на мобильных устройствах с ограниченными ресурсами.
  5. Сложности при обработке сложной бизнес-логики: Если бизнес-логика приложения довольно сложная и требует прямого взаимодействия между Model и View, MVVM может создать дополнительный уровень абстракции (ViewModel), что может усложнить обработку таких сценариев.
  6. Сложности с библиотеками и инструментами: Некоторые библиотеки и инструменты могут быть не так хорошо поддержаны для подхода MVVM, как для других архитектурных паттернов. Это может ограничить выбор инструментов разработчика.

Важно отметить, что большинство из этих недостатков могут быть устранены или минимизированы с опытом, хорошей практикой разработки и правильным пониманием принципов MVVM. Каждый проект имеет свои уникальные требования, и выбор архитектурного паттерна должен базироваться на конкретных потребностях и контексте.

Дополнительные ссылки

  • Modern MVI и MVVM+ со всех сторон в 2023
  • iOS Clean Architecture MVVM: шаблон чистой архитектуры
  • Производительность и скорость: как в Duolingo внедрили MVVM на Android
  • Освоение MVVM на iOS
  • Movies: кино на основе MVVM

Паттерн MVVM

— это шаблон, который появился для обхода ограничений паттернов MVC и MVP, и объединяющий некоторые из их сильных сторон. Эта модель впервые появилась в составе фреймворка Small Talk в 80-х, и была позднее улучшена с учетом обновленной модели презентаций (MVP).

Шаблон MVVM имеет три основных компонента: модель, которая представляет бизнес-логику приложения, представление пользовательского интерфейса XAML, и представление-модель, в котором содержится вся логика построения графического интерфейса и ссылка на модель, поэтому он выступает в качестве модели для представления.

На следующем рисунке представлена диаграмма, которая показывает, как реализовать шаблон MVVM. Конечно, это общая реализация:

Структура приложения MVVM

Если вы планируете работать с WPF или Silverlight, вы должны воспользоваться механизмом привязок (binding), предоставляемым этими технологиями. Для этого, ваша модель-представление должна реализовать некоторые конкретные интерфейсы, необходимые модулю привязки из WPF и Silverlight. Одним из них является INotifyPropertyChanged, введенный в .NET Framework начиная с версии 2 и выше. Этот интерфейс реализует систему уведомлений которая активируется, когда значение свойства изменяется. Это требуется в модели-представления, чтобы сделать механизм привязки пользовательского интерфейса XAML динамическим.

Другой настройкой являются команды, предоставляемые интерфейсом ICommand, которые доступны для WPF и Silverlight. Команды могут привязываться к определенному XAML-элементу и определять поведение данного элемента при определенных действиях.

Третьим компонентом в показанной выше структуре является шаблон данных DataTemplate, который определяет как структурировать конкретную модель-представления или особое состояние модели-представления.

Пример приложения MVVM

Сейчас мы реализуем приложение с помощью шаблона MVVM, аналогичное рассмотренному в предыдущих статьях — простое приложение выбора проектов. Давайте начнем с диаграммы классов, представляющую конечную структуру приложения:

Структура приложения MVVM

Как видите представление теперь пустое (содержит только пользовательский интерфейс на XAML), вся логика строится на отношениях между моделью и модель-представлением.

Структура View Models

Нам понадобится два вида моделей для этого приложения. Каждая модель-представления имеет свои собственные задачи в представлении. Использование нескольких вложенных моделей называется иерархическим модель-представлением (hierarchical view model).

Модель-представление ProjectsViewModel в нашем приложении будет содержать состояние и логику представления ProjectsView, это показано на следующем рисунке:

Взаимосвязь View Model и View

Свойства привязки и их назначение описаны ниже:

  • Projects — представляет обновляемую коллекцию ObservableCollection для привязки к элементу ComboBox в представлении.
  • SelectedValue — целочисленное свойство, привязанное к выбранному проекту в ComboBox с использованием односторонней привязки. Это свойство помогает получать уведомления об изменении выбранного элемента в ComboBox.
  • SelectedProject — свойство, связывающее детали представления (сметная стоимость, фактическая стоимость) с типом проекта.
  • DetailsEnabled — привязывается к свойству IsEnabled текстовых полей сметной и фактической стоимостей в представлении. Если в ComboBox проект не выбран, поля блокируются.
  • DetailsEstimateStatus — перечисление класса Status, которое привязывается в триггерах, управляющих цветом текстового поля фактической стоимости.
  • UpdateCommand — прототип интерфейса ICommand связанный с кнопкой UpdateButton.

Второе модель-представление ProjectViewModel будет содержать состояние представления и логику для управления деталями в нашем представлении (фактическую и сметную стоимости):

Еще одна модель-представление

Структура этого модель-представления очень похожа на базовый класс Project потому что оно реализует интерфейс IProject. (Напомню, если вы еще этого не сделали, необходимо добавить в проект ссылку на сборку ProjectBilling.DataAccess.dll, которую мы создали в теме RAD & Monolitic и представляющую код доступа к данным — базовый класс Project, интерфейс IProject и несколько вспомогательных методов для извлечения данных.)

Model

Модель в нашем приложении представлена классом ProjectsModel реализующем интерфейс IProjectsModel. Интерфейс я добавил для возможного расширения приложения путем внедрения зависимостей и облегчения тестирования, он включает следующие члены:

  • Projects — содержит коллекцию проектов ObservableCollection привязанную к свойству Projects модель-представления ProjectsViewModel.
  • ProjectUpdated — событие обновления данных проекта. Обновление данных в модели приводит к автоматическому обновлению всех представлений в приложении.
  • UpdateProject — фактически обработчик события ProjectUpdated, вызывающий метод Update() экземпляра Project.

Для начала нам нужно определить несколько вспомогательных классов и интерфейс IProjectsModel. Класс Notifier реализует интерфейс INotifyPropertyChanged и будет использоваться в модели и модели-представлении для уведомлений об изменениях. Фактически он является оболочкой для инкапсуляции INotifyPropertyChanged. Класс ProjectsEventArgs просто добавляет аргументы (в виде свойств) в обработчик события IProjectsModel.ProjectUpdated (в нашем примере будем передавать только ссылку на прототип IProject). Добавьте следующий код в файл модели, например BillingProject.Model.cs:

using System; using System.ComponentModel; using System.Collections.ObjectModel; using System.Linq; using ProjectBilling.DataAccess; namespace ProjectBilling.Application < public class Notifier : INotifyPropertyChanged < public event PropertyChangedEventHandler PropertyChanged = delegate < >; protected void NotifyPropertyChanged( string propertyName) < PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); >> public interface IProjectsModel < ObservableCollectionProjects < get; set; >event EventHandler ProjectUpdated; void UpdateProject(IProject updatedProject); > public class ProjectEventArgs : EventArgs < public IProject Project < get; set; >public ProjectEventArgs(IProject project) < Project = project; >> public class ProjectsModel : IProjectsModel < >> 

Структура класса ProjectsModel выглядит следующим образом:

public class ProjectsModel : IProjectsModel < public ObservableCollectionProjects < get; set; >public event EventHandler ProjectUpdated = delegate < >; public ProjectsModel(IDataService dataService) < Projects = new ObservableCollection(); foreach (Project project in dataService.GetProjects()) < Projects.Add(project); >> public void UpdateProject(IProject updatedProject) < GetProject(updatedProject.ID).Update(updatedProject); ProjectUpdated(this, new ProjectEventArgs(updatedProject)); >private Project GetProject(int projectId) < return Projects.FirstOrDefault( project =>project.ID == projectId); > >

В конструкторе класса ProjectsModel передается ссылка на прототип IDataService, который объявлен в коде доступа к данным (ProjectBilling.DataAccess.dll). С помощью вспомогательного метода IDataService.GetProjects() инициализируется коллекция Projects. UpdateProjects() — метод, который сначала обновляет коллекцию IProjectsModel.Projects, а затем вызывает событие ProjectUpdated, чтобы уведомить все модели-представления о произошедших изменениях.

ProjectViewModel

Теперь добавим класс ProjectViewModel, реализующий одно из двух модель-представление:

using ProjectBilling.DataAccess; namespace ProjectBilling.Application < public interface IProjectViewModel : IProject < Status EstimateStatus < get; set; >> public class ProjectViewModel : Notifier, IProjectViewModel < private int _id; private string _name; private double _estimate; private double _actual; private Status _estimateStatus = Status.None; public int ID < get < return _id; >set < _id = value; NotifyPropertyChanged("Id"); >> public string Name < get < return _name; >set < _name = value; NotifyPropertyChanged("Name"); >> public double Estimate < get < return _estimate; >set < _estimate = value; NotifyPropertyChanged("Estimate"); >> public double Actual < get < return _actual; >set < _actual = value; UpdateEstimateStatus(); NotifyPropertyChanged("Actual"); >> public Status EstimateStatus < get < return _estimateStatus; >set < _estimateStatus = value; NotifyPropertyChanged("EstimateStatus"); >> > >

Перечисление Status содержит три значения, которые соответствуют простой логике сравнения фактической и сметной стоимостей в нашем приложении (это перечисление будет объявлено в ProjectsViewModel, поэтому не обращайте внимание на ошибки, которые покажет Visual Studio при отсутствии объявления Status).

Интерфейс IProjectViewModel обязует добавить ссылку на это перечисление. Сам класс ProjectViewModel является довольно простым, он объявляет несколько закрытых переменных (инкапсулированных в общедоступных свойствах), которые соответствуют сигнатуре базового класса Project. ProjectViewModel унаследован от Notifier, поэтому в общедоступных свойствах включена поддержка уведомлений о изменении.

Добавим также в этот класс два конструктора и несколько вспомогательных методов:

public ProjectViewModel() <> public ProjectViewModel(IProject project) < if (project == null) return; Update(project); >public void Update(IProject project) < Name = project.Name; Estimate = project.Estimate; Actual = project.Actual; >private void UpdateEstimateStatus()

Здесь мы добавили конструктор, в который передается прототип интерфейса IProject и в котором происходит обновление за счет вызова метода Update(), который легко обновляет ProjectViewModel и детали проекта в представлении. Вспомогательный метод UpdateEstimateStatus() реализует дополнительную логику сравнения фактической стоимости проекта со сметной.

ProjectsViewModel

Первым делом объявим перечисление Status, которое содержит три возможных значения: None — фактическая стоимость равна нулю, Good — фактическая стоимость меньше или равна сметной стоимости, Bad — фактическая стоимость соответственно превышает сметную:

public enum Status

Теперь добавим интерфейс IProjectsViewModel, реализующий INotifyPropertyChanged, содержащий ссылку на выбранный объект модели-представления IProjectViewModel и метод UpdateProject, обновляющий детали представления:

public interface IProjectsViewModel : INotifyPropertyChanged < IProjectViewModel SelectedProject < get; set; >void UpdateProject(); >

Теперь добавим сам класс модель-представления ProjectsViewModel:

using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Windows.Input; using ProjectBilling.DataAccess; namespace ProjectBilling.Application < public class ProjectsViewModel : Notifier, IProjectsViewModel < public const string SELECTED_PROJECT_PROPERRTY_NAME = "SelectedProject"; private readonly IProjectsModel _model; private IProjectViewModel _selectedProject; private Status _detailsEstimateStatus = Status.None; private bool _detailsEnabled; private readonly ICommand _updateCommand; public ObservableCollectionProjects < get < return _model.Projects; >> public int? SelectedValue < set < if (value == null) return; Project project = GetProject((int)value); if (SelectedProject == null) < SelectedProject = new ProjectViewModel(project); >else < SelectedProject.Update(project); >DetailsEstimateStatus = SelectedProject.EstimateStatus; > > public IProjectViewModel SelectedProject < get < return _selectedProject; >set < if (value == null) < _selectedProject = value; DetailsEnabled = false; >else < if (_selectedProject == null) < _selectedProject = new ProjectViewModel(value); >_selectedProject.Update(value); DetailsEstimateStatus = _selectedProject.EstimateStatus; DetailsEnabled = true; NotifyPropertyChanged( SELECTED_PROJECT_PROPERRTY_NAME); > > > public Status DetailsEstimateStatus < get < return _detailsEstimateStatus; >set < _detailsEstimateStatus = value; NotifyPropertyChanged("DetailsEstimateStatus"); >> public bool DetailsEnabled < get < return _detailsEnabled; >set < _detailsEnabled = value; NotifyPropertyChanged("DetailsEnabled"); >> public ICommand UpdateCommand < get < return _updateCommand; >> public ProjectsViewModel(IProjectsModel projectModel) < _model = projectModel; _model.ProjectUpdated += model_ProjectUpdated; _updateCommand = new UpdateCommand(this); >public void UpdateProject() < DetailsEstimateStatus = SelectedProject.EstimateStatus; _model.UpdateProject(SelectedProject); >private void model_ProjectUpdated(object sender, ProjectEventArgs e) < GetProject(e.Project.ID).Update(e.Project); if (SelectedProject != null && e.Project.ID == SelectedProject.ID) < SelectedProject.Update(e.Project); DetailsEstimateStatus = SelectedProject.EstimateStatus; >> private Project GetProject(int projectId) < return (from p in Projects where p.ID == projectId select p).FirstOrDefault(); >> >

Итак, давайте разберем по-порядку этот код. ProjectsViewModel унаследован от Notifier, это добавляет поддержку интерфейса INotifyPropertyChanged (без его явной реализации), включая уведомления о изменениях.

Переменная _model содержит ссылку на модель ProjectsModel, при этом будет создана только одна модель, общая для всех модель-представлений. Коллекция Projects просто содержит ссылку на IProjectsModel.Projects. Свойство SelectedValue привязывается в представлении к номеру выбранного элемента в ComboBox, т.е. ComboBox.SelectedIndex. При этом в самом свойстве обновляется другое свойство SelectedProject и обновляется статус через свойство DetailsEstimateStatus.

Свойство SelectedProject является экземпляром IProjectViewModel и обеспечивает состояние представления в зависимости от различных условий. Свойство DetailsEstimateStatus возвращает текущий статус проекта, используя перечисление Status. DetailsEnabled — логическое свойство, определяющее будут ли включены детали проекта в представлении (два текстовых поля и кнопка Update). DetailsEstimateStatus и DetailsEnabled используются при объявлении SelectedProject, при этом, если в SelectedProject передается null детали блокируются.

UpdateCommand — свойство, реализующее интерфейс ICommand, связывающее событие клика по кнопке Update в представлении с обновлением модели.

В конструкторе класса ProjectsViewModel инициализируется модель через переменную _model, присоединяется обработчик события IProjectsModel.ProjectUpdated и создается экземпляр класса UpdateCommand, который будет показан чуть позже.

Метод UpdateProject() используется в классе UpdateCommand для обновления модели. model_ProjectUpdated() — обработчик событий, который будет вызываться в ответ на событие IProjectsModel.ProjectUpdated. Этот обработчик сначала обновляет коллекцию Projects, а затем будет изменять свойство SelectedProject, если его ID такой же, как и у обновленного проекта.

Давайте добавим класс UpdateCommand:

internal class UpdateCommand : ICommand < private const int ARE_EQUAL = 0; private const int NONE_SELECTED = -1; private IProjectsViewModel _vm; public UpdateCommand(IProjectsViewModel viewModel) < _vm = viewModel; _vm.PropertyChanged += vm_PropertyChanged; >private void vm_PropertyChanged(object sender, PropertyChangedEventArgs e) < if (string.Compare(e.PropertyName, ProjectsViewModel. SELECTED_PROJECT_PROPERRTY_NAME) == ARE_EQUAL) < CanExecuteChanged(this, new EventArgs()); >> public bool CanExecute(object parameter) < if (_vm.SelectedProject == null) return false; return ((ProjectViewModel)_vm.SelectedProject).ID >NONE_SELECTED; > public event EventHandler CanExecuteChanged = delegate < >; public void Execute(object parameter) < _vm.UpdateProject(); >>

В конструкторе класса UpdateCommand инициализируется переменная _vm содержащая ссылку на IProjectsViewModel и добавляется обработчик события vm_PropertyChanged. Здесь проверяется источник, вызвавший команду, IComand.CanExecuteChanged укажет источник команды и вызовет ICommand.CanExecute().

CanExecute() — метод, реализацию которого требует ICommand, вызывается по команде от источника, чтобы определить, может ли он в данный момент выполнить нужную команду. Представление обычно отключает себя, когда ICommand.CanExecute() возвращает false, а в нашем случае это используется, чтобы контролировать, когда UpdateButton будет включена или отключена.

Событие CanExecuteChanged и метод Execute() также являются частью реализации ICommand. В Execute() мы просто инициализируем вызов ProjectsViewModel.UpdateProject.

Графический интерфейс

Теперь мы создадим графический интерфейс приложения WPF, использующего эту модель и модель-представления. Мы будем использовать главное окно приложения (в моем случае MainWindow.xaml) как фабрику, для запуска нескольких дополнительных окон, которые и реализуют представление. Добавьте в проект окно ProjectsView.xaml:

          " ItemsSource="" DisplayMemberPath="Name" SelectedValuePath="ID" />          " Text="" Style="" /> " Text="" /> " />    
using System.Windows; namespace ProjectBilling.UI.WPF < public partial class ProjectsView : Window < public ProjectsView() < InitializeComponent(); >> >

Если вы только начинаете использовать MVVM вас может удивить отсутствие кода в частичном классе, реализующем представление. На самом деле это является следствием использования MVVM, т.к. бизнес-логику приложения мы концентрируем в модели, взаимодействие представления и модели в модель-представлениях, а само представление использует привязки и команды за счет синтаксиса XAML.

Давайте рассмотрим структуру этого окна подробнее, разбив ее на ключевые элементы:

  • ComboBox — позволяет пользователю выбирать тип проекта. Свойство ItemsSource привязывается к коллекции ProjectsViewModel.Projects, при этом DisplayMemberPath устанавливается в Name (т.е. в раскрывающемся списке будет отображаться название проекта), а SelectedValuePath в Id — выделенный элемент возвращает не сам объект Project, а только его идентификатор. SelectedValue привязывается к одноименному свойству модель-представления ProjectsViewModel, при этом обратите внимание на тип привязки — OneWayToSource, т.е. используется односторонняя привязка, от ComboBox к SelectedValue.
  • Детали представления реализованы в виде двух текстовых полей, отражающих сметную и фактическую стоимости проекта. Эти детали будут автоматически обновляться при выборе другого типа проекта в ComboBox.
  • Кнопка Update использует свой набор команд через привязку к ProjectsViewModel.UpdateCommand.

Обратите внимание, что в окне также добавлена некоторая логика для представления, реализованного в наборе триггеров стиля EstimateStyle. Условия триггеров привязываются к возможным значениям перечисления Status и задают различный цвет шрифта в текстовых полях, в зависимости от значений фактической и сметной стоимостей.

Теперь добавим небольшую логику в окне MainWindow.xaml для фабричного создания окон ProjectsView:

public partial class MainWindow : Window < private IProjectsModel _projectModel; public MainWindow() < InitializeComponent(); _projectModel = new ProjectsModel( new DataServiceStub()); >private void ShowProjectsButton_Click(object sender, RoutedEventArgs e) < ProjectsView view = new ProjectsView(); view.DataContext = new ProjectsViewModel(_projectModel); view.Owner = this; view.Show(); >>

Этот код непосредственно не относится к реализации MVVM, а просто служит для вспомогательных целей.

Запустив наше приложение и открыв несколько окон ProjectsView, выберите в них одинаковый проект, например «Jones», установите для него фактическую стоимость и нажмите кнопку Update. Вы увидите, что обновятся данные и в других окнах:

Запуск приложения MVVM

Данное поведение достигается тем, что обновление одного представления вызывает обновление модели, которая, в свою очередь, обновляет все другие представления.

Преимущества MVVM

Итак, рассмотрев довольно большой пример приложения MVVM, возникает резонный вопрос, а зачем этот паттерн вообще использовать, если того же результата можно было добиться «малой кровью»? Я приведу несколько доводов в пользу MVVM:

Тестируемость MVVM-приложений

Приложения, разработанные с использованием MVVM, обладают очень хорошим основанием для проведения модульного тестирования с целью проверки работы отдельных классов и методов.

Меньшее количество кода

Объем кода, необходимого для управления представлением немного снижается при использовании MVVM, а это означает, что снижается риск допустить ошибки и уменьшается код для написания модульных тестов.

Улучшенное проектирование приложений

Разработчики и дизайнеры могут самостоятельно работать над разными частями приложения. Как вы видели на примере, представление генерируется в XAML-разметке и использует базовый синтаксис привязок и команд, для взаимодействия с модель-представлением. Вы можете создать модель-представление, которое предоставляет необходимые точки входа для связывания с представлением (например, общедоступные свойства), которые в конечном представлении можно будет легко привязать. Это позволяет дизайнерам работать над внешним видом приложения, а программистам над бизнес-логикой приложения.

Легкость понимания логики представления

MVVM предусматривает хорошо организованную и легкую для понимания конструкцию построения графического интерфейса за счет использования механизмов привязок, команд и шаблонов данных.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *