打开视图

一个常规视图(非主视图)可以通过两种方式打开:通过 URL(在 @Route 注解指定)导航打开;在当前页面通过对话框形式打开。

使用导航方式时,浏览器的 URL 会根据应用程序 UI 的状态展示不同的 URL,并支持 深度链接。应用程序的用户可以复制特定视图和内容的 URL 并分享,例如,展示某个实体详情的 URL。

导航方式的缺点就是打开指定视图的传参相对复杂。只能以 路由查询 参数的形式在 URL 中拼接。此外,关闭视图时,也没有办法给调用端返回任何信息。

当通过对话框窗口的形式打开视图时,浏览器 URL 不会有变化,但是调用方代码可以访问打开的视图实例。因此,可以支持给视图实例传递参数或者在关闭视图时返回一些结果值。例如,在选择控件中打开列表视图选择并返回实体实例时就使用了这种方式。

主菜单通过导航的方式打开视图。标准操作 list_createlist_edit 也是通过导航方式打开详情视图,但是可以配置使用对话框方式。The entity_lookup 标准操作始终使用对话框的方式打开列表视图,以便能返回选中的实体。

下面我们介绍如何在应用程序代码中以编程的方式打开视图。

如需通过 URL 打开视图,可以注入 ViewNavigators bean,然后使用流式接口指定当前视图和打开的视图类:

@Autowired
private ViewNavigators viewNavigators;

private void navigateToView() {
    viewNavigators.view(this, MyOnboardingView.class).navigate();
}

navigate() 是最终实现导航跳转的方法。

如果调用的类不是一个视图,则不能以当前视图的身份传递 this,而应该使用 UiComponentUtils.getCurrentView() 获取当前打开的视图。

如果希望在打开的视图关闭后返回调用的视图,则使用 withBackwardNavigation(true) 方法:

@Autowired
private ViewNavigators viewNavigators;

private void navigateToViewThenBack() {
    viewNavigators.view(this, MyOnboardingView.class)
            .withBackwardNavigation(true)
            .navigate();
}

后退导航限制

后退导航仅支持单层视图嵌套。导航栈不会存储整个视图转换链。

由于此限制:

  • 从详情视图打开的视图必须使用 DIALOG 打开模式。

  • 导航历史仅限保存直接的父子关系。

  • 无法使用后退导航恢复深层导航链接。

如需导航至一个列表视图,使用 listView() 方法并传递实体类。会根据 视图推断约定 选择列表视图的类。示例:

@Autowired
private ViewNavigators viewNavigators;

private void navigateToListView() {
    viewNavigators.listView(this, Department.class).navigate();
}

也可以显式指定列表视图类或 id,示例:

@Autowired
private ViewNavigators viewNavigators;

private void navigateToListViewWithClass() {
    viewNavigators.listView(this, Department.class)
            .withViewClass(DepartmentListView.class)
            .navigate();
}

如需导航至详情视图,使用 detailView() 方法并传递实体类或一个绑定至该实体的可视化组件。会根据 视图推断约定 选择详情视图的类。

如需新建一个实体,调用 newEntity(),示例:

@Autowired
private ViewNavigators viewNavigators;

private void navigateToCreateEntity() {
    viewNavigators.detailView(this, Department.class)
            .newEntity()
            .navigate();
}

编辑实体实例,调用 editEntity(),示例:

@Autowired
private ViewNavigators viewNavigators;

private void navigateToEditEntity(Department entity) {
    viewNavigators.detailView(this, Department.class)
            .editEntity(entity)
            .navigate();
}

也可以显式指定详情视图类或 id,使用 withViewId()withViewClass() 方法。

传递参数

推荐使用流式接口的 withQueryParameters() 方法为导航打开的视图传递参数:

@Autowired
private ViewNavigators viewNavigators;

private void navigateToViewWithQueryParameters() {
    viewNavigators.view(this, FancyMessageView.class)
            .withQueryParameters(QueryParameters.of("message", "Hello World!"))
            .navigate();
}

此时,会在 URL 中添加参数,例如:

http://localhost:8080/FancyMessageView?message=Hello%20World!

在打开的视图中,可以使用 QueryParametersChangeEvent 处理器 获取参数:

@ViewComponent
private H1 messageLabel;

public void setMessage(String message) {
    messageLabel.setText(message);
}

@Subscribe
public void onQueryParametersChange(final QueryParametersChangeEvent event) {
    List<String> messageParams = event.getQueryParameters().getParameters().get("message");
    if (messageParams != null && !messageParams.isEmpty())
        setMessage(messageParams.get(0));
}

另一个方法是使用流式接口的 withAfterNavigationHandler() 方法直接将参数传递给打开的视图对象:

@Autowired
private ViewNavigators viewNavigators;

private void navigateToViewWithAfterNavigationHandler() {
    viewNavigators.view(this, FancyMessageView.class)
            .withAfterNavigationHandler(afterViewNavigationEvent -> {
                FancyMessageView view = afterViewNavigationEvent.getView();
                view.setMessage("Hello World!");
            })
            .navigate();
}

此时,URL 中不包含参数:

http://localhost:8080/FancyMessageView

这种方式更简单,并且支持传递复杂类型的参数,但是缺点也很明显,与在弹窗中打开视图一样,这种方式不会提供深度链接,用户在刷新网页时会丢失视图状态。

打开弹窗

DialogWindows bean 提供流式接口用于以对话框窗口的形式打开视图。流式接口的结束方法提供对打开视图实例的访问。支持直接向视图实例传递参数,并可以添加监听器,在打开的视图关闭后获取结果。

基本方法

如需以对话框的形式打开一个视图,可以注入 DialogWindows bean,调用 view() 方法,传递当前视图和需要打开的视图类。然后调用 open() 方法:

@Autowired
private DialogWindows dialogWindows;

private void openView() {
    dialogWindows.view(this, MyOnboardingView.class).open();
}

设置弹窗参数

用几种方式可以设置打开的弹窗参数,包括尺寸:

  1. 使用 @DialogMode 注解

    在需要以弹窗方式打开的视图类上添加 @DialogMode 注解。这里可以设置视图弹窗的默认大小、是否模态窗以及其他弹窗参数。

    @ViewController("MyOnboardingView")
    @ViewDescriptor("my-onboarding-view.xml")
    @DialogMode(width = "50em", height = "40em", modality = DialogModalityMode.STRICT)
    public class MyOnboardingView extends StandardView {
        // some code
    }

    使用 modality 属性控制对话框与 UI 其余部分的交互方式。可选值包括:

    • STRICT - 显示模态遮罩,将焦点限制在对话框内,阻止鼠标指针事件,并阻止后台请求。

    • VISUAL - 行为与 STRICT 相同,但不阻止后台请求。

    • MODELESS - 不使用模态:无模态遮罩、无焦点限制、无后台阻止。

    • DEFAULT - 兼容模式,保留已弃用的 DialogMode.modal() 属性配置的行为。

  2. 在打开的视图内部设置大小

    可以在视图控制器中修改视图弹窗的大小。有助于依赖运行时条件动态调整弹窗的大小,例如需要展示或隐藏某些页面组件。

    @Subscribe
    public void onBeforeShow(BeforeShowEvent event) {
        Dialog dialog = UiComponentUtils.findDialog(this);
        if (dialog != null) {
            dialog.setWidth("80em");
            dialog.setHeight("60em");
        }
    }
    如果当前视图不是以对话框形式打开的,则 findDialog() 方法返回 null。因此需要检查对话框实例是否为 null
  3. 打开弹窗时设置大小

    使用 DialogWindows bean 时,可以在构建对话框窗口之后、打开之前设置大小。

    @Autowired
    private DialogWindows dialogWindows;
    
    private void openViewWithSize() {
        DialogWindow<MyOnboardingView> window =
                dialogWindows.view(this, MyOnboardingView.class).build();
        window.setWidth("40em");
        window.setHeight("30em");
        window.open();
    }

传递参数

如果希望为打开的视图传递参数,调用 build(),然后设置参数,再打开窗口:

@Autowired
private DialogWindows dialogWindows;

private void openViewWithParams(String username) {
    DialogWindow<MyOnboardingView> window =
            dialogWindows.view(this, MyOnboardingView.class).build();
    window.getView().setUsername(username);
    window.open();
}
调用 build() 方法时,会触发 InitEvent。如果打开一个对话框创建新实体,则在调用 build() 方法时还会触发 InitEntityEvent。视图的其他 生命周期事件 将在调用 open() 方法时触发。

结果处理

如需获取视图关闭后的结果,可以添加对话框的 AfterCloseEvent 监听器:

@Autowired
private DialogWindows dialogWindows;

private void openViewWithParamsAndResults(String username) {
    DialogWindow<MyOnboardingView> window =
            dialogWindows.view(this, MyOnboardingView.class).build();
    window.getView().setUsername(username);
    window.addAfterCloseListener(afterCloseEvent -> {
        if (afterCloseEvent.closedWith(StandardOutcome.SAVE)) {
            // ...
        }
    });
    window.open();
}

AfterCloseEvent 对象包含传递给视图 close() 方法的 CloseAction(关闭操作)参数值。例如,当使用 OK 按钮关闭标准实体详情视图时,关闭操作为 save。可以使用事件对象的 getCloseAction()closedWith() 方法分析关闭操作。

使用流式接口也可以添加 AfterCloseEvent 监听器:

@Autowired
private DialogWindows dialogWindows;

private void openViewWithResults() {
    dialogWindows.view(this, MyOnboardingView.class)
            .withAfterCloseListener(afterCloseEvent -> {
                if (afterCloseEvent.closedWith(StandardOutcome.SAVE)) {
                    // ...
                }
            })
            .open();
}

选择实体

如需使用列表视图选择实体,请使用 lookup() 方法打开视图:

@Autowired
private DialogWindows dialogWindows;

private void openLookupView() {
    dialogWindows.lookup(this, Department.class)
            .withSelectHandler(departments -> {
                // ...
            })
            .open();
}

使用 withSelectHandler() 方法提供一个 lambda,用于接收打开视图中选择的实体实例集合。

创建并编辑实体

如需在详情视图中新建一个实体,先指定视图类,然后调用 newEntity()。使用 AfterCloseEvent 监听器获取创建的实体。示例:

@Autowired
private DialogWindows dialogWindows;

private void openDetailViewToCreateEntity() {
    dialogWindows.detail(this, Department.class)
            .withViewClass(DepartmentDetailView.class)
            .newEntity()
            .withAfterCloseListener(afterCloseEvent -> {
                if (afterCloseEvent.closedWith(StandardOutcome.SAVE)) {
                    Department department = afterCloseEvent.getView().getEditedEntity();
                    // ...
                }
            })
            .open();
}

如需在详情视图编辑已有实体,使用 editEntity() 方法提供需要编辑的实体实例:

@Autowired
private DialogWindows dialogWindows;

private void openDetailViewToEditEntity(Department department) {
    dialogWindows.detail(this, Department.class)
            .withViewClass(DepartmentDetailView.class)
            .editEntity(department)
            .withAfterCloseListener(afterCloseEvent -> {
                if (afterCloseEvent.closedWith(StandardOutcome.SAVE)) {
                    Department editedDepartment = afterCloseEvent.getView().getEditedEntity();
                    // ...
                }
            })
            .open();
}

可以用本章开始时介绍的方法为列表和详情视图提供输入参数:先调用 build(),然后给视图设置参数,最后再打开窗口。

其他构建方法

DetailWindowBuilderDetailWindowClassBuilder 提供了额外的方法,可将打开的详情视图与调用视图集成:

方法 描述

withInitializer()

在详情视图打开之前初始化一个新的实体实例。初始化器仅在创建模式下被调用,即在 newEntity()newEntity(entity) 之后。

withContainer()

指定在详情视图以 SAVE 关闭后需要更新的 CollectionContainer。如果该容器是嵌套的(如 属性容器 中所述),框架还会为新的子实体初始化父实体引用,并为组合编辑配置父子 DataContext 关系。

withListDataComponent()

指定一个 ListDataComponent,例如 DataGrid,当没有通过 withContainer() 显式提供数据容器时,框架将从该组件获取绑定的集合容器。对话框关闭后,该组件也会获得焦点。

withParentDataContext()

为打开的详情视图设置父 DataContext。此时,详情视图会将更改保存到父数据上下文,而不是直接保存到数据存储。当子实体必须与主实体一起保存时,请使用此方法。有关信息,请参阅 父数据上下文

withTransformation()

将保存的实体写回字段、容器或列表数据组件之前对其进行特定的转换。

withField()

设置一个字段组件,该组件在详情视图以 SAVE 关闭后接收保存的实体。

withAddFirst()

当使用独立的集合容器时,定义新实体是否应添加到集合容器的开头位置。对于嵌套容器,新实体始终添加到末尾。

withViewConfigurer()

支持在对话框显示之前配置打开的视图实例。

这些构建器方法通常在为子实体打开详情对话框时使用。

使用 withListDataComponent()withContainer() 将对话框与当前视图中显示的集合进行关联,使用 withParentDataContext() 将编辑后的子实体合并到父视图数据上下文中。

具体设置取决于视图数据模型和实体关系配置。完整示例请参考 组合指南。有关父子数据上下文的更多信息,请参考 父数据上下文。其他示例可在 UI 示例 中找到。

视图推断约定

框架支持从实体类推断列表或详情视图。

当使用 viewNavigators.listView(this, SomeEntity.class) 方法导航至列表视图时,框架会按照下列顺序选择视图类:

  1. 视图使用了注解 @PrimaryLookupView(SomeEntity.class)

  2. 视图的 id 为 SomeEntity.list

当使用 dialogWindows.lookup(this, SomeEntity.class) 方法打开一个列表视图用于查找时,框架会按照下列顺序选择视图类:

  1. 视图使用了注解 @PrimaryLookupView(SomeEntity.class)

  2. 视图的 id 为 SomeEntity.lookup

  3. 视图使用了注解 @PrimaryListView(SomeEntity.class)

  4. 视图的 id 为 SomeEntity.list

当导航至一个详情视图,或者在对话框打开详情视图时,框架会按照下列顺序选择视图类:

  1. 视图使用了注解 @PrimaryDetailView(SomeEntity.class)

  2. 视图的 id 为 SomeEntity.detail