четверг, 21 февраля 2008 г.

Model-View-ViewModel for WPF

В моем проекте весь UI построен на очень простом и одновременно очень мощном паттерне проектирования клиентских приложений DM-V-VM (DataModel-View-ViewModel), для реализации которого я написал простой framework (модное слово не удержался), с огромной помощью серии статей Dan'a Crevier(см. ссылку выше).
Основной технологией для разработки клиентских приложений в проекте был выбран WPF, в комбинации с Composite Application Block (CAB). В этом framework'е (CAB) активно используются и стимулируется разработчиком использование 2 паттернов для клиентских приложений: Model-View-Controller (M-V-C) и Model-View-Presenter (M-V-P). Мой же "Framework" является расширением и дополнением к CAB и SmartClient Software Factory (SCSF) - фабрики построенной на Composite Application Block), и использует часть ее функциональности, а в частности IoC-контейнер CAB для выведения зависимостей классов.

Паттерн DM-V-VM является частным случаем другого более общего паттерна написания UI кода - M-V-P, и в терминах Фаулера может называться PresentationModel. Есть с ним одна проблема - он шикарно вписывается в модель программирования с использованием WPF, но не позволит с легкостью переключиться на другую UI технологию, как это позволяет M-V-P (можно почитать у Dr.WPF). И, если вы готовы с этим мириться (а я очень даже готов, т.к. не планирую менять UI технологию), то эта модель в самый раз. Да, и, возможно, в сложных UI (например отображение и редактирование данных из многих, > 5 источников данных) поддержка ее также окажется черезчур сложной. Здесь я бы посоветовал использовать M-V-P, но на практике с такими случаями я пока не встречался.
Начну же препарировать данный паттерн.
DataModel(DM)

Классы, которые поставляют данные (бизнесс объекты). Их обязанность поставлять бизнес-объекты для отображения и редактирования в UI в WPF-дружественной манере (например, реализуя INotifyPropertyChanged). Все обращения к свойствам этих классов должны производиться в UI потоке и ни в коем случае не должны блокировать его.

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

На самом деле данные DM может получать из самых различных источников, что реализуется через делегирование общения с поставщиком данных Provider'у (можно было бы назвать и стратегией). Многое в реализации базового класса взято из DM Dan'a Crveier, поэтому не буду приводить здесь весь код, а сосредоточусь на усовершенствованиях его класса.

  1. Первое усовершенствование - введение базового интерфейса для DM и введение generic-интерфейса для наследников, параметризуемых Provider'ом.

  2. Введение типизированного провайдера и создание generic интерфейса DM с ограничением на поставляемый тип данных.


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

Введение generic интерфейса для Provider'а позволило написать generic класс DM параметризуемый интерфейсом поставщика и типом данных SEModel (SingleEntityModel SEDM), избавив тем самым от написания многих классов наследников, занимающихся только тем, что поставляют разные типы данных.



Обратите внимание, что в классе у свойства Entity присутствуют публичные get и set методы, а в интерфейсе только get. Это сделано для того, чтобы обеспечить возможность асинхронной установки значения свойства по его имени через PropertyDescriptor, инкапсуляция же обеспечивается через публикацию только лишь интерфейса ISEDM, где Entity - свойство только для чтения, т.е. клиенты взаимодействуют только через интерфейс, который может "инжектироваться" как зависимость в клиенты IoC-контейнером.
View

VM про класс View ничего не должен знать, и общается с ним через интерфейс IView. Как я уже упоминал выше, DM-V-VM не позволяет с легкостью менять UI технологию, не переписывая кода VM (так как в VM вынесены такие technology-specific понятия как WPF command), то я просто для упрощения дальнейшего кода ввел интерфейс IWPFView, в котором опубликовал специфичные для WPF свойства. Такое вот волевое решение. =)


ViewModel

И, наконец, главный участник троицы - ViewModel. Этот класс должен содержать всю логику UI. VM инстанцируется при создании View IoC-контейнером и выводит все его зависимости. Базовый класс ViewModelBase параметризуется интерфейсом View, а его наследник в дополнение еще параметризуется интерфейсом DM.


Эти классы вводят ряд полезных методов для перегрузки, вызываемых при установке/удалении вида/модели, при перехвате событий от модели о успешном обновлении/ об ошибке. Также VM при получении View подписывается на его события Loaded/Unloaded и предлагает виртуальные методы для перегрузки вызываемые при перехвате этих событий. В реализации по умолчанию VM в методе OnViewSet присваивает IView.DataContext указатель на себя, с тем, чтобы IView мог обновляться самостоятельно при перехвате событий от VM, DM и бизнес-объектов, поставленных DM. Таким образом в терминах Фаулера View является ActiveView.

На диаграмме классов также можно заметить класс ActionsViewModel. Этот класс содержит список CommandModels (также очень удобная конструкция от Dan'a Crevier инкапсулирующая WPF комманды), на каждую из комманд которого ActionsViewModel автоматически создает в View CommandBinding.

ViewModel в данном его виде не запрещает редактировать данные, поставленные DM и никаким образом не следит за валидностью и состоянием объекта (в процессе редактирования он просто может быть приведен в невалидное состояние, что затем вызовет цепочку ошибок). Во избежание этого в моем проекте все объекты редактируются в Wizard'ах, которые производят валидацию этих объектов и не дают сохранять объекты в промежуточном или невалидном состоянии (об этом в следующем посте). Если же есть необходимость редактировать бизнес-объект прямо в View, то рекомендую прочитать вот этот пост от Pete W.

DM-V-VM

Соберем теперь пример. Пусть у нас будет некий объект Foo, который будет поставляться SEModel (вот где пригодилась функциональность generic класса - не надо писать новый класс!) при помощи FooProvider.





Вся логика содержится в FooViewModel. Этот класс наследуется от ActionsViewModel и содержит 2 CommandModel: IncCommandModel и DecCommandModel. IncCommandModel увеличивает значение Foo.Bar, а DecCommandModel уменьшает. При загрузке View ViewModel просит DM обновиться.




Теперь очередь View.

В code-behind при создании View создается ViewModel.



И, поскольку ViewModel уже установил View.DataContext на себя, то можно в View поместить ContentControl, "забайндить" его свойство Content на DataContext (он используется поумолчанию, если в Binding не указан путь) и написать темплейт для рендеринга ViewModel.



И здесь мы столкнемся с единственным злом generic класса DM - binding далеко не всегда может найти у этого класса указанный путь, поэтому здесь мы явно при перехвате успешного обновления DM в FooViewModel сохраняем объект Foo в ресурсах View.



Да, если не использовать IoC-контейнер, то все [CreateNew] и [ServiceDependency] надо заменить на вызов конструкторов.

Вот, что у нас получилось:


Удачи.
PS: В ближайшем будущем выходит новый проект от PnP group - Prism. Он призван послужить заменой CAB в процессе разработки LOB приложений на WPF. Жду.
Upd: выложил код примера здесь.

5 комментариев:

Анонимный комментирует...

А как бы можно было увидеть полный исходник примера? А то я попытался сделать что-то подобное, но до пары моментов не могу доехать

RobertT комментирует...

в дополнении к посту я выложил исходники, вот ссылка: http://slil.ru/25678938

пишите, если что-то будет не ясно.
Удачи.

Анонимный комментирует...

А можно еще выложить исходники, если не затруднит? А то пишет, что файл не найден :(

Анонимный комментирует...

Да, очень бы хотелось поглядеть на исходники.

RobertT комментирует...

Все исходники выложил здесь: http://www.megaupload.com/?d=OEWPQ15D

проверьте последний пост =)

ссылку в этом посте поправлю