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

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



Интерфейс Screens

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

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

Контроллер экрана
@UiController("demo_FancyMessageScreen")
@UiDescriptor("fancy-message-screen.xml")
@DialogMode(forceDialog = true, width = "300px")
public class FancyMessageScreen extends Screen {

    @Inject
    private Label<String> messageLabel;

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

    @Subscribe("closeBtn")
    protected void onCloseBtnClick(Button.ClickEvent event) {
        closeWithDefaultAction();
    }
}
1 - параметр экрана
XML-дескриптор экрана
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Fancy Message">
    <layout>
        <label id="messageLabel" value="A message" stylename="h1"/>
        <button id="closeBtn" caption="Close"/>
    </layout>
</window>

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

@Inject
private Screens screens;

private void showFancyMessage(String message) {
    FancyMessageScreen screen = screens.create(FancyMessageScreen.class);
    screen.setFancyMessage(message);
    screens.show(screen);
}

Обратите внимание, что мы сначала создаём экземпляр экрана, передаём в него параметр, а затем отображаем экран.

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

@Inject
private Screens screens;

private void showDefaultFancyMessage() {
    screens.create(FancyMessageScreen.class).show();
}

Screens не является Spring-бином, поэтому его можно только инжектировать в контроллер экрана или получить его с помощью статического метода ComponentsHelper.getScreenContext(component).getScreens().

Бин ScreenBuilders

Бин ScreenBuilders позволяет открывать все типы экранов с различными параметрами. Ниже приведён пример вызова экрана и выполнения некоторого кода после того, как экран закрывается (более подробно см. здесь):

@Inject
private ScreenBuilders screenBuilders;
@Inject
private Notifications notifications;

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

Далее мы рассмотрим работу с экранами редактирования и выбора сущностей.

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

@Inject
private ScreenBuilders screenBuilders;

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

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

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

@Inject
private GroupTable<Customer> customersTable;
@Inject
private ScreenBuilders screenBuilders;

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

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

@Inject
private GroupTable<Customer> customersTable;
@Inject
private ScreenBuilders screenBuilders;

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

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

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

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

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

@Inject
private GroupTable<Customer> customersTable;
@Inject
private ScreenBuilders screenBuilders;

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

private void createNewEntity() {
    screenBuilders.editor(customersTable)
            .newEntity()
            .withInitializer(customer -> {          // lambda to initialize new instance
                customer.setName("New customer");
            })
            .withScreenClass(CustomerEdit.class)    // specific editor screen
            .withLaunchMode(OpenMode.DIALOG)        // open as modal dialog
            .build()
            .show();
}

Экраны выбора сущностей также можно открывать с различными параметрами.

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

@Inject
private TextField<String> userField;
@Inject
private ScreenBuilders screenBuilders;

private void lookupUser() {
    screenBuilders.lookup(User.class, this)
            .withSelectHandler(users -> {
                User user = users.iterator().next();
                userField.setValue(user.getName());
            })
            .build()
            .show();
}

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

@Inject
private PickerField<User> userPickerField;
@Inject
private ScreenBuilders screenBuilders;

private void lookupUser() {
    screenBuilders.lookup(User.class, this)
            .withField(userPickerField)     // set result to the field
            .build()
            .show();
}

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

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

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

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

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

@Inject
private TextField<String> userField;
@Inject
private ScreenBuilders screenBuilders;

private void lookupUser() {
    screenBuilders.lookup(User.class, this)
            .withScreenId("sec$User.browse")          // specific lookup screen
            .withLaunchMode(OpenMode.DIALOG)        // open as modal dialog
            .withSelectHandler(users -> {
                User user = users.iterator().next();
                userField.setValue(user.getName());
            })
            .build()
            .show();
}
Передача параметров в экраны

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

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

@Inject
private ScreenBuilders screenBuilders;

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

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

import com.haulmont.cuba.gui.screen.ScreenOptions;

public class FancyMessageOptions implements ScreenOptions {

    private String message;

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

    public String getMessage() {
        return message;
    }
}

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

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

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

@Inject
private ScreenBuilders screenBuilders;

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

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

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

@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 после своего закрытия. Экрану можно добавить слушатель для нотификации об этом событии, например:

@Inject
private Screens screens;
@Inject
private Notifications notifications;

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

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

@Inject
private ScreenBuilders screenBuilders;
@Inject
private Notifications notifications;

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

Объект события предоставляет информацию о том, как экран был закрыт: его метод getCloseAction() возвращает объект с интерфейсом CloseAction. Интерфейс FrameOwner, реализуемый контроллерами экранов, содержит несколько констант, определяющих реализации CloseAction, используемые фреймворком. В приложении можно использовать эти константы, либо определить свои собственные реализации.

Рассмотрим следующий простой экран:

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.screen.*;

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

    private String result;

    public String getResult() {
        return result;
    }

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

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

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

@Inject
private ScreenBuilders screenBuilders;
@Inject
private Notifications notifications;

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

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

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.screen.StandardCloseAction;

public class MyCloseAction extends StandardCloseAction {

    private String result;

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

    public String getResult() {
        return result;
    }
}

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

@Inject
private Screens screens;
@Inject
private Notifications notifications;

private void openOtherScreen() {
    Screen otherScreen = screens.create("demo_OtherScreen", 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.