Открытие экранов

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

Интерфейс Screens

Интерфейс Screens позволяет создавать и отображать экраны всех типов.

Предположим, есть экран для демонстрации сообщения с особым форматированием. Контроллер может выглядет таким образом:

@UiController("sample_FancyMessageScreen")
@UiDescriptor("fancy-message-screen.xml")
public class FancyMessageScreen extends Screen {

    @Autowired
    private Label<String> messageLabel; (1)

    public void setFancyMessage(String message) { (2)
        messageLabel.setValue(message);
    }

    @Subscribe("closeBtn")
    protected void onCloseBtnClick(Button.ClickEvent event) { (3)
        closeWithDefaultAction();
    }
}
1 Инжектирование компонента Label.
2 Метод принимает параметр экрана String.
3 Подписка на ClickEvent.

XML-дескриптор:

<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://fancyMessageScreen.caption">
    <layout>
        <label id="messageLabel" value="A message" stylename="h1"/>
        <button id="closeBtn" caption="Close"/>
    </layout>
</window>

Теперь его можно создать и открыть из другого экрана следующим образом:

@Autowired
private Screens screens;

private void showFancyScreen(String message) {
    FancyMessageScreen fancyScreen = screens.create(FancyMessageScreen.class); (1)
    fancyScreen.setFancyMessage(message); (2)
    screens.show(fancyScreen); (3)
}
1 Создает экземпляр экрана.
2 Передает параметр для экрана.
3 Показывает экран.

Если экран не требует передачи параметров из вызывающего кода, его можно создать и открыть одной строкой:

@Autowired
private Screens screens;

private void showDefaultFancyScreen() {
    screens.create(FancyMessageScreen.class).show();
}
Обратите внимание, что Screens не является Spring-бином, поэтому его можно только инжектировать в контроллер экрана или получить с помощью статического метода ComponentsHelper.getScreenContext(component).getScreens().

Бин ScreenBuilders

Бин ScreenBuilders позволяет открывать все типы экранов с различными параметрами.

Ниже приведен пример вызова экрана и выполнения некоторого кода после того, как экран закрывается:

@Autowired
private ScreenBuilders screenBuilders;

@Autowired
private Notifications notifications;

private void openOtherScreen() {
    screenBuilders.screen(this)
            .withScreenClass(OtherScreen.class)
            .withAfterCloseListener(e -> {
                notifications.create().withCaption("Closed").show();
            })
            .build()
            .show();
}

Открытие экранов редактирования

В большинстве случаев экраны редактирования открываются с помощью стандартных действий, таких как CreateAction. Давайте рассмотрим примеры, когда можно использовать API ScreenBuilders напрямую для открытия экрана из обработчика BaseAction или Button.

Экран редактирования по умолчанию определяется по следующей схеме:

  1. Если существует экран редактирования с аннотацией @PrimaryEditorScreen, будет использован он.

  2. Если такого экрана нет, будет использован экран с идентификатором вида <entity_name>.edit, например, sales_Customer.edit.

Пример открытия редактора по умолчанию для экземпляра сущности Customer:

@Autowired
private ScreenBuilders screenBuilders;

private void editSelectedEntity(Customer entity) {
    screenBuilders.editor(Customer.class, this)
            .editEntity(entity)
            .build()
            .show();
}

В данном примере редактор изменит экземпляр сущности Customer, но вызывающий экран не получит назад обновленную сущность.

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

@Autowired
private GroupTable<Customer> customersTable;

@Autowired
private ScreenBuilders screenBuilders;

private void editSelectedEntity() {
    screenBuilders.editor(customersTable).build().show();
}

Чтобы создать новый экземпляр сущности и открыть экран его редактирования, достаточно вызвать метод newEntity() builder’а:

@Autowired
private GroupTable<Customer> customersTable;

@Autowired
private ScreenBuilders screenBuilders;

private void createNewEntity() {
    screenBuilders.editor(customersTable)
            .newEntity()
            .build()
            .show();
}

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

@Autowired
private GroupTable<Customer> customersTable;

@Autowired
private ScreenBuilders screenBuilders;

private void editSelectedEntity(Customer entity) {
    screenBuilders.editor(Customer.class, this)
            .editEntity(entity)
            .build()
            .show();
}
private void createNewEntityWithParameter() {
    screenBuilders.editor(customersTable)
            .newEntity()
            .withInitializer(customer -> { (1)
                customer.setLevel(Level.SILVER);
            })
            .withScreenClass(CustomerEdit.class) (2)
            .withOpenMode(OpenMode.DIALOG) (3)
            .build()
            .show();
}
1 Инициализирует новый экземпляр.
2 Указывает экран редактирования.
3 Открывает диалог.

Открытие экранов выбора

Рассмотрим несколько примеров работы с экранами выбора. Как и в случае с экранами редактора, в основном такие экраны открываются с помощью стандартных действий, таких как действие EntityLookupAction. Приведенные ниже примеры показывают использование API ScreenBuilders и могут быть полезны, если стандартные действия не используются.

Экран выбора по умолчанию определяется по следующей схеме:

  1. Если существует экран выбора с аннотацией @PrimaryLookupScreen, будет использован он.

  2. Если такого экрана нет, будет использован экран с идентификатором вида <entity_name>.lookup, например, sales_Customer.lookup.

  3. Если и такого экрана нет, будет использован экран с идентификатором вида <entity_name>.browse, например, sales_Customer.browse.

Экраны выбора сущностей также можно открывать с различными параметрами. В приведенном ниже примере открывается экран выбора сущности Customer, и имя выбранного клиента записывается в textField:

@Autowired
private TextField userField;

@Autowired
private ScreenBuilders screenBuilders;

private void lookupCustomer() {
    screenBuilders.lookup(Customer.class, this)
            .withSelectHandler(customers -> {
                Customer customer = customers.iterator().next();
                userField.setValue(customer.getFirstName() + " " + customer.getLastName());
            })
            .build()
            .show();
}

Если нужно установить выбранную сущность в качестве значения поля, используйте краткую форму вызова:

@Autowired
private EntityPicker customerEntityPicker;

@Autowired
private ScreenBuilders screenBuilders;

private void lookupCustomerSelect() {
    screenBuilders.lookup(Customer.class, this)
            .withField(customerEntityPicker)
            .build()
            .show();
}

Как и в случае с экранами редактирования, можете использовать методы builder’а для передачи дополнительных параметров в открываемые экраны. Например, следующий код выбирает сущность Customer в конкретном экране выбора, открываемом в режиме диалогового окна:

@Autowired
private TextField userField;

@Autowired
private ScreenBuilders screenBuilders;

private void lookupCustomerWithParameter() {
    screenBuilders.lookup(Customer.class, this)
            .withScreenId("uiex1_Customer.browse")
            .withOpenMode(OpenMode.DIALOG)
            .withSelectHandler(users -> {
                Customer customer = users.iterator().next();
                userField.setValue(customer.getFirstName() + " " + customer.getLastName());
            })
            .build()
            .show();
}

Передача параметров в экраны

Рекомендуемый способ передачи параметров в открываемый экран – использование публичных setter-методов контроллера, как продемонстрировано в примере выше. С помощью такого подхода можно передавать параметры в экраны любого типа, в том числе экраны редактирования и выбора сущностей, открываемые через ScreenBuilders или из главного меню.

Пример вызова FancyMessageScreen с передачей параметра и использованием ScreenBuilders:

@Autowired
private ScreenBuilders screenBuilders;

private void showFancyScreen(String message) {
    FancyMessageScreen screen = screenBuilders.screen(this)
            .withScreenClass(FancyMessageScreen.class)
            .build();
    screen.setFancyMessage(message);
    screen.show();
}

Если экран открывается из стандартного действия, такого как CreateAction, используйте его обработчик screenConfigurer для передачи параметров через публичные сеттеры контроллера экрана.

Другой способ – определить специальный класс для параметров и передавать экземпляр этого класса в стандартный метод withOptions() билдера. Класс параметров должен реализовывать маркер-интерфейс ScreenOptions. Например:

public class FancyMessageOptions implements ScreenOptions {

    private String message;

    public FancyMessageOptions(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

В открываемом экране FancyMessageScreen, объект параметров может быть получен в обработчиках InitEvent и AfterInitEvent:

@Autowired
private Label<String> messageLabel; (1)

@Subscribe
public void onInit(InitEvent event) {
    ScreenOptions options = event.getOptions();
    if (options instanceof FancyMessageOptions) {
        String message = ((FancyMessageOptions) options).getMessage();
        messageLabel.setValue(message);
    }
}

Пример вызова экрана FancyMessageScreen через ScreenBuilders с передачей ScreenOptions:

@Autowired
private ScreenBuilders screenBuilders;

private void showFancyScreen(String message) {
    screenBuilders.screen(this)
            .withScreenClass(FancyMessageScreen.class)
            .withOptions(new FancyMessageOptions(message))
            .build()
            .show();
}

Как видите, данный подход требует приведения типов в контроллере, получающем параметры, поэтому используйте его только когда это необходимо и предпочитайте type-safe подход с setter-методами, описанный выше.

Если экран открывается из стандартного действия, такого как CreateAction, используйте его обработчик screenOptionsSupplier для создания и инициализации требуемого объекта ScreenOptions.

Использование объекта ScreenOptions является единственным способом получения параметров, если экран открывается из другого экрана, основанного на устаревшем API. В этом случае объект параметров имеет тип MapScreenOptions и может быть обработан следующим образом:

@Autowired
private Label<String> messageLabel;

@Subscribe
private void onInit(InitEvent event) {
    ScreenOptions options = event.getOptions();
    if (options instanceof MapScreenOptions) {
        String message = (String) ((MapScreenOptions) options).getParams().get("message");
        messageLabel.setValue(message);
    }
}

Выполнение кода после закрытия и возврат значений

Каждый экран посылает событие AfterCloseEvent после своего закрытия. Экрану можно добавить слушатель для уведомления об этом событии, например:

@Autowired
private Screens screens;

@Autowired
private Notifications notifications;

private void openOtherScreen() {
    OtherScreen otherScreen = screens.create(OtherScreen.class);
    otherScreen.addAfterCloseListener(afterCloseEvent -> {
        notifications.create().withCaption("Closed " + afterCloseEvent.getSource()).show();
    });
    otherScreen.show();
}

При использовании ScreenBuilders, слушатель можно передать в методе withAfterCloseListener():

@Autowired
private Notifications notifications;

@Autowired
private ScreenBuilders screenBuilders;

private void openOtherScreen() {
    screenBuilders.screen(this)
            .withScreenClass(OtherScreen.class)
            .withAfterCloseListener(afterCloseEvent -> {
                notifications.create().withCaption("Closed " + afterCloseEvent.getSource()).show();
            })
            .build()
            .show();
}

Объект события предоставляет информацию о том, как экран был закрыт. Эта информация может быть получена двумя способами:

  • Проверкой, был ли экран закрыт с одним из стандартных значений перечисления StandardOutcome.

  • Получением объекта CloseAction. Первый способ проще, второй более гибкий.

Рассмотрим первый подход: закрытие экрана с указанием StandardOutcome и его проверкой в вызывающем коде. Вызывается следующий экран:

@UiController("sample_OtherScreen")
@UiDescriptor("other-screen.xml")
public class OtherScreen extends Screen {

    private String result;

    public String getResult() {
        return result;
    }

    @Subscribe("okBtn")
    public void onOkBtnClick(Button.ClickEvent event) {
        result = "Done";
        close(StandardOutcome.COMMIT); (1)
    }

    @Subscribe("cancelBtn")
    public void onCancelBtnClick(Button.ClickEvent event) {
        close(StandardOutcome.CLOSE); (2)
    }
}
1 При нажатии кнопки Ok, установить некоторое результирующее значение и закрыть экран со значением перечисления StandardOutcome.COMMIT.
2 При нажатии кнопки Cancel, закрыть экран с StandardOutcome.CLOSE.

Теперь в слушателе AfterCloseEvent можно проанализировать, как экран был закрыт, с помощью метода closedWith() события, и, если необходимо, прочитать возвращаемое экраном значение:

@Autowired
private ScreenBuilders screenBuilders;

@Autowired
private Notifications notifications;

private void openOtherScreen() {
    screenBuilders.screen(this)
            .withScreenClass(OtherScreen.class)
            .withAfterCloseListener(afterCloseEvent -> {
                OtherScreen otherScreen = afterCloseEvent.getSource();
                if (afterCloseEvent.closedWith(StandardOutcome.COMMIT)) {
                    String result = otherScreen.getResult();
                    notifications.create().withCaption("Result: " + result).show();
                }
            })
            .build()
            .show();
}

Использование собственного CloseAction

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

public class MyCloseAction extends StandardCloseAction {

    private String result;

    public MyCloseAction(String result) {
        super("myCloseAction");
        this.result = result;
    }

    public String getResult() {
        return result;
    }
}

Теперь можно использовать данное действие при закрытии экрана:

@UiController("sample_NewOtherScreen")
@UiDescriptor("new-other-screen.xml")
public class NewOtherScreen extends Screen {

    @Subscribe("okBtn")
    public void onOkBtnClick(Button.ClickEvent event) {
        close(new MyCloseAction("Done")); (1)
    }

    @Subscribe("cancelBtn")
    public void onCancelBtnClick(Button.ClickEvent event) {
        closeWithDefaultAction(); (2)
    }
}
1 При нажатии кнопки Ok, создать экземпляр CloseAction и передать ему результирующее значение.
2 При нажатии кнопки Cancel, закрыть экран с действием закрытия по умолчанию, предоставляемым фреймворком.

В слушателе AfterCloseEvent можно получить CloseAction из объекта события и прочитать результирующее значение:

@Autowired
private Screens screens;

@Autowired
private Notifications notifications;

private void openNewOtherScreen() {
    Screen otherScreen = screens.create("sample_NewOtherScreen", OpenMode.THIS_TAB);
    otherScreen.addAfterCloseListener(afterCloseEvent -> {
        CloseAction closeAction = afterCloseEvent.getCloseAction();
        if (closeAction instanceof MyCloseAction) {
            String result = ((MyCloseAction) closeAction).getResult();
            notifications.create().withCaption("Result: " + result).show();
        }
    });
    otherScreen.show();
}

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

Разумеется, данный подход к возврату значений через действия закрытия может использоваться и при открытии экранов с помощью ScreenBuilders.