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(); }
Редактор сущности по умолчанию определяется по следующей схеме:
-
Если существует экран редактирования с аннотацией @PrimaryEditorScreen, будет использован он.
-
Если такого экрана нет, будет использован экран с идентификатором вида
{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(); }
Экран выбора сущности по умолчанию определяется по следующей схеме:
-
Если существует экран выбора с аннотацией @PrimaryLookupScreen, будет использован он.
-
Если такого экрана нет, будет использован экран с идентификатором вида
{entity_name}.lookup
(например,sales_Customer.lookup
). -
Если и такого экрана нет, будет использован экран с идентификатором вида
{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
.