1. FlexPure MVC архитектура для приложений
enterprise уровня
Описание:
1. Сначала мы вспомним основные составляющие микроахитектурыPureMVC, используя презентацию
SamuelAsherRivello.
2. Далее рассмотрим структуру и задачи, возникающие при использовании PureMVC в больших проектах.
a. Разновидности и жизненный цикл элементов системы
b. Необходимость синхронизации работы медиаторов с асинхронно создаваемыми видимыми
элементами.
3. «Страничная» архитектура для PureMVC проекта:
a. Управление View компонентами в «страничной» архитектуре:
i. отложенное (deferred) создание,
ii. расположение в иерархии DisplayObjects- слоты, медиаторы-слотхолдеры и их
взаимодействие с медиаторами-клиентами.
b. Жизненный цикл «страницы», дескриптор «страницы»
c. Прелоадер страницы
4. Взаимодействие View Components и Mediators
a. Шаблон «страницы»
b. Интерфейс «страничного» View Component
c. Обмен данными между медиатором и View компонентом.
d. Состояние «редактирования» (Editing state)
e. Обработка нажатий кнопок (Buttons)
f. Обработка других команд (пример – Chevrons)
5. Использование модулей в «страничной» архитектуре.
Требования к слушателям:
знание AS3, жизненного цикла компонентов Flex, PureMVChttp://puremvc.org
Докладчик:
Сергей Шичинов - SergiyShychynov (EPAM)Sergiy_Shychynov@epam.com
Примечание: пункт 4 был исключён из доклада для того чтобы длительность доклада
соответствовала регламенту.
2012-03-24
2. Основные составляющие PureMVC
(используя презентацию SamuelAsherRivello)
http://www.adobe.com/newsletters/inspire/december2008/articles/article6/index.html
MVC in PureMVC
Схема взаимодействия компонентов PureMVC для выполнения элементарной операции:
- Внешний вид компонента HelloGoogle – кнопка и текстовое поле для сообщения
- Последовательность элементарных действий выполняемых при нажатии кнопки
- Положение этих действий на диаграмме PureMVC
3.
4. Основные задачи, возникающие при использовании PureMVC в больших
проектах.
Когда речь идет о приложение enterprise уровня, то это означает что они БОЛЬШИЕ. То есть имеется
БОЛЬШОЕ количество РАЗНЫХ форм и компонентов, которые необходимо будет использовать в разных
ситуациях и в разное время. И речи о том, что все элементы будут созданы одновременно или заранее быть
не может. Более того – было бы неплохо, чтобы и грузить их все сразу не требовалось.
Рассмотрим несколько разных «видов» гипотетического большого приложения.
5.
6. Среди всего UIбольшого приложения можно выделить элементы (подсистемы):
1. Которые постоянно находятся на экране и доступны для взаимодействия.
2. Которые могут вызываться пользователем на экран по желанию и ведут себя относительно независимо
от других элементов UI.
3. Которые имеют отношение к текущей операции, выполняемой пользователем, показываются на экране
когда он начинает выполнять эту операцию и будут убраны с экрана когда он эту операцию
завершит.Причем таких циклов появления и исчезновения с экрана может быть много. Этот третий тип
подсистем мы будем в дальнейшем называть страницами – Pages.
7. И если операции по обслуживанию жизненного цикла (описанные далее) для компонетов 1 и 2 типа
можно выполнить один раз (например в StartupCommand), то выполнение всех нижеперечисленных
действий для компонентов подсистем 3-его типа является рутинной операцией и нуждается в
автоматизации.
Жизненный цикл элементов системы
Каждая подсистема 3 типа («страница») может состоять из одного или нескольких видимых элементов,
функционирующих совместно в одном промежутке времени. И для функционирования любой отдельной
подсистемы нашей большой системы нам будет необходимо:
1. Создать (инстанциировать) и разместить на экране (в иерархии DisplayObjects) необходимые компоненты
- ViewComponents. (Здесь и дальше компонентом будет называться видимый элемент, занимающий
место в иерархии DisplayObjects.)
2. создать и зарегистрировать необходимые медиаторы – Mediators.
3. связать медиаторы с соответствующими компонентами
a. медиаторы должны иметь ссылки на свои компоненты, для того чтобы передавать в них данные и
совершать действия
b. медиаторы прослушивают события – Events от компонентов для того чтобы узнать что нужно что-
то делать
4. зарегистрировать необходимые для работы подсистемы комманды – Commands (связать их с
Notifications),
5. создать и зарегистрировать необходимые для работы прокси–Proxy,
6. отобразить в наших компонентах валидные данные и обеспечить взаимодействие
a. Как правило, эти данные загружаются извне (с сервера). Это может занять определенное время.
Эти данные возможно не получится загрузить в результате ошибки. И возможно имеются некие
политики безопасности, которые в зависимости от того какие именно данные хочет увидеть
пользователь, могут не разрешить ему увидеть эти данные – и мы не узнаем о том разрешено это
или нет, пока не получим ответ с сервера.
Во всех этих случаях неразумно показывать на экране новые компоненты, еще не заполненные
данными, до того как эти данные будут получены с сервера. Это процесс мы будем называть
предзагрузка – preloader.
b. Во время работы пользователя с подсистемой, коммуникация с сервером обеспечивается по
стандартной схеме изложенной выше.
8. c. Отдельный вопрос – это обработка ошибок (серверных) – в зависимости от типа ошибок мы может
либо дальше продолжать оставаться на странице, либо потребуется обновить ее, либо придется
покинуть ее.
7. После того как пользователь завершил операцию нам необходимо корректно освободить ресурсы
системы от текущей «страницы» (очевидно перед тем как будет открыта следующая).
a. Удаление «актеров» PureMVC (медиаторов)
i. «отписать» медиаторы от системы (unregister) чтобы они не прослушивали больше
нотификейшены.
ii. Можно также удалять медиаторы, но можно сохранять их в «кэше» до следующего
использования.
iii. Команды тоже можно «отписать» от системы, но это не обязательно.
iv. Некоторые прокси можно было бы тоже отписывать от системы и удалять, но обычно этого
не нужно. И даже вредно.
b. Отписывание медиаторов от events, которые они прослушивали от компонентов.
c. Удаление видимых компонентов.
Вообще говоря, операция создания компонента и добавления его в DisplayList гораздо более
ресурсоемкая, чем сделать невидимый компонент – видимым. Поэтому для увеличения
перформанса и для того чтобы видимые элементы появлялись более плавно вместо удаления
компонентов лучше делать их невидимыми.
d. Если в процессе выполнения операций были загружены какие-то большие данные и ссылки на них
сохранились в компонентах или «актерах» PureMVC – то нужно их очистить, чтобы позволить
GarbageCollector –у освободить память.
Необходимость синхронизации работы медиаторов с асинхронно создаваемыми видимыми
элементами.
Как известно видимые элементы – компоненты флекса не создаются мгновенно по запросу. Как правил от
момента создание экземпляра компонента, установки его свойств и добавления его в иерархию
DisplayObjectsпроходит некоторое время и несколько этапов, прежде чем событие «creationComplete»дает
нам знать, что компонент и все его подкомпоненты полностью созданы и готовы к работе.
До этого момента медиатор, даже если и будет иметь ссылку на компонент, не сможет полноценно с ним
работать.
DefferedMediator
Решает проблему асинхронного создания компонентов. Откладывает вызовы initialize и activate до
момента, когда компонент будет создан и инициализирован. А потом еще откладывает update до
момента, когда все медиаторы из списка будут активированы.
Constructor() – чистый конструктор (не выполняющий никаких действий)
* Override it to complete setup mediator properties and associate mediator
* with the View Component. Use setupMediator function to do most of the tasks.
function create():void
* The method is called after component <code>creationComplete</code> event.
* Create it to add listeners and make additional view component initialization.
function initialize():void;
* This method is called every time when you show and activate the view component.
* Override it to add additional mediator's and view component's activation.
functionactivate():void;
waitForMediatorsActivation(mediatorNames);
* Override it to update view component in accordance with current viewParams.
* You can process here both stateParams and viewParams.
* Is called after initialize() (after creationComplete event) and
9. * activate() methods.
function update():void
* Deactivate mediator (remove from pureMVC workflow).
* You can override it to make additional deactivation.
functiondeactivate():void;
* Create it to remove listeners and make additional view component finalization.
functionfinalize():void;
Кто, когда и как создает видимые элементы.
Слоты, медиаторы-слотхолдеры и их взаимодействие с компонентами-клиентами.
Задача по созданию и связыванию вью компонентов с медиаторами возложена на медиатор.
Медиатору известен класс его компонента. При создании медиатора – в методе create() – медиатор создает
экемпляр вью компонента и помещает его в DisplayList. Делается это с помощью концепции медиаторов-
слотхолдеров, которые имеют в составе своего компонента slot – контейнер (обычно ViewStack) в который
можно поместить, найти и удалить комопнент.
SlotViewComponentDescription
viewComponentName:String,
viewComponentSlot:String,
viewComponent:UIComponent
SlotAMediator
M1Mediator
медиатор-слотхолдер
медиатор-клиент SlotAMediator получает
M1Mediator создает нотификейшен, адресованный ему
компонет и отправляет его и добавляет в свой слот (ViewStack
слоту "SlotAMediator" - или другой контейнер) новый
просит его хранить под компонет под именем "M1"
именем "M1"
10.
11. «Страничная» архитектура для PureMVC проекта
Мы ввели понятие страниц – Pages.
«Страница»– это некоторое множество видимых элементов, функционирующих совместно в одном
промежутке времени. Эти компоненты и обслуживающие их актеры PureMVC совместно создаются, работают
и удаляются.
Выполнением всех этих операций в PureMVC традиционно занимаются команды.
Чтобы не писать отдельные команды для открытия каждой страницы, была создана единая команда
открытия новой страницы ViewPageCommand. Эта команда выполняет все необходимые при открытии
страницы действия в соответствии с переданным ей в качестве входного параметра дескриптором
страницы.
Она работает совместно с PagesProxy, которыйи является хранителем информации обо всех доступных
в системе «страницах» и «состояния» текущей открытой страницы.
Основные задачиPagesProxy:
- хранение справочника всех страницах в системе
functiongetPageDescription(pageName:String):PageDescription
- хранение информации о текущей открытой странице (ее имя, дескриптор, стейт, состояние
редактирование, helpContext и прочее)
Соответственно все страницы имеют уникальное имя
publicclass Pages
{
publicstaticconstABOUT:String = "about";
publicstaticconstADMIN_EMAIL_TEMPLATE_EDIT:String = "adminEmailTemplateEdit";
publicstaticconstDOCUMENT_TRANSLATION:String = "documentTranslation";
…
}
идескрипторы
PageDescription
pageName:String,
preloader:Class, - наследникBasePagePreloader
mediators:Array, - списокклассовнаследониковBaseMediator
proxies:Array = null,
commands:Array = null, - списокCommandDescription(name:String, commandClass:Class)
stateParams:Object = null, - редко встречающиеся дополнительные параметры
pageTitle:String = null, } - часто встречающиеся дополнительные параметры
breadcrumbs:Array = null. … }
Примердескриптора страницы:
newPageDescription(Pages.QUESTIONNAIRE,
QuestionnaireAnswerPreloader,
[QuestionnaireSlotHolderMediator, QuestionnaireAnswerMediator],
[QuestionnairesProxy, ProcessEditProxy, ProcessTypesListProxy, TimeScaleProxy],
[
newCommandDescription(SPNotification.FIND_NEW_QUEST, LoadQuestiannairCommand),
newCommandDescription(SPNotification.UPDATE_ASSET_QUEST, UpdateAssetForQuestCommand),
],
{questionnaireComponentState: QuestionnaireComponentState.ANSWER},
"Questionnaire Registry"
)
PageNotificationParams
varpageName:String;
12. varpageState:String;
varid:Number;
varbackPageName:String;
varpreloader:BasePreloaderMediator; - создается и добавляется в процессе открытия страницы
ViewPageCommand
1. Проверяет наличие дескриптора страницы и при необходимости подгружает модуль содержащий
страницу. Входными параметрами для открытия страницы являются:
a. pageDescriptor – находится по имени страницы
b. stateParams : {} – является дополнительным набором параметров страницы (которые не
вошли в сам дескрипотор – так как редко используются)
c. viewParams: PageNotificationParams – дополнительные параметры страницы, заполняемые
при ее открытии
2. Создает и регистрирует необходимые прокси (они могут быть нужны прелоадеру)
3. Создает и запускает прелоадер
a. load()
b. validate() + препроцессинг
4. Если прелоадер «дал добро», то закрывает текущую страницу
a. отписывает от системы медиаторы
b. подчищает «временные» данные (на вью компонентах и медиаторах!)
c. отписывает медиатор от events компонентов
d. добавляет ссылку на прелоадер с загруженными и подготовленными данными во viewParams
5. Регистрирует команды
6. Создает (или достает из кеша) медиаторы, передает им значения stateParams и viewParams и
регистрирует их в системе.
a. Если медиаторы создаются то это делается в следующем порядке
i. Конструктор
ii. Setup params (stateParamsиviewParams)
iii. create()
b. если достается из кеша, то просто Setupparams (stateParams и viewParams)
7. Активирует медиаторы. В процессе активации медиаторы должны создать свои компоненты,
разместить их в иерархии DisplayLists, сделать видимыми и подписаться на необходимые events.
8. После активации всех медиаторов вызывается update()
Прелоадер – Preloader
Прелоадерслужитдлятогочтобызагрузитьнесколько «частей» данных
(используяпроксиилинепосредственноделегаты). После полной загрузки всех частей возможна их валидация
и дополнительная обработка. Прелоадер отрабатывает при попытке открыть новую «страницу» до того как
она будет реально открыта. В случае если валидатор прелоадера выдает false, страница не будет открыта.
Вместо этого будет выведено сообщение об ошибке.
Фрагмент кода вызова прелоадера при открытии страницы
if(pageDesc.preloader != null) {
setLoadingState(true);
registerProxies(); // for preloadin purposes
varpreloaderMediator:BasePreloaderMediator = new (pageDesc.preloader)();
preloaderMediator.viewParams = params;
preloaderMediator.stateParams = pageDescriptor.stateParam;
preloaderMediator.callback = executeAfterPreload;
preloaderMediator.load();
BasePreloaderMediator
varstateParams:Object;
varviewParams:PageNotificationParams;
13. * Override it to create more preloader parts. You can use viewParams and stateParams.
function load():void
{
//...
addDelegatePreloaderPart("YYYid", ResourceTypeDelegate.instance.getResourceTypeById(id));
//...
addNotificationPreloaderPart("XXXid", XXXResultNotification, XXXFaultNotification);
xxxProxy.load(blablabla);
//...
activate();
}
* Override it to validate loaded data. If ERROR - you have to setup
* preloader.errorKey before returning false value.
function validate():Boolean
{
if(preloader.errorKey == null)
{
// достаем нужное нам данное и проверяем
getPart("YYYid") – используем для проверки
}
return (preloader.errorKey == null);
}
Пример прелоадера «детям до 16»
publicclassManageGroupMembershipPreloaderextendsBasePreloaderMediator
{
overridepublicfunction load():void
{
varid:Number = getID(true);
if(!isNaN(id))
addDelegatePreloaderPart(ContactProxy.CONTACT,
ContactDelegate.instance.getContactById(id) );
addNotificationPreloaderPart(LanguageProxy.PLAIN_LANGUAGE_LIST,
SPNotification.PLAIN_LANGUAGE_LIST_LOADED,
SPNotification.PLAIN_LANGUAGE_LIST_FAILED);
LanguageProxy(facade.retrieveProxy(LanguageProxy.NAME)).getPlainLanguageList();
super.load();
}
overridepublicfunction validate():Boolean
{
if(super.validate())
{
varcontact:Contact = getPart(ContactProxy.CONTACT) as Contact;
if(contact != null&&contact.age< 16)
setPreloaderError("Просмотр этой страницы не разрешен детям до 16");
returnsuper.validate();
}
}
Модульность
В настоящее время система имеет:
полторы сотни (150) основных страниц со связанными с ними rollover формами,
несколько десятков popup-форм
некоторое кол-во подсистем,
работающих постоянно на экране,
вызываемых по требованию пользователя
14. или работающий в качестве «демонов» – daemon (ping, systemmessages)
и«сервисов» (messages, multiple operations, progress indication etc.)
Кодовая база клиентской части проекта включает более 2000 файлов. Из них более 1600 as файлов и
около 400 mxml.
При этом с точки зрения заказчика это все разбито на 8 логических «модулей» и лицензия на
использование каждого из них может покупаться отдельно.
Кроме того многие пользователи могут не иметь доступа к некоторым модулям (администрирование,
отчеты) по соображениям секьюрности или в соответствии с их ролями.
Понятно, что вопрос о том чтобы не грузить все это сразу при открытии приложения возник уже
давно.То есть необходимо при запуске загрузить только ядро приложения а решение о необходимости подгрузки
дополнительного функционала уже принимать по ситуации.
Необходимо: при начальной загрузке загружать только ядро системы, а все дополнительные, не всегда или редко
используемые возможности подгружать по требованию. Для уменьшения времени загрузки и потребляемого трафика.
RSLдля этого использовать нельзя, так как для RSLприходилось бы вручную выбирать какие классы включать в эту
библиотеку а какие - нет. Что неприемлемо при колве классов больше 2000.
Флексовые модули для этого как бы не задумывались (это скорее неудачный способ монетизации флексовых
приложений), но воспользоваться ими получилось.
- Флексовый линкер при компиляции модуля позволяет собрать все классы, по цепочке зависимостей
- есть возможность указать линкеру, что необходимо исключить классы, которые уже есть в ядре приложения.
И в разбиении системы на модули очень помогло то, что мы имели большинство функционала, организованного в виде
«страниц». Страницы были использованы как ноды для дерева линковки всех классов, необходимых для работы
текущего модуля.
Таким образом, модуль подгружает ViewPageCommandв момент когда впервые понадобиться страница содержащаяся в
этом модуле. После загрузки модуля (и всех его классов) все PageDescriptors, содержавшиеся в этом модуле добаляются
в список доступных страниц в PagesProxy.
Структура иерархии модулей
Module – единица компиляции и линковки
ModuleDescription – содержитполныйнаборвсехPageDescriptorsмодуля
PageDescriptionsXXX – логически сгруппированные наборы дескрипторов страниц
moduleDescriptors.xml – справочник, содержащий информацию о том какая страница в каком
модуле содержится
flex-include-vo.xml – список всех VO системы для включения в ядро системы (решение проблемы
регистрации VO для ремотинга)
Соответствующие ANTскрипты. !!!НИКОГДА не включайте модули из среды FlashBuilder!!!
Проблема регистрации классов для римотинга.
[RemoteClass(alias="com.os.sp.domain.messaging.MessagesGroup")]
publicclassMessagesGroupextendsMessagesGroupBaseimplementsIOSSPMessage{}
Не регистрирует класс, если VOлинкуется через модуль!
Решение проблемы:
1) вмодуледелаемregisterServerClass(MessagesGroup);
publicstaticfunctionregisterServerClass(classRef:Class):void
{
registerClassAlias(getClassServerName(classRef), classRef);
17. >
<load-configfilename="${FLEX_LOCAL_CONFIGURATION}"/>
</mxmlc>
Компиляциямодулей
<targetname="compile-modules"if="flex.modular.exist">
<compile-module-simplemoduleName="ModuleAdministration"/>
<compile-module-simplemoduleName="ModuleMessaging"/>
<compile-module-simplemoduleName="ModuleImportExport"/>
</target>
Фрагмент из макроопределения компиляции модуля
<mxmlcfile="${MODULE_SOURCE_FOLDER}/@{moduleName}.@{moduleType}"
output="${BUILD_FOLDER}/@{moduleName}.swf"
load-externs="${BUILD_FOLDER}/@{dependsOn}.${FULL_LINK_REPORT_POSTFIX}"
>
</mxmlc>
Приложение: особенности реализации SlotHolderмедиаторов
Задача по созданию и связыванию вью компонентов с медиаторами возложена на медиатор.
Медиатору известен класс его компонента. При создании медиатора – в методе create() – медиатор создает
инстанс класса вью компонента и отправляет его слот-холдермедиатору чтобы тот поместил его себе в
DisplayList.
protectedfunctionsetupMediator(…)
mediatorName:String = null,
// AUTO – такое же как имя класса – для регистрации в PureMVC
viewComponentReference:Object = null,
// 1) null must be null,
// 2) component class
// 3) component instance (descendant of UIComponent)
viewComponentName:String = null, // viewComponent custom name
// AUTO – по имени медиатора без постфикса Mediator
// будет использовано как id и name в слотхолдере
viewComponentSlot:String = null, // specific slot for register/find viewComponent (if need)
viewComponentInitAction:int = 0 // special init action (0 - no action)
INIT_NONE:int = 0; // no action
INIT_FIND_OR_WAITING_FOR_THE_VIEW_COMPONENT:int = 1;
// find view component with appropriate name in appropriate slot or (in not found)
// listen for the BaseNotifications.SLOT_VIEW_COMPONENT_CREATION_COMPLETE notification
INIT_FIND_VIEW_COMPONENT:int = 2;
// find view component with appropriate name in appropriate slot
INIT_ADD_TO_SLOT:int = 4;
// add view component to appropriate slot using appropriate name
overridepublicfunctionlistNotificationInterests():Array
{
return (slots == null)
? super.listNotificationInterests()
: super.listNotificationInterests().concat(
BaseNotifications.SLOT_VIEW_COMPONENT_ADD,
BaseNotifications.SLOT_VIEW_COMPONENT_REMOVE_BY_NAME,
18. BaseNotifications.SLOT_VIEW_COMPONENT_SHOW,
BaseNotifications.SLOT_VIEW_COMPONENT_HIDE,
BaseNotifications.SLOT_VIEW_COMPONENT_FIND
);
}
overridepublicfunctionhandleNotification(notification:INotification):void
{
super.handleNotification(notification);
…
}
privatefunction defaultSlotHolderNotificationHandler(notification:INotification):void
{
if(slots && slots[notification.getType()]) {
vardesc:SlotViewComponentDescription = notification.getBody() as SVCD;
varcomponent:UIComponent;
component = getViewComponentFromSlot(desc.viewComponentSlot, desc.viewComponentName);
switch(notification.getName()) {
caseBaseNotifications.SLOT_VIEW_COMPONENT_SHOW:
FlexUIComponentsUtils.showViewComponent(component, true);
break;
caseBaseNotifications.SLOT_VIEW_COMPONENT_HIDE:
component.visible = false;
break;
caseBaseNotifications.SLOT_VIEW_COMPONENT_ADD:
addViewComponentToInternalSlot(desc);
desc.viewComponent = null; // mark viewComponent as added
break;
caseBaseNotifications.SLOT_VIEW_COMPONENT_REMOVE_BY_NAME:
removeViewComponentFromSlotByName(desc);
break;
caseBaseNotifications.SLOT_VIEW_COMPONENT_FIND:
desc.viewComponent = component;
break;
}}}
Далее описанантипаттерн, который используется для оптимизации взаимодействия слотхолдеров. Но мне не
стыдно, потому что это, с одной стороны – служит оптимизации (можно было и не делать), с другой – можно
было бы без этого обойтись просто воспользовавшись синглтоном-менеджером.
* Register handler for Notification - works just like addEventListebner.
* It doesn't depend on registerMediator/removeMediator !BE AWARE!
functionaddNotificationListener(notificationName:String, handler:Function):void
{
org.puremvc.as3.core.View.getInstance().registerObserver(
notificationName, new Observer(handler, this) );
}
* Remove Notification handler - works just like removeEventListebner.
* It doesn't depend on registerMediator/removeMediator !BE AWARE!
functionremoveNotificationListener(notificationName:String):void
{
org.puremvc.as3.core.View.getInstance().removeObserver(
notificationName, this );
}
Примериспользования:
* Try to add view component in appropriate external slot. If there isn't
* appropriate slot then start waiting for SLOT_CREATION_COMPLETE.
functionaddViewComponentToExternalSlot():void {
vardesc:SlotViewComponentDescription = newSlotViewComponentDescription