Обзор использования Silverlight Prism. Часть 1. Теоритическая
1. Введение.
При разработке клиентского приложения на Silverlight сталкиваешься с проблемой организации его архитектуры и взаимодействия отдельных блоков в нем. Эта структура должна быть понятной и легко настраиваемой в ходе всего жизненного цикла приложения. Проект Prism также известный как Composite Application Guidance предоставляет широкий круг инструментов для выстраивания архитектуры приложения уровня предприятия (enterprise level).
Проект Prism доступен в виде исходного кода по лицензии “MICROSOFT PATTERNS & PRACTICES LICENSE” по адресу http://compositewpf.codeplex.com/.
Текущая версия Prism 2.2 (for Silverlight 4) — May 2010 Release, но есть более новая версия Prism v4 Drop 10 в стадии beta. Текущий обзор основан на стабильной версии проекта.
Использование набора инструментов Prism позволяет организовать приложение как группу независимых модулей. Каждый модуль может быть расширен и изменен, не зависимо от остальных. Каждый модуль легко поддается тестированию из-за активного использования паттерна Inversion of Control. Простая реализация загрузчика позволяет регистрировать базовые сервисы, загружать модули в необходимом порядке и настраивать зависимости одних модулей от других и многое другое. Пользовательский интерфейс основан на регионах, в которые внедряются виды различных модулей по необходимости. Это дает большую гибкость в компоновке визуальных элементов приложения. Но обо всем по порядку.
2. Обращение контроля (Inversion of Control). Unity.
Базовым понятием для организации расширяемости приложения является паттерн Inversion of Control (IoC). Этот шаблон проектирования является связывающим элементом для всех блоков приложения в Prism. Одной из основных форм этого паттерна является Dependency Injection (DI) и ее используемая в Prism реализация – Unity. Проект Unity Application Block (http://unity.codeplex.com/) это отдельное решение независимое от Prism, но используемый в нем. В последней версии Prism v4 появилась новая реализация IoC на основе Managed Extensibility Framework.
a. Внедрение зависимости (Dependency Injection).
Данный шаблон проектирования позволяет создавать объекты и разрешать зависимости для них. То есть если класс зависит от других классов, то экземпляры этих классов будут подставлены контейнером DI. В Prism используется реализация DI на основе проекта Unity. При необходимости реализацию DI можно подменить любой другой.
В Unity существует два способа определения зависимостей объекта:
Через конструктор. В конструкторе объекта перечисляются внешние объекты, от которых зависит экземпляр (это могут быть классы, но чаще всего интерфейсы).Через свойства. С помощью атрибута DependencyAttribute размечают свойства экземпляр, которые должны быть выставлены DI в ходе внедрения зависимостей.
Основными преимуществами является простата тестирования классов создаваемых с помощью DI контейнера, так как класс чаще всего зависит от интерфейсов, а классы, реализующие эти интерфейсы, легко подметить с целью тестирования. Так же набор зависимостей можно легко менять и это не приводит к изменению в коде в местах создания экземпляра класса.
b. Обнаружитель сервисов (Service Locator).
Паттерн Service Locator решает те же задачи что и DI, но немного иначе. Он позволяет получать классам доступ к сервисам, не давая знать, кто и как реализует эти сервисы. Его часто используют как альтернативу DI, бывают случаи, когда необходим именно этот шаблон проектирования. Например, когда нужно получить множество реализаций сервиса. В качестве реализации SL также выступает Unity.
3. Загрузчик (Bootstrapper).
Первое о чем необходимо озаботится при создании приложения на основе Prism это создание наследника от UnityBootstrapper. Класс UnityBootstrapper содержит в себе логику регистрации сервисов используемых Prism, загрузки и инициализации модулей, менеджеров регионов и т.д. Тут же создается и экземпляр UnityContainer, который выступает в качестве реализации IoC.
В последних версиях Prism был добавлен абстрактный класс Bootstrapper на основе, которого можно создавать своих наследников построенных на основе других реализаций IoC. Однако и в версии 2.2 можно создать свою версию этого класса, так как загрузчик лишь агрегирует те действия, которые нужно выполнить при запуске приложения, и их можно выполнить самому не используя этот класс.
4. Модульность (Modularity).
Все приложение на основе Prism состоит из набора независимых модулей, которые располагаются в отдельных сборках. Такой способ организации дает целый ряд преимуществ: предоставляет высокую степень независимости блоков, позволяет отдельным командам разрабатывать отдельные блоки, позволяет развиваться модулям независимо, дает высокую степень гибкости при изменении приложения.
Каждый модуль обычно помещается в отдельную сборку. Прежде всего, необходимо создать реализацию для интерфейса IModule. В методе класса модуля Initialize можно регистрировать сервисы, используемые в модуле. Здесь же с помощью сервиса IRegionManager в регионы внедряются виды, используемые модулем. Могут быть добавлены методы-обработчики выполнения глобальных команд.
Модуль в ходе запуска проходит следующий стадии:
Список модулей определяется в IModuleCatalog и используется в IModuleManager.IModuleManager управляет загрузкой модулей на основе описания модулей.Он же после загрузки модуля создает IModuleInitializer и вызывает метод модуля Initialize.
Один из значительных преимуществ, которые предоставляет конфигурация модулей это возможность загрузки по необходимости отдельных модулей. Данная функция очень востребована для больших web приложений с богатым интерфейсом (RIA).
Модульная организация приложения, конечно, не отменяет наличие общих сборок доступных в большинстве модулей. В них обычно выносится инфраструктура приложения: общие контролы, конверторы, классы событий, общие интерфейсы сервисов, глобальные команды, ресурсы и т.д.
5. Менеджер регионов (Region Manager).
Еще одним сервисом, регистрируемым в загрузчике, является IRegionManager. Данный сервис реализует механизм композиции интерфейса пользователя. В оболочке(shell) с помощью присоединенных свойств делается разметка регионов. Далее на основе именованных регионов происходит связывание с ними видов(“view”) различных модулей.
a.Оболочка (Shell).
Shell обычно создается в основном модуле приложения, рядом с классом загрузчика. В XAML разметке добавляются элементы, которые станут контейнерами для видов модулей – регионами.
Регионами могут выступать следующие классы:
System.Windows.Controls.ContentControl – позволяет отображать только один вид, вставка нового заменяет старый;System.Windows.Controls.ItemsControl – позволяет размещать виды один за другим;System.Windows.Primitives.Selector – также как и предыдущий может отображать множество видов.
А также все наследники вышеперечисленных классов, например, System.Windows.Controls.TabControl, вид помещается в отдельную закладку(TabItem). Различные элементы-контейнеры определяют, каким образом будут появляться виды модулей, добавленные в регион.
b. Исследование вида (View Discovery).
Данный способ связывания региона и вида является более простым, но не всегда применимым. Для этого используется сервис IRegionManager, он создает новый экземпляр вида и вставляет в регион с указанным именем. Созданием экземпляра вида и добавлением его в регион управляет IRegionManager.
c. Внедрение вида (View Injection).
Данный способ предпочтителен в ряде случаев:
Необходимо программно управлять моментом добавления и удаления вида из региона.Добавление в регион экземпляров вида одного типа, но с различными данными.Определение того, в какой экземпляр региона добавлен вид. В случае вложенных регионов.
Для этого из коллекции IRegionManager.Regions по имени получают экземпляр региона IRegion и добавляют, удаляют и/или активируют вид.
5.Взаимодействие модулей (Communication).
В связи с модульной организацией приложения и их высокой степенью независимости друг от друга возникает вопрос о том, как передать информацию между блоками. Например, в случае, когда в регионе слева отображается список неких объектов, а в регионе справа детальная информация. Если виды добавляются в левый и правый регионы разными модулями, то подписаться на событие изменения выбранного элемента в списке слева, чтобы изменить данные в детальной информации справа, не представляется возможным. Далее описывается 4 способа передачи данных между модулями. По сути, все они сводятся к общему классу доступному модулям обменивающимся данными.
a. Команды (Commanding).
Команды удобно использовать, когда необходимо реагировать на действия пользователя (нажатие кнопки, выбор пункта меню и т.д.) и когда доступность этого действия должна определяться бизнес логикой.
Библиотеки Prism предоставляют два класса реализующих интерфейс ICommand. Класс DelegateCommand, который позволяет вызвать метод делегата, когда выполняется команда. Класс CompositeCommand, который позволяет объединить несколько команда. Когда выполняется композитная команда, выполняются и все дочерние команды. Также доступность композитной команды зависит от доступности всех ее дочерних команд.
Классы команд в первую очередь позволяют выполнять необходимые действия в бизнес логике, не подписываясь на события элементов пользовательского интерфейса, а посредствам связывания данных (binding).
Кроме всего прочего CompositeCommand можно использовать для взаимодействия модулей. Для этого в классе доступном взаимодействующим модулям объявляется глобальное свойство с экземпляром композитной команды (например, Save, Load, Open и т.д.) в качестве значения. Далее модули могут регистрировать свои дочерние команды в объявленной композитной команде и выполнять свои методы при выполнении композитной команды.
Важным моментом для Silverlight является то, что в связывании данных нельзя использовать глобальные свойства (static). Однако это ограничение легко обходится созданием обертки со свойством (уже не глобальным), которое обращается к глобальной команде.
b.Агрегирование событий (Event Aggregator).
Если нужно передать событие между модулями и нет необходимости вернуть ответ, то удобнее всего использовать класс EventAggregator.
Данный класс поддерживает как множество мест вызова события, так и множество мест обработки события.
Для того чтобы воспользоваться этим средством связи нужно в общей сборке создать новый класс события, наследника CompositePresentationEvent. Тип Т определяет тип параметра передаваемого при вызове события в обработчик.
Реализация IEventAggregator регистрируется в ходе запуска загрузчика, поэтому экземпляр этого класс будет передан в ходе внедрения зависимостей или с помощью обнаружителя сервисов.
У сервиса IEventAggregator получают экземпляр события и производят подписку на событие или публикацию события (методы Subscribe и Publish соответственно).
a.Контекст регионов (Region Context).
С Prism мы можем использовать RegionContext, чтобы передавать данные между видом, содержащим регионы, и видами, добавленными в регион.
Задать контекст в разметке XAML можно с помощью присоединенного свойства RegionManager.RegionContext, также как имя региона.
Другой вариант, в коде получив по имени региона экземпляр IRegion из коллекции RegionManager.Regions можно задать значение свойства Context (также можно получить значение). Экземпляр IRegion имеет событие PropertyChanged, которое можно использовать для отслеживания изменения значения свойства Context.
Еще один способ это использовать статический метод GetObservableContext класса RegionContext. В качестве параметра метода выступает экземпляр вида. Метод возвращает объект типа ObservableObject и через его свойство Value можно получить значение RegionContext для заданного вида.
b. Общие сервисы (Shared Services).
В случае если ни один из описанных выше способов не удовлетворяет требованиям можно использовать следующий механизм. В сборке доступной взаимодействующим модулям создается сервис. Этот сервис должен предоставлять событие, подписавшись на которое любой модуль мог реагировать на изменившиеся в сервисе данные.
Модули не имеют представления, как реализован сервис, так как обращаются к интерфейсу. О конкретной реализации знает только обнаружитель сервисов в момент регистрации сервиса. При этом реализация может находиться в любом модуле или в общей сборке, главное чтобы регистрация этого сервиса происходила до обращения к его членам.
7. Паттерн MVVM (Model – View – ViewModel).
Процесс создания Silverlight приложения на основе Prism сам по себе не обязывает использовать какой-то конкретный паттерн разделения на данные, логику и представление. Можно с одинаковым успехом использовать шаблоны проектирования MVC, MVP, MP или MVVM.
Однако в последнее время шаблон Model-View-ViewModel завоевывает все большую популярность в среде разработки приложений на WPF, Silverlight и Windows Phone 7. Это обусловлено, прежде всего, возможностями и целями разделения кода и разметки XAML. Разметка XAML призвана формировать только внешний вид и часть поведения визуальных элементов, причем заниматься этим может не разработчик, а дизайнер по средствам инструмента Expression Blend.
Проект Prism последней версии уже имеет набор примеров с реализацией этого шаблона и ряд часто используемых с этим паттерном функций. В дальнейшем функционал будет явно расширен.
Отображение данных в паттерне MVVM идет за счет привязки к свойствам экземпляра ViewModel, причем ViewModel не имеет зависимостей от View. Немаловажную роль играют конверторы, которые позволяют преобразовывать данные в момент привязывания.
Выполнение изменений в модели в ответ на действия пользователя производятся за счет привязки к командам. Привязка данных может быть как односторонней (статичные надписи), так и двусторонней (поля ввода), когда изменения во View отражаются на ViewModel.
Все изменения в Model производятся классом ViewModel, поэтому обычно он имеет зависимость от сервиса Model. Обратный процесс, то есть изменения Model, также отражаются в ViewModel.
8. Работа с данными.
Руководство по разработке приложений на базе Prism не содержит многих аспектов разработки. В том числе не освещает процесс клиент-серверного взаимодействия. Однако когда вы реализовали сервис Model из шаблона MVVM вам необходимо создать класс реализации этого сервиса. В простейшем случае данные могут выгружаться на диск в виде файла на стороне клиента, но это обычно небольшие приложения и узкоспециализированные. В большинстве случаев вашему приложению необходимо обращаться к базам данных и различным сервисам. Эта тема очень обширная и достойна отдельной статьи, здесь я опишу только пару способов получения данных.
a. Сервисы Windows Communication Foundation (WCF services).
Основным средством создания сервисов в Silverlight являются WCF сервисы.
Коротко процесс создания сервиса представляет собой следующее. В проекте серверной части приложения необходимо добавить новый элемент “WCF Service” или “Silverlight-enabled WCF Service”, который уже включает корректные настройки для использования его Silverlight приложением. Далее в класс сервиса добавляются методы предоставляемые сервисом. Класс необходимо разметить атрибутами для создания контракта. В методах сервиса реализуется обращение к базе данных, другим сервисам и серверным ресурсам. В “web.config” нужно поместить настройки для корректной публикации сервиса, если их там еще нет.
При публикации сервиса может возникнуть проблема, если протокол, путь или порт сервиса отличается от тех, которые использует Silverlight приложение. Такое обращение является кросс-доменным вызовом(cross domain call) и, чтобы средства безопасности позволили это сделать, нужно поместить файл “clientaccesspolicy.xml” в корень серверной части, где расположен сервис.
Для того чтобы получить доступ к сервису на клиентской стороне необходимо создать proxy-класс для обращения к сервису. Большинство действий по его созданию берет на себя среда Visual Studio. В Silverlight проект добавляется Service Reference на созданный нами сервис или на любой другой сервис, к которому мы имеем доступ и хотим использовать. Далее создавая экземпляр proxy-класса можно обращаться к методам сервиса и получать данные. Стоит отметить, что в Silverlight обращения к сервисам идут только через асинхронные вызовы.
b. Сервисы WCF Rich Internet Application (WCF RIA services).
Данные сервисы поставляются в составе Silverlight 4 Tools. Эти сервисы упрощают разработку передачи данных из базы данных на сервере в клиентское приложение. Также RIA сервисы позволяют логику, описанную на сервере, сделать доступной на клиенте. Построены они на базе WCF сервисов, описанных ранее.
На стороне сервера получение данных может осуществляться на основе Entity Framework (EF), LINQ2SQL, NHibernate и т.д. Для примера, чтобы построить обращение на основе EF в проект добавляется EF модель. Она генерируется средой и позволяет получать данные из базы. Далее добавляется элемент DomainService. Это специальный WCF позволяющий делать запросы к модели, обновлять ее, проверять данные и т.д. После добавления DomainService на сервере, на клиенте генерируется класс DomainContext. Далее создавая экземпляр этого типа можно делать запросы и загружать данные из базы.
9. Заключение
Руководство по разработке на основе Prism конечно не панацея и не поможет абсолютно во всех случая. Однако инструменты и сервисы, используемые в ней, ускоряют разработку приложения. Модули, формирующие его, легко расширяются и тестируются. Настройка различных стадий работы системы довольно гибкая. Неприятные случаи, когда есть требование по изменению интерфейса, решаются довольно просто.
Еще одним большим удобством является поддержка multi-targeting, то есть разработка как под платформу Silverlight, так и под WPF. Большую часть кода можно сделать общим и изменения в одном проекте будут отражаться в другом.
Проект активно развивается и публикует новые версии. В последних версиях Prism появились библиотеки под новую развивающуюся мобильную платформу Windows Phone 7.
Мой блог находят по следующим фразам