Навигация: Вариант Реализации Корпоративного Приложения

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

Последние несколько лет я работал над системой управления школой и системой управления лекарствами.

Мне приходилось решать разного рода проблемы, связанные с навигацией по приложению.

Но в зависимости от используемых фреймворков не всегда можно было найти удобное решение.

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

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

В общем, всю логику навигации по приложению мне пришлось хранить в голове.

Однако мне хотелось, чтобы система была такой же простой в использовании, как, например, интернет-браузер.

Переходите на нужные вам страницы в один-два клика.

Посмотрите путь для навигации по приложению.

Чтобы был простой и понятный механизм всего приложения.

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



Введение

Каждое корпоративное приложение имеет свою собственную модель предметной области, которая определяет отношения между сущностями системы.

Сущности могут быть связаны по-разному: один-к-одному, один-ко-многим, многие-ко-многим.

Допустим, есть некоторая модель предметной области приложения.



Навигация: вариант реализации корпоративного приложения

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

Также представление может отображать сразу несколько сущностей, например, как показано на рисунке ниже.

B-view работает с сущностями классов B, D и E.

Навигация: вариант реализации корпоративного приложения

В соответствии со связями на уровне модели предметной области пользователь может перемещаться между соответствующими представлениями, например, как показано на рисунке ниже, начиная с представления B, переходить к представлению C, затем к F, затем к E.

Навигация: вариант реализации корпоративного приложения

В других случаях пользователь может начать с представления А и перейти вниз по цепочке к виду Е.



Навигация: вариант реализации корпоративного приложения

Или другой вариант — начать работать с другой стороны, от представления Е и через переходы дойти до представления А.



Навигация: вариант реализации корпоративного приложения

Чтобы реализовать такое множество различных вариантов навигации по приложению, было бы неплохо сначала определиться с технологиями.



MVC или компонентная структура

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

Из компонентных фреймворков возьмите простоту создания различных форм, таблиц, графиков и других элементов интерфейса и объедините это с возможностью управления навигацией приложения с помощью внешней конфигурации по аналогичным принципам, реализованным в Struts или Spring Web Flow. В качестве компонентного фреймворка был выбран Vaadin, при этом подходящей реализации для навигации, аналогичной Spring Web Flow, не нашлось.

Попытка самостоятельно интегрировать Vaadin и Spring Web Flow не удалась из-за существенных различий в механизме запроса/ответа моделей.

Поэтому было решено реализовать собственную версию Web Flow, которая не будет зависеть от модели «Запрос/Ответ».

За основу был взят Диаграмма состояний UML и внедрил Lexaden Web Flow. В нем был создан механизм соединения Statechart с моделью компонентов таким образом, чтобы можно было в зависимости от состояний переключать визуальные компоненты в определенной области приложения.

На рисунке ниже показаны основные компоненты Lexaden Web Flow.

Навигация: вариант реализации корпоративного приложения



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

А процессор событий используется для доступа к метаинформации, которая определяет, какие события доступны в данный момент и в каком состоянии находится процесс навигации.



Механизм Lexaden Web Flow
Движок реагирует на события, поступающие от процессора событий, и осуществляет процесс перехода между состояниями согласно ранее полученной конфигурации.



Государственный контролер
Контроллер состояния пользовательского интерфейса реагирует на события, поступающие от механизма Lexaden Web Flow, и встраивает представления, полученные от контроллеров, в макет приложения.

Он также отвечает за передачу контроллерам событий о переходе из одного состояния навигации в другое.



Начинать

Чтобы получить первый опыт использования Lexaden Web Flow, вы можете принять два состояния и переключать панели приложения между собой в зависимости от событий.



Навигация: вариант реализации корпоративного приложения

Пример того, как это реализовано в XML:

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

<flow initial="Panel1" .

> <controller id="Panel1"> <on event="panel2" to="Panel2"/> </controller> <controller id="Panel2"> <on event="panel1" to="Panel1"/> <on event="ok" to="OK"/> </controller> <final id="OK"/> </flow>

По событию от контроллера «Панель1» приложение переключается и отображает «Панель2» и наоборот. При возникновении события «ОК» выполнение программы завершается.

Далее рассмотрим более сложный пример, возьмем доменную сущность «человек» и реализуем для нее CRUD-операции, чтобы каждая операция соответствовала отдельному состоянию и управлялась собственным контроллером.



<flow initial="list" .

> <module id="person"> <controller id="list"> <on event="create" to="create"/> <on event="read" to="read"/> <on event="update" to="update"/> <on event="delete" to="delete"/> </controller> <controller id="create"> .

</controller> <controller id="read"> .

</controller> <controller id="update"> <on event="updated" to="list"/> <on event="canceled" to="list"/> </controller> <controller id="delete"> .

</controller> </module> </flow>

Но здесь есть проблема.

Как мне вернуться от контроллеров «создание», «чтение», «обновление» и «удаление» к контроллеру «список»? Самым очевидным было бы установить явные переходы к контроллеру «список» в каждом контроллере:

<on event="updated" to="list"/> <on event="canceled" to="list"/>

Но такая опция привяжет контроллеры «создать», «читать», «обновить» и «удалить» к контроллеру «списка», что не позволит повторно использовать контроллеры «обновления» или «удаления», например , контроллер «чтения».

Зачем переходить от «читающего» контроллера обратно к «списку» и потом добираться до «обновления», если можно сразу перейти от «прочитанного» к «обновляемому» контроллеру? Решение — добавить «конечные» состояния внутри каждого контроллера, чтобы при возникновении событий переходить к ним, а не к внешним контроллерам:

<controller id="create"> <on event="created" to="created"/> <on event="canceled" to="canceled"/> <final id="created"/> <final id="canceled"/> </controller> <controller id="update"> <on event="updated" to="updated"/> <on event="canceled" to="canceled"/> <final id="updated"/> <final id="canceled"/> </controller>

При входе в конечное состояние Lexaden Web Flow сообщается, что контроллер завершен, и LWF передает управление предыдущему контроллеру с помощью сообщения, состоящего из имени текущего контроллера и имени конечного состояния.

Например, «update.updated», «update.canceled» или «create.created», которые будут использоваться как результат завершения работы контроллера.



Базовый контроллер
При переходе от контроллера к контроллеру приходится каждый раз заново создавать представления.

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

В его обязанности входит определение основного жизненного цикла контроллера.

Базовый контроллер состоит из состояний «действия» и «просмотра».

Состояния действий отвечают за действия по инициализации представления и извлечению данных из модели предметной области.

Состояния представления предназначены для определения того, когда представление необходимо отобразить в приложении.



<controller id="controller" initial="initView"> <!-- init view. create all components of the view. get ready to setup data --> <action id="initView" extends="action"> <on to="loadData"/> </action> <!-- setup data before displaying the view. get data from context attributes --> <action id="loadData" extends="action"> <on to="displayView"/> </action> <view id="displayView" extends="view"> <on event="ok" to="ok"/> <on event="close" to="close"/> <on event="cancel" to="canceled"/> </view> <final id="close" extends="action"/> <final id="canceled" extends="action"/> <final id="ok" extends="action"/> </controller>

Действие — «initView» используется для инициализации представления в контроллере.

После того, как контроллер создал представление, происходит переход к действию «loadData», которое используется для загрузки данных в представление.

Состояние «displayView» сообщает платформе о необходимости вставить представление, полученное из контроллера, в приложения Layout. При событиях «ок», «закрыть» или «отмена» контроллер из «displayView» переходит в соответствующие конечные состояния.

Это означает, что контроллер завершил свою работу.



Навигация: вариант реализации корпоративного приложения

Для привязки состояний «действия» к методам соответствующего Java-контроллера используются аннотации:

@OnEnterState(EventConstants.INIT_VIEW) public void initView(StateEvent event) { .

}

В этом случае, когда контроллер входит в состояние действия «initView», вызывается метод initView, отмеченный аннотацией OnEnterState с названием состояния.

И событие, сгенерированное другим контроллером, передается ему в качестве параметра.

Событие может содержать любые данные, например, идентификатор выбранного объекта на предыдущем экране.

И используется для загрузки данных, связанных с идентификатором.

Таким же образом вы можете подписаться на события, когда приложение переходит в состояние «просмотра».



@OnEnterState(EventConstants.DISPLAY_VIEW) public void refreshTable(StateEvent event) { … }

Это можно использовать для обновления данных в таблице или форме каждый раз, когда пользователь возвращается в состояние «displayView».



Модули
Поскольку контроллеров в корпоративном приложении может быть достаточно много, их лучше объединять в модули.

Убедитесь, что модули независимы друг от друга и обмениваются информацией только через события.

Например, из модуля «заказы» можно перейти в модуль «адреса» с помощью события go_addresses.

Навигация: вариант реализации корпоративного приложения

Так, например, набор контроллеров для CRUD-операций можно объединить в модуль, например:

<module id="addresses" initial="list"> .

<controller id="list" … > <on event="go_create" to="create"/> <on event="go_read" to="read"/> <on event="go_update" to="update"/> <on event="go_delete" to="delete"/> </controller> <controller id="create" … > … </controller> <controller id="read" … > … </controller> <controller id="update" … > … </controller> <controller id="delete" … > … </controller> .

</module>

При входе в модуль «адреса» приложение попадает в контроллер «список».

Соответствующие контроллеры Java привязываются к контроллерам из Lexaden Web Flow, например, с помощью следующего кода:

flowControllerContext.bindController("addresses/list", “content”, new AddressesListController ()); flowControllerContext.bindController("addresses/create", “content”, new AddressesCreateController ()); flowControllerContext.bindController("addresses/read", “content”, new AddressesReadController ()); flowControllerContext.bindController("addresses/update", “content”, new AddressesUpdateController ()); flowControllerContext.bindController("addresses/delete", “content”, new AddressesDeleteController ());

С контроллером AddressesListController связано определенное представление (AddressesListView), которое становится активным, когда приложение переходит в состояние «адреса/список».

Точно так же другие контроллеры своими представлениями привязаны к состояниям «создание», «чтение», «обновление», «удаление».

На основе событий от контроллера, привязанного к состоянию «список», приложение переходит в соответствующие состояния «создание», «чтение», «обновление» или «удаление» и отображает соответствующие представления пользователю.



Навигация: вариант реализации корпоративного приложения



CRUD: базовые операции с объектами домена
Поскольку большинство доменных объектов в системе могут поддерживать одни и те же операции, такие как Список, Создание, Чтение, Обновление, Удаление, было бы неплохо определить логику навигации в отдельном модуле — «crud», а затем, наследующем от В нем можно создавать модули для разных объектов домена с автоматической поддержкой операций CRUD.

<module id="crud" initial="list" .

> <controller id="list" extends="controller"> <on event="create" to="create"/> <on event="read" to="read"/> <on event="update" to="update"/> <on event="delete" to="delete"/> </controller> <controller id="create" extends="controller"> … </controller> <controller id="read" extends="controller"> … </controller> <controller id="update" extends="controller"> … </controller> <controller id="delete" extends="controller"> … </controller> </module> <module id="orders" extends="crud" > <on event="go_account" to="account"/> <on event="go_addresses" to="addresses"/> </module> <module id="account" extends="crud"/> <module id="addresses" extends="crud"/>

Модули «заказы», «счет», «адреса» наследуются от определенного выше модуля «crud» и получают в свое распоряжение копию логики переходов между состояниями CRUD. Теперь создание новых модулей происходит достаточно лаконично, что позволяет легко добавлять новые модули в процессе разработки системы.



Readonly + CRUD: разграничение прав просмотра и редактирования
Чтобы иметь возможность ограничить доступ к определенным операциям над объектами домена в приложении, вы можете разделить CRUD на два модуля «только для чтения» и «crud».

В этом случае readonly будет использоваться только для чтения, а crud — для полного редактирования сущностей приложения.



<module id="readonly" initial="list" extends="module"> <controller id="list" extends="controller"> <on event="read" to="read"/> </controller> <controller id="read" extends="controller"/> </module> <module id="crud" initial="list" extends="readonly"> <controller id="list" extends="readonly/list"> <on event="create" to="create"/> <on event="update" to="update"/> <on event="delete" to="delete"/> .

</controller> <controller id="create" extends="controller"> … </controller> <controller id="read" extends="controller"> … </controller> <controller id="update" extends="controller"> … </controller> <controller id="delete" extends="controller"> … </controller> </module> <module id="orders" extends="readonly" > <on event="go_account" to="account"/> <on event="go_addresses" to="addresses"/> </module> <module id="account" extends="crud"/> <module id="addresses" extends="crud"/>

Поскольку модуль «заказы» унаследован от модуля «только для чтения», теперь пользователь сможет видеть список заказов и просматривать каждый заказ в отдельности, но не сможет их создавать, редактировать или удалять.

Модули «учетная запись» и «адреса», унаследованные от модуля «crud», позволят пользователю просматривать, создавать, обновлять и удалять объекты.



Пикеры: повторное использование модулей для выбора значений из списка
Приложение может использовать множество разных таблиц с определенными настройками.

Обычно они привязаны к «списку» состояний из модулей CRUD. Но их конфигурация настроена таким образом, что при возникновении события «чтения» оно поступает на контроллер «чтения».

Используя наследование, вы можете повторно использовать таблицы не только для просмотра списка объектов, но и для выбора значений из таблицы.

Для этого в модуль crud можно добавить контроллер «picker», который будет наследоваться от контроллера crud/list:

<module id="crud" initial="list".

> … <controller id="picker" extends="crud/list"> <on event="read" to="picked"/> <final id="picked" extends="action"/> </controller> </module>

Контроллер — «пикер» наследуется от «crud/list» и переопределяет событие «чтение», перенаправляя в конечное состояние — «выбрано».

Это позволяет нажать на строку в таблице, чтобы вернуться на предыдущий экран и получить событие «picker.picked» с идентификатором выбранного объекта.

Перехватив его в контроллере предыдущего экрана, обновите содержимое, например, выпадающего списка.



Навигация: вариант реализации корпоративного приложения

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



<module id="orders" extends="readonly" > <on event="go_account" to="account"/> <on event="go_addresses" to="addresses"/> <on event="select.account" to="account/picker"/> <on event="select.addresses" to="addresses/picker"/> </module> <module id="account" extends="crud"/> <module id="addresses" extends="crud"/>

События «выбрать.

аккаунт» и «выбрать.

адреса» из модуля «заказы» приводят к переходам к «счет/выборщик» и «адреса/выборщик», позволяя выбрать нужные объекты из таблиц.



Навигация: вариант реализации корпоративного приложения

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



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

Разный набор модулей может использоваться разными ролями (администраторами, менеджерами и другими сотрудниками организации), для этого они объединяются в профили, например, как показано на рисунке ниже:

Навигация: вариант реализации корпоративного приложения

Профиль — это часть приложения, привязанная к определенной роли в системе.

Например, профилю администратора доступен весь функционал в системе, а клиенту доступен только ограниченный функционал, например, сделать заказ, просмотреть статус заказа.



Навигация: вариант реализации корпоративного приложения

Ниже приведен пример настройки модуля в профиле:

<profile id="customer" .

> … <module id="orders" … > <on event="go_account" to="account"/> <on event="go_addresses" to="addresses"/> </module> <module id="account" … > </module> <module id="addresses" … > </module> … </profile>

Переходы между модулями желательно определять на уровне профиля, чтобы сами модули оставались независимыми друг от друга и могли повторно использоваться в других профилях.

Чтобы разрешить одновременное использование одних и тех же модулей в разных профилях, Lexaden Web Flow поддерживает наследование.

Например, определив модуль «t_addresses» на верхнем уровне, вы можете включать его в разные профили.



<flow> .

<profile id="t_admin" .

> … <module id="addresses" extends="t_addresses"> <controller id="list" extends="addresses/list"> <on event="go_export" to="export"/> </controller> <controller id="export" > .

</controller> </module> … </profile> <profile id="t_customer" .

> … <module id="addresses" extends="t_addresses"/> … </profile> <module id="t_addresses" initial="list"> .

</module> .

</flow>

Профили «t_admin» и «t_customer» получают копии модуля «t_addresses», определенного на верхнем уровне.

Также, используя наследование в профиле «t_admin», модуль «адрес» расширяет возможности контроллера «список», добавляя событие «go_export», которое приведет к переходу приложения в состояние «admin/addresses/export».

.

Соответствующий контроллер с представлением экспорта привязан к «адресам/экспорту».

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

Затем профили включаются в «приложение» с использованием того же наследования.



<application id="application" .

> … <profile id="admin" extends="t_admin"/> <profile id="manager" extends="t_manager" /> <profile id="team_leader" extends="t_manager" > <on event="go_team" to="team"/> <module id="team"…> … </module> </profile> <profile id="employee" extends="t_employee"/> <profile id="customer" extends="t_customer"/> … </application> <profile id="t_admin" .

> .

</profile> <profile id="t_manager" .

>.

</profile> <profile id="t_employee" .

>.

</profile> <profile id="t_customer" .

>.

</profile>

Все состояния в системе поддерживают наследование и полиморфизм, что упрощает их повторное использование с небольшими изменениями.

Основное состояние «приложение» используется для привязки макета приложения, который определяет базовую структуру пользовательского интерфейса.

Это делается следующим образом:

flowControllerContext.bindController("application", new ApplicationLayoutController());

Используя внешний контекст flowControllerContext, ApplicationLayoutController привязывается к состоянию «приложения», которое внутри содержит структуру пользовательского интерфейса приложения.

Внутри этой структуры определены так называемые «заполнители», задача которых — разметить Макет приложения на определенные части, куда будут вставляться различные представления во время навигации по приложению.



Навигация: вариант реализации корпоративного приложения

Например, Left SideBar может служить для размещения меню приложения, заголовка для логотипа, панели поиска или кнопки входа в систему.

Right SideBar можно использовать для размещения различных видов вспомогательных окон.

Контент служит основой для информации приложения.

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



flowControllerContext.bindController("addresses/list", “content”, new AddressesListController ()); flowControllerContext.bindController("addresses/create", “content”, new AddressesCreateController ()); flowControllerContext.bindController("addresses/read", “content”, new AddressesReadController ()); flowControllerContext.bindController("addresses/update", “content”, new AddressesUpdateController ()); flowControllerContext.bindController("addresses/delete", “content”, new AddressesDeleteController ());

Все контроллеры, привязанные к модулю «адреса», будут привязаны к заполнителю — «контенту».

Во время работы программы виды этих контроллеров будут отображаться в соответствующем месте макета приложения.



Потоки: процесс выполнения
Чтобы иметь возможность перемещаться по системе, начиная с разных объектов предметной области, Lexaden Web Flow использует механизм потоков.

Чтобы запустить поток, тег on указывает атрибут type="flow" на уровне профиля.

Когда приложение генерирует событие типа «поток», Lexaden Web Flow либо запускает новый поток, либо переключается на уже активный поток.



<profile id="manager" … /> <on type="flow" event="orders.flow" to="orders"/> <on type="flow" event="account.flow" to="account"/> <on type="flow" event="addresses.flow" to="addresses"/> <module id="orders" extends="readonly" > <on event="go_account" to="account"/> <on event="go_addresses" to="addresses"/> </module> <module id="account" extends="crud"/> <module id="addresses" extends="crud"/> </profile>

Меню приложения используется для отправки событий с типами потоков, которые открывают потоки во вкладках, как показано на изображении ниже:

Навигация: вариант реализации корпоративного приложения

В приложении потоки связаны с закрытием вкладок, а путь навигации внутри активного потока отображается с помощью компонента «Хлебная крошка».



Навигация: вариант реализации корпоративного приложения

Контроллеры функционируют как асинхронные функции.

Чтобы завершить свою работу, они должны перейти в одно из нескольких внутренних конечных состояний типа Final. При входе в это состояние Lexaden Web Flow генерирует новое событие, состоящее из имени контроллера и имени конечного состояния внутри контроллера.

Например, для контроллера «чтения» с внутренним конечным состоянием «ОК» LWF сгенерирует событие «read.ok», передавая это событие предыдущему контроллеру потока, позволяя ему обработать результат выполнения.



<controller id="list" extends="controller"> <on event="read" to="read"/> <on event="read.ok".

/> .

</controller> <controller id="read" extends="controller"> <on event="update" to="update"/> <on event="update.updated" to="updated"/> <action id="updated" extends="action"/> <!-- this part already exists in the "read" controller such as it is inherited from the parent "controller" state <view id="displayView" extends="view"> <on event="ok" to="ok"/> .

</view> <final id="ok" extends="action"/> --> … </controller> <controller id="update" extends="controller"> <on event="updated" to="updated"/> <final id="updated" extends="action"/> <!-- this part already exists in the "update" controller such as it is inherited from the parent "controller" state <view id="displayView" extends="view"> <on event="cancel" to="canceled"/> .



Теги: #навигация по сайту #корпоративная #java #vaadin #lexaden webflow #разработка веб-сайтов #java #Анализ и проектирование систем

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.