打开视图

一个常规视图(非主视图)可以通过两种方式打开:通过 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")
    public class MyOnboardingView extends StandardView {
        // some code
    }
  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(),然后给视图设置参数,最后再打开窗口。

视图推断约定

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

当使用 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