Примеси экранов
Примеси позволяют создавать функциональность, которая может быть переиспользована во разных экранах без необходимости наследования этих экранов от общих базовых классов. Примеси реализуются с помощью интерфейсов Java с default-методами.
Примеси имеют следующие характеристики:
-
Экран может иметь несколько примесей.
-
В интерфейсе примеси можно подписываться на события экрана.
-
Если необходимо, примесь может сохранять некоторое состояние в экране.
-
Примесь может обращаться к компонентам экрана и инфраструктурным бинам, например Dialogs, Notifications и пр.
-
Для параметризации поведения примеси, она может полагаться на аннотации экрана или вводить абстрактные методы, которые должен будет реализовать экран.
Обычно использование примеси заключается просто в реализации определенного интерфейса в контроллере экрана.
Примесь может использовать следующие классы для работы с экраном и инфраструктурой:
-
io.jmix.ui.screen.Extensions
предоставляет статические методы для сохранения и извлечения состояния из экрана, в котором примесь используется, а также для получения бинаBeanLocator
, который в свою очередь позволяет получить любой Spring бин. -
io.jmix.ui.screen.UiControllerUtils
предоставляет доступ к UI-компонентам и компонентам данных экрана.
Примеры
В примерах ниже демонстрируется создание и использование примесей.
Примесь Banner
Это очень простая примесь, которая отображает надпись вверху экрана.
public interface HasBanner {
@Subscribe
default void initBanner(Screen.InitEvent event) {
ApplicationContext applicationContext = Extensions.getApplicationContext(event.getSource()); (1)
UiComponents uiComponents = applicationContext.getBean(UiComponents.class); // (2)
Label<String> banner = uiComponents.create(Label.TYPE_STRING); (3)
banner.setStyleName(ThemeClassNames.LABEL_H2);
banner.setValue("Hello, world!");
event.getSource().getWindow().add(banner, 0); (4)
}
}
1 | Получение ApplicationContext . |
2 | Получение фабрики UI-компонентов. |
3 | Создание компонента Label и установка его параметров. |
4 | Добавление компонента Label в корневой UI-компонент экрана. |
Примесь может быть использована в экране следующим образом:
@UiController("demo_Order.edit")
@UiDescriptor("demo-order-edit.xml")
@EditedEntityContainer("orderDc")
public class DemoOrderEdit extends StandardEditor<Order> implements HasBanner {
// ...
}
Примесь DeclarativeLoaderParameters
Следующая примесь помогает устанавливать отношения master-detail между контейнерами данных. Обычно для этого необходимо подписаться на ItemChangeEvent
master-контейнера и задать параметр для detail-загрузчика, как описано в разделе Зависимости между компонентами данных. Примесь сможет сделать это автоматически, если параметр будет иметь специальное имя, указывающее на master-контейнер.
Создаваемая примесь будет использовать объект-состояние для передачи информации между обработчиками событий экрана. Это сделано в основном в целях демонстрации, так как в данном случае можно было бы разместить всю логику в единственном обработчике BeforeShowEvent
.
Сначала создадим класс объекта состояния. Он содержит единственное поле для сохранения набора загрузчиков, которые должны сработать в обработчике BeforeShowEvent
:
public class DeclarativeLoaderParametersState {
private Set<DataLoader> loadersToLoadBeforeShow;
public DeclarativeLoaderParametersState(Set<DataLoader> loadersToLoadBeforeShow) {
this.loadersToLoadBeforeShow = loadersToLoadBeforeShow;
}
public Set<DataLoader> getLoadersToLoadBeforeShow() {
return loadersToLoadBeforeShow;
}
}
Теперь создадим интерфейс примеси:
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="ordersDc"
class="ui.ex1.entity.Order" fetchPlan="_base">
<loader id="ordersDl">
<query>
<![CDATA[select e from uiex1_Order e where e.customer = :container$customersDc]]>
</query>
</loader>
</collection>
<collection id="customersDc" class="ui.ex1.entity.Customer" fetchPlan="_base">
<loader id="customersDl">
<query>
<![CDATA[select e from uiex1_Customer e]]>
</query>
</loader>
</collection>
В контроллере экрана достаточно добавить интерфейс примеси, и она будет вызывать загрузчики нужным образом:
@UiController("demo_Order.browse")
@UiDescriptor("demo-order-browse.xml")
@LookupComponent("ordersTable")
public class DemoOrderBrowse extends StandardLookup<Order> implements DeclarativeLoaderParameters {
}