Обзор использования Silverlight Prism. Часть 1. Теория.

Автор: Кирюшин Александр Иванович
Перевод: Гредасова Елена Николаевна
Источники: RSDN Magazine #4-2010
Блог компании Enterra, Inc.

Материал предоставил: Кирюшин Александр Иванович
Опубликовано: 20.01.2011.
Версия текста: 1.1
Введение
Инверсия управления (Inversion of Control). Unity
Внедрение зависимости (Dependency Injection)
Обнаружитель сервисов (Service Locator)
Загрузчик (Bootstrapper)
Модульность (Modularity)
Менеджер регионов (Region Manager)
Оболочка (Shell)
Исследование вида (View Discovery)
Внедрение вида (View Injection)
Взаимодействие модулей (Communication)
Команды (Commanding)
Агрегирование событий (Event Aggregator)
Контекст регионов (Region Context)
Общие сервисы (Shared Services)
Паттерн MVVM (Model – View – ViewModel)
Работа с данными
Сервисы Windows Communication Foundation (WCF services)
Сервисы WCF Rich Internet Application (WCF RIA services)
Заключение
Список литературы

Введение

При разработке клиентского приложения на Silverlight сталкиваешься с проблемой организации его архитектуры и взаимодействия отдельных блоков в нем. Структура приложения должна быть понятной и легко настраиваемой в ходе всего его жизненного цикла. Проект Prism, также известный как Composite Application Guidance, предоставляет широкий круг инструментов для выстраивания архитектуры приложения уровня предприятия (enterprise level).

Проект Prism доступен в виде исходного кода по лицензии “MICROSOFT PATTERNS & PRACTICES LICENSE” по адресу http://compositewpf.codeplex.com/.

Текущая версия Prism 2.2 (for Silverligh​t 4) - May 2010 Release, но есть более новая версия Prism v4 Drop 10 в стадии beta. Текущий обзор основан на стабильной версии проекта.

Использование набора инструментов Prism позволяет организовать приложение как группу независимых модулей. Каждый модуль может быть расширен и изменен независимо от остальных. Каждый модуль легко поддается тестированию благодаря активному использованию паттерна Inversion of Control. Простая реализация загрузчика позволяет регистрировать базовые сервисы, загружать модули в необходимом порядке, настраивать зависимости одних модулей от других, а также выполнять многие другие действия. Пользовательский интерфейс основан на регионах, в которые внедряются виды различных модулей по необходимости. Это дает большую гибкость в компоновке визуальных элементов приложения. Но обо всем по порядку.

Инверсия управления (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.


Внедрение зависимости (Dependency Injection)

Данный шаблон проектирования позволяет создавать объекты и разрешать зависимости для них. То есть если класс зависит от других классов, то экземпляры этих классов будут подставлены контейнером Dependency Injection (DI). В Prism используется реализация DI на основе проекта Unity. При необходимости реализацию DI можно подменить любой другой.

В Unity существует два способа определения зависимостей объекта:

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

Обнаружитель сервисов (Service Locator)

Паттерн Service Locator решает те же задачи, что и DI, но немного иначе. Он позволяет классам получать доступ к сервисам, не давая знать, кто и как реализует эти сервисы. Его часто используют как альтернативу DI. Бывают случаи, когда необходимо использовать именно этот паттерн проектирования, например, когда нужно получить множество реализаций сервиса. В качестве реализации SL также выступает Unity.

Загрузчик (Bootstrapper)

Первое, о чем необходимо позаботиться при создании приложения на основе Prism – это создание наследника класса UnityBootstrapper. Класс UnityBootstrapper содержит в себе логику регистрации сервисов, используемых Prism, загрузки и инициализации модулей и т.д. Тут же создается и экземпляр UnityContainer, который выступает в качестве реализации IoC.

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

Модульность (Modularity)

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


Каждый модуль обычно помещается в отдельную сборку. Прежде всего, необходимо создать реализацию для интерфейса IModule. В методе класса модуля Initialize можно регистрировать сервисы, используемые в модуле. Здесь же с помощью сервиса IRegionManager в регионы внедряются представления, используемые модулем. Могут быть добавлены методы-обработчики выполнения глобальных команд.

Загрузка модулей производится в несколько стадий:

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

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

Менеджер регионов (Region Manager)

Еще одним сервисом, регистрируемым в загрузчике, является IRegionManager. Данный сервис реализует механизм композиции интерфейса пользователя на основе именованных областей – регионов. В оболочке (shell) с помощью присоединенных свойств (Attached Property) делается соответствующая разметка. Далее на основе именованных регионов происходит связывание с ними представлений (“view”) различных модулей.


Оболочка (Shell)

Shell обычно создается в основном модуле приложения, рядом с классом загрузчика. В XAML-разметке добавляются элементы, которые станут контейнерами для представлений модулей, другими словами регионами.

Регионами могут выступать следующие классы:

А также все наследники вышеперечисленных классов, например, System.Windows.Controls.TabControl (вид помещается в отдельную закладку (TabItem)). Различные элементы-контейнеры определяют, каким образом будут появляться виды модулей, добавленные в регион.

Исследование вида (View Discovery)

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

Внедрение вида (View Injection)

Данный способ предпочтителен в ряде случаев:

Для этого из коллекции IRegionManager.Regions по имени получают экземпляр региона IRegion и добавляют, удаляют и/или активируют вид.

Взаимодействие модулей (Communication)

В связи с модульной организацией приложения и высокой степенью независимости модулей друг от друга возникает вопрос о передаче информации между модулями. Примером может служить случай, когда в регионе слева отображается список неких объектов, а в регионе справа – детальная информация. Если виды добавляются в левый и правый регионы разными модулями, то подписаться на событие изменения выбранного элемента в списке слева, чтобы изменить данные в детальной информации справа, не представляется возможным. Ниже описываются 4 способа передачи данных между модулями. По сути, все они сводятся к общему объекту, доступному модулям, обменивающимся данными.

Команды (Commanding)

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

Библиотеки Prism предоставляют два класса, реализующих интерфейс ICommand: класс DelegateCommand, который позволяет вызвать метод делегата, когда выполняется команда, и класс CompositeCommand, который позволяет объединить несколько команд. При выполнении композитной команды выполняются и все дочерние команды. Доступность композитной команды зависит от доступности всех ее дочерних команд.

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

Кроме всего прочего, CompositeCommand можно использовать для взаимодействия модулей. Для этого в классе, доступном взаимодействующим модулям, объявляется статическое (static) свойство с экземпляром композитной команды (например, Save, Load, Open и т.д.) в качестве значения. Далее модули могут регистрировать свои дочерние команды в объявленной композитной команде и выполнять свои методы при выполнении композитной команды.

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

Агрегирование событий (Event Aggregator)

Если нужно передать событие между модулями, и нет необходимости вернуть ответ, то удобнее всего использовать класс EventAggregator. Данный класс поддерживает как множество мест вызова события, так и множество мест обработки события.

Чтобы воспользоваться этим средством связи, нужно в общей сборке создать новый класс события, наследника CompositePresentationEvent<T>. Тип Т определяет тип параметра, передаваемого при вызове события в обработчик.

Реализация IEventAggregator регистрируется в ходе запуска загрузчика, поэтому экземпляр этого класс будет передан в ходе внедрения зависимостей или с помощью обнаружителя сервисов. У сервиса IEventAggregator получают экземпляр события и производят подписку на событие или публикацию события (методы Subscribe и Publish соответственно).

Контекст регионов (Region Context)

С Prism мы можем использовать RegionContext, чтобы передавать данные между видом, содержащим регионы, и видами, добавленными в регион. Задать контекст в разметке XAML можно с помощью присоединенного свойства RegionManager.RegionContext, так же, как имя региона.

Другой вариант – в коде, получив по имени региона экземпляр IRegion из коллекции RegionManager.Regions, можно задать (или получить) значение свойства Context. Экземпляр IRegion имеет событие PropertyChanged, которое можно использовать для отслеживания изменения значения свойства Context.

Еще один способ – это использовать статический метод GetObservableContext класса RegionContext. В качестве параметра метода выступает экземпляр вида. Метод возвращает объект типа ObservableObject<object>, и через его свойство Value можно получить значение RegionContext для заданного вида.

Общие сервисы (Shared Services)

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

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

Паттерн 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.

Работа с данными

Руководство по разработке приложений на базе Prism не содержит многих аспектов разработки, в том числе не освещает процесс клиент-серверного взаимодействия. Однако когда вы объявили интерфейс для сервиса Model из паттерна MVVM, вам необходимо создать класс реализации этого сервиса. В простейшем случае данные могут выгружаться на диск в виде файла на стороне клиента, но это приемлемо для небольших узкоспециализированных приложений. В большинстве случаев приложению необходимо обращаться к базам данных и различным сервисам. Эта тема очень обширна и достойна отдельной статьи, здесь я опишу только пару способов получения данных.

Сервисы 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 обращения к сервисам идут только через асинхронные вызовы.

Сервисы WCF Rich Internet Application (WCF RIA services)

Данные сервисы поставляются в составе Silverlight 4 Tools. Эти сервисы упрощают реализацию передачи данных из базы данных на сервере в клиентское приложение. Также RIA-сервисы позволяют логику, описанную на сервере, сделать доступной на клиенте. Построены они на базе WCF-сервисов, описанных ранее.


На стороне сервера получение данных может осуществляться на основе Entity Framework (EF), LINQ2SQL, NHibernate и т.д. Для примера, чтобы построить обращение на основе EF, в проект добавляется EF-модель. Она генерируется средой и позволяет получать данные из базы. Далее добавляется элемент DomainService. Это специальный WCF-сервис, позволяющий делать запросы к модели, обновлять ее, проверять данные и т.д. После добавления DomainService на сервере, на клиенте генерируется класс DomainContext. Далее, создавая экземпляр этого типа, можно делать запросы и загружать данные из базы.

Заключение

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

Еще одним большим удобством является поддержка разработки как под платформу Silverlight, так и под WPF. Большую часть кода можно сделать общей, и изменения в одном проекте будут отражаться в другом.

Проект активно развивается, публикуются новые версии. В последних версиях Prism появились библиотеки под новую развивающуюся мобильную платформу Windows Phone 7.

Список литературы

  1. http://msdn.microsoft.com/en-us/library/ff648611.aspx
  2. http://msdn.microsoft.com/en-us/magazine/dd943055.aspx


Эта статья опубликована в журнале RSDN Magazine #4-2010. Информацию о журнале можно найти здесь