3.5.1.3. 打开界面
- 使用 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 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 bean,所以只能将它注入到界面控制器或使用ComponentsHelper.getScreenContext(component).getScreens()
静态方法获取。
- 使用 ScreenBuilders bean
-
通过
ScreenBuilders
bean 可以使用各种参数打开所有类型的界面。下面是用它打开界面并且在界面关闭之后执行一些代码的例子(更多细节参考这里):@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(); }
在这种情况下,编辑界面将更新实体,但调用界面将不会接收到更新后的实例。
最常见的情况是需要编辑某些用
Table
或DataGrid
组件显示的实体。那么应该使用以下调用方式,它更简洁且能自动更新表格组件:@Inject private GroupTable<Customer> customersTable; @Inject private ScreenBuilders screenBuilders; private void editSelectedEntity() { screenBuilders.editor(customersTable).build().show(); }
要创建一个新的实体实例并打开它的编辑界面,只需在构建器上调用
newEntity()
方法:@Inject private GroupTable<Customer> customersTable; @Inject private ScreenBuilders screenBuilders; private void createNewEntity() { screenBuilders.editor(customersTable) .newEntity() .build() .show(); }
默认编辑界面的确定过程如下:
-
如果存在使用@PrimaryEditorScreen注解的编辑界面,则使用它。
-
否则,使用 id 是
{entity_name}.edit
的编辑界面(例如,sales_Customer.edit
)。
界面构建器提供了许多方法来设置被打开界面的可选参数。例如,以下代码以对话框的方式打开的特定编辑界面,同时新建并初始化实体:
@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注解的查找界面,则使用它。
-
否则,如果存在 id 为
{entity_name}.lookup
的界面,则使用它(例如,sales_Customer.lookup
)。 -
否则,使用 id 为
{entity_name}.browse
的界面(例如,sales_Customer.browse
)。
与使用编辑界面一样,使用构建器方法设置打开界面的可选参数。例如,以下代码以对话框的方式打开特定的查找界面,在这个界面中查找
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或者从主菜单打开的实体编辑和查找界面。带有传参使用
ScreenBuilders
来调用FancyMessageScreen
如下所示:@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); } }
带有传递
ScreenOptions
参数使用ScreenBuilders
来调用FancyMessageScreen
如下所示:@Inject private ScreenBuilders screenBuilders; private void showFancyMessage(String message) { screenBuilders.screen(this) .withScreenClass(FancyMessageScreen.class) .withOptions(new FancyMessageOptions(message)) .build() .show(); }
可以看到,这个方式需要在控制界接收参数的时候进行类型转换,所以需要考虑清楚再使用。推荐还是用上面介绍的类型安全的使用 setter 的方式。
如果界面是基于legacy API从另一个界面打开的,那么使用
ScreenOptions
对象是唯一能获取到参数的方法。此时,参数对象是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
返回值时,调用方不需要知道打开的界面类是什么,因为不会调用具体的界面控制器内的方法。所以界面可以只通过其字符串 id 来创建。当然,在使用
ScreenBuilders
打开界面时,也可以使用相同的方式通过关闭操作返回结果。