Binding in View Models, подключите все!
Недавно я работал с клиентом над проблемой привязки данных в пользовательском интерфейсе WPF, который реализовывал шаблон MVVM (Model View View Model). МВВМ потрясающий когда все подключено правильно, и это может быть проблемой. В реализации шаблона много движущихся частей, и легко что-то упустить. Если вы это сделаете, все просто не будет работать правильно. Мне посчастливилось реализовать его во многих системах, создать библиотеки, которые помогают другим использовать его, а также написать статьи и руководства. Когда я был продакт-менеджером в Microsoft по шаблонам и практикам Prism, MVVM был центральной темой.
В этом посте я дам краткий обзор MVVM, а затем приведу пример, иллюстрирующий, как его использовать на практике. Пример вдохновлен проблемой, над которой я работал с клиентом.
Основная причина использования этого шаблона — реализация пользовательского интерфейса со сложной логикой в удобном для сопровождения виде. Это поможет вам отделить проблемы пользовательского интерфейса от бизнес-логики, ориентированной на пользовательский интерфейс. С MVVM ваш код будет легче тестировать, а ваши пользовательские интерфейсы будут менее хрупкими, по крайней мере, с точки зрения изменений в логике, не нарушающих пользовательский интерфейс, или наоборот.
Когда вы реализуете шаблон который также известен как Презентационная модель, существует ряд основных понятий. Также будут определенные нюансы и библиотеки, с которыми вам придется работать в зависимости от стека. В большей части этого поста я сосредоточусь на том, как это сделать в WPF.
Концепции
Вид — Пользовательский интерфейс. В случае WPF это может быть окно, фрейм или другой компонент.
Модель — Базовая модель данных, содержащая информацию, которая будет отображаться. Модели могут быть просто легкими контейнерами данных (анемичными) или иметь бизнес-методы. По этому поводу идет много религиозных дебатов, которых я буду избегать.
Посмотреть модель — Ориентированная на пользовательский интерфейс модель данных, к которой непосредственно привязывается пользовательский интерфейс. Он содержит элементы/логику, специфичные для контекста пользовательского интерфейса. Не смешивайте View Model и Model, это вызовет у вас проблемы. Модель представления отвечает за предоставление данных, которые будут отображаться в представлении. Он будет обращаться к базовым данным и форматировать их так, как это имеет смысл для пользовательского интерфейса. Он будет содержать логику, которая реагирует на события пользовательского интерфейса. Например, модель представления может иметь метод Refresh(), который вызывается пользовательским интерфейсом для обновления данных. Модель представления также обеспечивает инкапсуляцию. Это не позволяет пользовательскому интерфейсу иметь какие-либо интимные детали или связь с базовой инфраструктурой, а также предотвращает просачивание деталей пользовательского интерфейса.
Привязка данных — Это клей, который позволяет представлению подключаться к модели представления в WPF. Другие платформы, такие как React, имеют свои собственные механизмы состояния и привязки. Одна и та же концепция, хотя она может разыгрываться по-разному.
Интерфейсы и типы
В WPF есть несколько различных интерфейсов и типов, о которых вам нужно знать для реализации MVVM.
INotifyPropertyChanged — Этот интерфейс (сокращенно INPC) реализуется моделью представления, чтобы уведомить компонент пользовательского интерфейса об изменении свойства. Он содержит событие PropertyChanged, которое запускается. Если значение реквизита является коллекцией или сложным объектом, то событие запускается, когда реквизит обновляется новым экземпляром.
INotifyCollectionChanged — Этот интерфейс (сокращенно INCC) реализуется моделью представления, чтобы сообщить компоненту пользовательского интерфейса, что свойство коллекции имеет элементы, добавленные или удаленные из списка. Событие CollectionChanged срабатывает как часть этого.
ObservableCollection — Этот коллекция реализует INotifyCollectionChanged и запускает событие при его изменении. Эта коллекция очень удобна, так как может обернуть существующую коллекцию. Вы будете часто использовать его при создании пользовательских интерфейсов. По сути, это модель представления.
Давайте представим себе систему управления контактами (все, кто знает меня по моим дням работы с ASP.NET Web API PM, сейчас будут смеяться). Он имеет экран, на котором отображается список контактов. Вы можете фильтровать контакты на основе имени контакта. Он позволяет добавлять, удалять и т. д. Вы также можете отображать отдельные сведения о контакте.
Поговорим о компонентах. Я сосредоточусь конкретно на моделях представлений и предоставлю читателю возможность реализовать представление.
Ниже приведена схема, иллюстрирующая различные части. Отказ от ответственности: я кодер, а не дизайнер. 🤣
Вид
ContactListView
Это отображает список контактов. Он имеет текстовый элемент управления для указания фильтра. Список отображает имя, фамилию и телефон для каждого контакта. Также есть кнопки «Сохранить» и «Отменить».
ViewModel — у нас будет 2 соответствующие модели.
КонтактViewModel
Эта модель представления предназначена для поддержки отображения отдельного контакта и обеспечения всплывающих окон об изменениях.
public class ContactViewModel : INotifyPropertyChanged {
private Contact _contact
public ContactViewModel(Contact contact) {
_contact = contact;
}
public string OnPropertyChanged(string property) {...}
public First {
get { return _contact.First; }
set {
_contact.First = value;
OnPropertyChanged("First");
}
}
}
Контактлиствиевмодель
Модель представления предназначена для бизнес-логики пользовательского интерфейса представления списка контактов. Кнопки «Список» и «Сохранить» и «Отменить» привязаны к файлу this. Обратите внимание, как он обращается к службе данных для получения контактной информации.
public class ContactListViewModel : INotifyPropertyChanged {
private IContactData _data;
public ContactListViewModel(IContactService data) {
_data = data;
}
public string OnPropertyChanged(string property) {...}
private ObservableCollection<ContactViewModel> _contacts;
public ObservableCollection<ContactViewModel> Contacts {
get {
return _contacts;
}
set {
_contactList = value;
OnPropertyChanged("Contacts");
}
}
private string _filter;
public string Filter {
get {
return _filter;
}
set {
_filter = value;
OnPropertyChanged("Filter");
Contacts = _data.GetContacts(value).select((x)=>new ContactViewModel(x));
}
}
public void Save() {
_data.Save();
}
public void Undo() {
if (_filter == "") {
Contacts = Contacts = _data.GetContacts(value).select((x)=>new ContactViewModel(x));
}
else {
Contacts = Contacts = _data.GetContacts().select((x)=>new ContactViewModel(x));
}
}
public void Load() {
Contacts = _data.GetContacts().select((x)=>new ContactViewModel(x));
}
}
Модель
Модель представляет собой очень простой контейнер данных.
public class Contact {
public int ID {get;set;}
public string First {get;set;}
public string Last {get;set;}
public string Phone {get;set;}
}
Данные
ContactService отвечает за получение контактов. Он реализует интерфейс, позволяющий легко имитировать/внедрять его контейнером IoC.
public interface IContactService {
IQueryable<Contact> GetContacts()
IQueryable<Contact> GetContacts(string filter)
}
public class ContactService : IContactService {
IQueryable<Contact> GetContacts() {
...
}
IQueryable<Contact> GetContacts(string filter) {
...
}
public void Save() {
...
}
}
Как это все работает
Теперь, когда мы описали все компоненты и все правильно распаяно, давайте поговорим о том, как это все работает.
Загрузка данных
- Представление создается.
-
ContactListViewModel
построен сContactService
инъецированный -
Load()
метод вызывается для заполнения данных. Он призывает кContactService
- Полученные данные упаковываются в
ObservableCollection(ContactViewModel)
используяSelect()
для создания моделей представления. -
PropertyChanged
событие запускается, чтобы сообщить представлению, чтоContacts
свойство изменилось. - Пользовательский интерфейс отображает обновленные контакты и начинает отслеживать
CollectionChanged
события. Пользовательский интерфейс также привязывается к каждомуContactViewModel
экземпляр в коллекции для отслеживания событий `PropertyChanged’ для каждого контакта.
Фильтрация данных
- Свойство фильтра обновляется в пользовательском интерфейсе.
- Привязка данных обновляет
Filter
собственность наContactListViewModel
- Модель представления вызывает
ContactService.GetContacts
метод, проходящий через фильтр. - Результат заворачивается так же, как и при
Load()
.
Сохранение данных
ContactService.Save()
вызывается. Здесь много махинаций, так как я оставил реализацию на усмотрение читателя, например, использование Entity Framwork.
Отменить
-
ContactService
вызывается для перезагрузки данных.
Возвращаясь к началу этого поста, есть ряд вещей, которые необходимо правильно подключить. Вот несколько вещей, которые нужно помнить, которые могут помочь вам в реализации.
- Представление всегда должно вызывать ViewModel для логики. Он никогда не должен вызывать ни один из базовых классов.
- Если вы предоставляете коллекцию объектов (например, список контактов), вам следует серьезно подумать о том, чтобы обернуть их в модель представления, чтобы пользовательский интерфейс обнаруживал изменения.
- Установщики модели представления часто используются для запуска логики. В случае с ContactManager мы использовали установщик свойства Filter, чтобы принудительно перезагрузить данные.
INotifyPropertyChanged
используется для уведомления пользовательского интерфейса при обновлении отдельных свойств. НаличиеOnPropertyChanged
метод является общим соглашением для запуска события. Это было вызвано в установщике для фильтров и контактов.INotifyCollectionChanged
используется для уведомления пользовательского интерфейса о добавлении или удалении элементов в коллекцию. Если сам экземпляр коллекции изменен, тоPropertyChanged
событие должно быть запущено, чтобы пользовательский интерфейс мог его обнаружить.- Точки останова и окно просмотра — ваши друзья. При попытке отладки MVVM поместите точки останова в методы и свойства кода MVVM, и вы сможете точно увидеть, что происходит.
Существует много шаблонов, таких как INPC, которые вы должны реализовать как часть шаблона MVVM. Несколько лет назад я был большим поклонником знаменитого Легкий инструментарий MVVM написанный моим другом Лораном Бюньоном. Хотя это больше не поддерживается, Microsoft выпустила Инструментарий MVVM. Он содержит вспомогательные классы, такие как ObserableObject<T>
которые реализуют INPC для вас и избавляются от некоторых шаблонов.
Шаблон MVVM — это фантастический шаблон для создания приложений с пользовательским интерфейсом, который имеет много преимуществ, включая упрощение поддержки и тестирования кода. Реализация MVVM может быть легко испорчена. Надеюсь, этот пост поможет вам, когда вы углубитесь в его использование.
Если вы боретесь с использованием MVVM, я здесь, обращайтесь!