打开界面

可以通过 主界面标准操作(当使用浏览和编辑实体界面时)或从另外一个界面以编程方式打开。在本节,将介绍如何以编程的方式打开界面。

使用 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 注入 标签 组件。
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 bean,所以只能将它注入到界面控制器或使用 ComponentsHelper.getScreenContext(component).getScreens() 静态方法获取。

使用 ScreenBuilders Bean

ScreenBuilders bean 可以使用各种参数打开所有类型的界面。

下面是用它打开界面并且在界面关闭之后执行一些代码的例子:

@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,来打开编辑界面。我们看下面的例子使用 ScreenBuilders API 从 BaseAction按钮 处理器直接打开界面。

默认编辑界面的确定过程如下:

  1. 如果存在使用 @PrimaryEditorScreen 注解的编辑界面,则使用它。

  2. 否则,使用 id 是 <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();
}

在这种情况下,编辑界面将更新实体,但调用界面将不会接收到更新后的实例。

我们经常需要编辑某些用 TableDataGrid 组件显示的实体。那么应该使用以下调用方式,它更简洁且能自动更新表格组件:

@Autowired
private GroupTable<Customer> customersTable;

@Autowired
private ScreenBuilders screenBuilders;

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

要创建一个新的实体实例并打开它的编辑界面,只需在 builder 调用 newEntity() 方法:

@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 打开此类界面。下面的示例展示 ScreenBuilders API 的用法,如果不想用标准操作的话。

默认查找界面的确定过程如下:

  1. 如果存在使用 @PrimaryLookupScreen 注解的查找界面,则使用它。

  2. 否则,如果存在 id 为 <entity_name>.lookup 的界面,则使用它,例如,sales_Customer.lookup

  3. 否则,使用 id 为 <entity_name>.browse 的界面,例如,sales_Customer.browse.

实体查找界面也可以使用各种参数打开。下面的示例中,打开了 Customer 实体的查找界面,选中 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或者从主菜单打开的实体编辑和查找界面。

带有传参使用 ScreenBuilders 来调用 FancyMessageScreen 如下所示:

@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 处理器通过界面的公共 setters 传递参数。

另一个方式是为参数定义一个特殊的类,然后在 SreenBuilder 中将该类的实例传递给标准的 withOptions() 方法。参数类必需实现 ScreenOptions 标记接口。示例:

public class FancyMessageOptions implements ScreenOptions {

    private String message;

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

    public String getMessage() {
        return message;
    }
}

在打开的 FancyMessageScreen 界面,可以通过 InitEventAfterInitEvent 处理器获取参数:

@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);
    }
}

带有传递 ScreenOptions 参数使用 ScreenBuilders 来调用 FancyMessageScreen 如下所示:

@Autowired
private ScreenBuilders screenBuilders;

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

可以看到,这个方式需要在控制界接收参数的时候进行类型转换,所以需要谨慎使用。推荐还是用上面介绍的类型安全的使用 setter 的方式。

如果使用类似 CreateAction 的标准操作打开界面,可以用它的 screenOptionsSupplier 处理器创建并初始化所需的 ScreenOptions 对象。

如果界面是基于老 API 从另一个界面打开的,那么使用 ScreenOptions 对象是唯一能获取到参数的方法。此时,参数对象是 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 对象。

前一种方法比较简单,但是后一种方法比较灵活。

先看看第一种方式:使用标准输出关闭界面然后在调用代码内进行检测。将调用下面这个界面:

@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 按钮时,创建自定义关闭操作,并设置结果值。
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 返回值时,调用方不需要知道打开的界面类是什么,因为不会调用具体的界面控制器内的方法。所以界面可以只通过其字符串 id 来创建。

当然,在使用 ScreenBuilders 打开界面时,也可以使用相同的方式通过关闭操作返回结果。