Как связать view и viewmodel wpf
Для работы с паттерном MVVM создадим новый проект. По умолчанию в проект добавляется стартовое окно MainWindow — это и будет представление. И теперь нам нужна модель и ViewModel.
Добавим в проект новый класс Phone, который и будет представлять модель приложения:
using System.ComponentModel; using System.Runtime.CompilerServices; namespace MVVM < public class Phone : INotifyPropertyChanged < private string title; private string company; private int price; public string Title < get < return title; >set < title = value; OnPropertyChanged("Title"); >> public string Company < get < return company; >set < company = value; OnPropertyChanged("Company"); >> public int Price < get < return price; >set < price = value; OnPropertyChanged("Price"); >> public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged([CallerMemberName]string prop = "") < if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop)); >> >
Для уведомления системы об изменениях свойств модель Phone реализует интерфейс INotifyPropertyChanged. Хотя в рамках паттерна MVVM это необязательно. В других конструкциях и ситуациях все может быть определено иначе.
Также добавим в проект новый класс ApplicationViewModel, который будет представлять модель представления:
using System.ComponentModel; using System.Runtime.CompilerServices; using System.Collections.ObjectModel; namespace MVVM < public class ApplicationViewModel : INotifyPropertyChanged < private Phone selectedPhone; public ObservableCollectionPhones < get; set; >public Phone SelectedPhone < get < return selectedPhone; >set < selectedPhone = value; OnPropertyChanged("SelectedPhone"); >> public ApplicationViewModel() < Phones = new ObservableCollection< new Phone < Title="iPhone 7", Company="Apple", Price=56000 >, new Phone , new Phone , new Phone >; > public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged([CallerMemberName]string prop = "") < if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop)); >> >
Это класс модели представления, через который будут связаны модель Phone и представление MainWindow.xaml. В этом классе определен список объектов Phone и свойство, которое указывает на выделенный элемент в этом списке.
В итоге веcь проект будет выглядеть следующим образом:

Далее изменим код нашего представления — файла MainWindow.xaml:
" /> " /> " /> " /> " /> " />
Здесь определен элемент ListBox, который привязан к свойству Phones объекта ApplicationViewModel, а также определен набор элементов, которые привязаны к свойствам объекта Phone, выделенного в ListBox.
И изменим файл кода MainWindow.xaml.cs :
using System.Windows; namespace MVVM < public partial class MainWindow : Window < public MainWindow() < InitializeComponent(); DataContext = new ApplicationViewModel(); >> >
Здесь достаточно установить контекст данных для данного окна в виде объекта ApplicationViewModel, который свяжет представление и модели Phone.
И если мы запустим приложение, то увидим список объектов. Мы можем выбрать один из них, и его данные появятся в полях справа:

При этом не надо определять код загрузки объектов в ListBox, определять обработчики выбора объекта в списке или сохранения его данных. За нас все делает механизм привязки данных.
Определение модели
В данном случае мы сами определяем модель Phone. Однако не всегда мы имеем возможность реализовать в используемой модели интерфейс INotifyPropertyChanged. Также, возможно, мы захотим предусмотреть отдельное представление (отдельное окно) для манипуляций над одной моделью (добавление, изменение, удаление). Подобное представление может иметь в качестве ViewModel объект модели Phone. И в подобных случаях мы можем создать отдельную ViewModel для работы с одним объектом Phone, наподобие:
using System.ComponentModel; using System.Runtime.CompilerServices; namespace MVVM < public class PhoneViewModel : INotifyPropertyChanged < private Phone phone; public PhoneViewModel(Phone p) < phone = p; >public string Title < get < return phone.Title; >set < phone.Title = value; OnPropertyChanged("Title"); >> public string Company < get < return phone.Company; >set < phone.Company = value; OnPropertyChanged("Company"); >> public int Price < get < return phone.Price; >set < phone.Price = value; OnPropertyChanged("Price"); >> public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged([CallerMemberName]string prop = "") < if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(prop)); >> >
C# WPF MVVM связь ViewModel — Model и ViewModel — ViewModel
У меня есть примерно такой View окна
он состоит из 3 регионов: File Manager, Workspace и Menu. Мне нужно сделать добавление файла с помощью кнопки Menu в File Manager, View окна добавления очень простой
TextBox Name должен проверять существует ли такой файл и Border должен ставать красным если существует. Я должен сделать это строго по MVVM
и собственно говоря столкнулся с 2 вопросами: Как я понимаю Add File View Model должна говорить Add File View существует ли файл с таким именем, т.е должна быть модель которая будет получать коллекцию имен и им’я c Add File View и проверять сходство. Здесь нужно взять имена с File Manager Model и отправить в Add File Model, как это сделать? Можно создать свойство в File Manager View Model и через Binding как параметр скормить коллекцию имен команде которая открывает окно добавления нового файла но не нарушает ли такой подход MVVM? 2 Вопрос о связи между ViewModel и Model, насколько я знаю Model может иметь свойства которые имплементуруют INotifyPropertyChanged и View может к ним делать Binding, но как на меня это превращает Model в View Model, рассматривая Model со свойствами которые не имплементуруют INotifyPropertyChanged эти свойства нужно обернуть в View Model, как Model должна уведомлять об изменении View Model? о Prism, EventAggregator знаю, меня интересует решение без фреймворка
Отслеживать
задан 25 дек 2019 в 11:11
Mike Waters Mike Waters
330 5 5 серебряных знаков 24 24 бронзовых знака
Возможно, это немного по теме.
27 окт 2021 в 10:16
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
Если вы писали по старинке без mvvm приложения то тогда поймете. VM это почти тоже самое как xaml.cs файл раньше в которым мы писали логику интерфейса и данных. Так же и здесь, в VM уходит логика интерфейса и некоторые данные если упрощать. Логику работы с данными тут уже нужно выносить. То есть как вы и думали можно сделать список List файлов прямо во VM и забиндить на него вашу stackpanel или какой вы там элемент используете. А Model это уже Класс ваших файлов и их свойства которые вы будете дальше использовать. Получается биндим к List .
Может термины где то я пишу не совсем правильные, но именно такую логику предоставляют гайды по mvvm. В частности тот что на хабре. То есть те элементы которые мы ходим менять во View пишем во ViewModel? а потом передаем дальше измененные данные в другие классы/методы для работы.
Отслеживать
ответ дан 25 дек 2019 в 11:32
193 11 11 бронзовых знаков
1) В ответе на 2 вопрос я вам написал, что ViewModel должна давать представлению коллекцию объектов (ObservableCollection), которые представляют собой файл. Для вашего функционала с добавлением файла достаточно всего 1 команды, которая проверяет, есть ли в коллекции такой элемент, если есть, то устанавливает у него созданное вами свойство (булевское), которое биндится на Visibility бордера и преобразовывается из bool в Visibility. А если нет, то добавляет новую модель в ObservableCollection и она автоматически появляется в списке. Также не забудьте про метод CanExecute. Тут все очень просто, если значение в поле пустое, то нельзя добавить файл. Что касательно открытие нового окна. То здесь подход очень простой. Только View может создавать View. Код создания нового View нужно писать во вью. Можете писать декларативно, можете в CodeBehind.
2) Модель может реализовывать INotifyPropertyChanged. Скажу больше, это абсолютно нормально. Как реальный жизненный пример я вам могу привести в пример любую сущность из БД. Она с точки зрения MVVM является моделью. Вы не совсем правильно понимаете, что такое Model и ViewModel. ViewModel переводится как модель представления, т.е. это ФУНКЦИОНАЛЬНЫЕ ВОЗМОЖНОСТИ графического ИНТЕРФЕЙСА пользователя и в ней пишется код который реализует поведение интерфейса пользователя, а Model это реализация бизнес-логики (она же функциональность приложения). В вашем случае ViewModel должна возвращать коллекцию моделей (файлов) (лучше всего ObservableCollection), у которых есть свойство со значением имени, и это коллекция биндится на какой-нибудь контрол (например, ListBox с переопределенным шаблоном элемента). И на вашей картинке, которую вы привели, под состоянием модели имеется намного больше, чем простое изменение свойств. Вы не забывается, что под этими тремя квадратиками имеется ввиду 3 крупшейние части приложения (там может быть под 2к файлов, где, например, 800 файлов это View, 800 это ViewModel, а оставшиеся 400 это модели) и под состоянием модели может иметься ввиду значения кучи объектов одновременно.
Отслеживать
ответ дан 27 дек 2019 в 14:33
79 2 2 бронзовых знака
- c#
- wpf
-
Важное на Мете
Связанные
Похожие
Подписаться на ленту
Лента вопроса
Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.
Дизайн сайта / логотип © 2023 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2023.10.27.43697
Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.
Паттерн MVVM

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

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

Как видите представление теперь пустое (содержит только пользовательский интерфейс на XAML), вся логика строится на отношениях между моделью и модель-представлением.
Структура View Models
Нам понадобится два вида моделей для этого приложения. Каждая модель-представления имеет свои собственные задачи в представлении. Использование нескольких вложенных моделей называется иерархическим модель-представлением (hierarchical view model).
Модель-представление ProjectsViewModel в нашем приложении будет содержать состояние и логику представления ProjectsView, это показано на следующем рисунке:

Свойства привязки и их назначение описаны ниже:
- 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, а это означает, что снижается риск допустить ошибки и уменьшается код для написания модульных тестов.
Улучшенное проектирование приложений
Разработчики и дизайнеры могут самостоятельно работать над разными частями приложения. Как вы видели на примере, представление генерируется в XAML-разметке и использует базовый синтаксис привязок и команд, для взаимодействия с модель-представлением. Вы можете создать модель-представление, которое предоставляет необходимые точки входа для связывания с представлением (например, общедоступные свойства), которые в конечном представлении можно будет легко привязать. Это позволяет дизайнерам работать над внешним видом приложения, а программистам над бизнес-логикой приложения.
Легкость понимания логики представления
MVVM предусматривает хорошо организованную и легкую для понимания конструкцию построения графического интерфейса за счет использования механизмов привязок, команд и шаблонов данных.
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#
- ООП
- Промышленное программирование