3.5.1.5. Примеси экранов
Примеси позволяют создавать функциональность, которая может быть переиспользована во разных экранах без необходимости наследования этих экранов от общих базовых классов. Примеси реализуются с помощью интерфейсов Java с default-методами.
Примеси имеют следующие характеристики:
-
Экран может иметь несколько примесей.
-
В интерфейсе примеси можно подписываться на события экрана.
-
Если необходимо, примесь может сохранять некоторое состояние в экране.
-
Примесь может обращаться к компонентам экрана и инфраструктурным бинам, например Dialogs, Notifications, и пр.
-
Для параметризации поведения примеси, она может полагаться на аннотации экрана или вводить абстрактные методы, которые должен будет реализовать экран.
Обычно использование примеси заключается просто в реализации определенного интерфейса в контроллере экрана. В примере ниже, экран CustomerEditor
получает функциональность примесей, реализованных интерфейсами HasComments
, HasHistory
и HasAttachments
:
public class CustomerEditor extends StandardEditor<Customer>
implements HasComments, HasHistory, HasAttachments {
// ...
}
Примесь может использовать следующие классы для работы с экраном и инфраструктурой:
-
com.haulmont.cuba.gui.screen.Extensions
предоставляет статические методы для сохранения и извлечения состояния из экрана, в котором примесь используется, а также для получения бинаBeanLocator
, который в свою очередь позволяет получить любой Spring бин. -
UiControllerUtils
предоставляет доступ к UI-компонентам и компонентам данных экрана.
В примерах ниже демонстрируется создание и использование примесей.
- Примесь DeclarativeLoaderParameters
-
Следующая примесь помогает устанавливать отношения master-detail между контейнерами данных. Обычно для этого необходимо подписаться на
ItemChangeEvent
master-контейнера и задать параметр для detail-загрузчика, как описано в разделе Зависимости между компонентами данных. Примесь сможет сделать это автоматически, если параметр будет иметь специальное имя, указывающее на master-контейнер.Создаваемая примесь будет использовать объект-состояние для передачи информации между обработчиками событий экрана. Это сделано в основном для целей демонстрации, так как в данном случае можно было бы разместить всю логику в единственном обработчике
BeforeShowEvent
.Сначала создадим класс объекта состояния. Он содержит единственное поле для сохранения набора загрузчиков, которые должны сработать в обработчике
BeforeShowEvent
:package com.company.demo.web.mixins; import com.haulmont.cuba.gui.model.DataLoader; import java.util.Set; public class DeclarativeLoaderParametersState { private Set<DataLoader> loadersToLoadBeforeShow; public DeclarativeLoaderParametersState(Set<DataLoader> loadersToLoadBeforeShow) { this.loadersToLoadBeforeShow = loadersToLoadBeforeShow; } public Set<DataLoader> getLoadersToLoadBeforeShow() { return loadersToLoadBeforeShow; } }
Теперь создадим интерфейс примеси:
package com.company.demo.web.mixins; import com.haulmont.cuba.gui.model.*; import com.haulmont.cuba.gui.screen.*; import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; public interface DeclarativeLoaderParameters { Pattern CONTAINER_REF_PATTERN = Pattern.compile(":(container\\$(\\w+))"); @Subscribe default void onDeclarativeLoaderParametersInit(Screen.InitEvent event) { (1) Screen screen = event.getSource(); ScreenData screenData = UiControllerUtils.getScreenData(screen); (2) Set<DataLoader> loadersToLoadBeforeShow = new HashSet<>(); for (String loaderId : screenData.getLoaderIds()) { DataLoader loader = screenData.getLoader(loaderId); String query = loader.getQuery(); Matcher matcher = CONTAINER_REF_PATTERN.matcher(query); while (matcher.find()) { (3) String paramName = matcher.group(1); String containerId = matcher.group(2); InstanceContainer<?> container = screenData.getContainer(containerId); container.addItemChangeListener(itemChangeEvent -> { (4) loader.setParameter(paramName, itemChangeEvent.getItem()); (5) loader.load(); }); if (container instanceof HasLoader) { (6) loadersToLoadBeforeShow.add(((HasLoader) container).getLoader()); } } } DeclarativeLoaderParametersState state = new DeclarativeLoaderParametersState(loadersToLoadBeforeShow); (7) Extensions.register(screen, DeclarativeLoaderParametersState.class, state); } @Subscribe default void onDeclarativeLoaderParametersBeforeShow(Screen.BeforeShowEvent event) { (8) Screen screen = event.getSource(); DeclarativeLoaderParametersState state = Extensions.get(screen, DeclarativeLoaderParametersState.class); for (DataLoader loader : state.getLoadersToLoadBeforeShow()) { loader.load(); (9) } } }
1 - подписка на InitEvent. 2 - получение объекта ScreenData
, в котором зарегистрированы все контейнеры и загрузчики данных, объявленные в XML-дескрипторе.3 - проверка, соответствует ли имя параметра загрузчика паттерну`:container$masterContainerId`. 4 - извлечение id master-контейнера из имени параметра и регистрация обработчика ItemChangeEvent
для этого контейнера.5 - перезагрузка detail-загрузчика для нового выбранного элемента в master-контейнере. 6 - добавление master-загрузчика в набор для вызова позже в обработчике BeforeShowEvent
.7 - создание объекта состояния и сохранение его в экране с помощью класса Extensions
.8 - подписка на BeforeShowEvent. 9 - вызов всех master-загрузчиков в обработчике InitEvent
.Определим master и detail контейнеры и загрузчики в XML-дескрипторе экрана. Detail-загрузчик должен иметь параметр с именем вида
:container$masterContainerId
:<collection id="countriesDc" class="com.company.demo.entity.Country" view="_local"> <loader id="countriesDl"> <query><![CDATA[select e from demo_Country e]]></query> </loader> </collection> <collection id="citiesDc" class="com.company.demo.entity.City" view="city-view"> <loader id="citiesDl"> <query><![CDATA[ select e from demo_City e where e.country = :container$countriesDc ]]></query> </loader> </collection>
В контроллере экрана достаточно добавить интерфейс примеси, и она будет вызывать загрузчики нужным образом:
package com.company.demo.web.country; import com.company.demo.entity.Country; import com.company.demo.web.mixins.DeclarativeLoaderParameters; import com.haulmont.cuba.gui.screen.*; @UiController("demo_Country.browse") @UiDescriptor("country-browse.xml") @LookupComponent("countriesTable") public class CountryBrowse extends StandardLookup<Country> implements DeclarativeLoaderParameters { }