Примеси экранов

Примеси позволяют создавать функциональность, которая может быть переиспользована во разных экранах без необходимости наследования этих экранов от общих базовых классов. Примеси реализуются с помощью интерфейсов 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 {
}