7. 从头创建 UI
到这一步,应用程序已经具备了管理员和 HR 经理使用的所有功能:可以配置部门和入职步骤,管理用户,生成并跟踪每个新员工的入职步骤。
现在我们需要为员工创建 UI,支持他们管理自己的入职流程。员工需要能登录系统并打开 My Onboarding
界面查看自己的入职步骤。每个步骤可以通过勾选复选框的方式标记为已完成。超期的步骤需要被高亮显示以作提醒。
下面是 My Onboarding
界面的一个原型:
前面的章节中,我们是通过自动生成和修改实体的 CRUD 界面的方式来创建 UI。本节中,我们将从头创建 My Onboarding
界面。
创建空界面
如果你的应用程序正在运行,先通过主工具栏的 Stop()按钮停止运行。
在 Jmix 工具窗口中,点击 New()→ Screen:
Create Jmix Screen 窗口中,选择 Blank screen
(空界面)模板:
点击 Next。
向导的下一步中,输入:
-
Package name:
com.company.onboarding.screen.myonboarding
-
Descriptor name:
my-onboarding-screen
-
Controller name:
MyOnboardingScreen
点击 Next。
向导的下一步中,修改界面标题为 My onboarding
:
点击 Create。
Studio 会创建一个空界面,并在设计器打开:
新界面也会被添加到主菜单中。在 Jmix 工具窗口,双击 User Interface → Main Menu 节点,切换到 Structure 标签页,将 MyOnboardingScreen
拖放至顶部:
点击主工具栏中的 Debug()按钮启动应用程序。在浏览器打开 http://localhost:8080
然后登录应用程序。
点击主菜单的 Application → My onboarding,会打开一个空界面。
添加表格
我们先给界面添加一个表格,用来展示当前用户的入职步骤。
定义数据容器
首先,添加一个数据容器,用于为 UI 表格提供 UserStep
实体集合。点击操作面板的 Add Component,选择 Data components
并双击 Collection
,在弹出的对话框中 Entity 字段选择 UserStep
,点击 OK:
Studio 会创建集合数据容器:
<data>
<collection id="userStepsDc"
class="com.company.onboarding.entity.UserStep">
<fetchPlan extends="_base"/>
<loader id="userStepsDl">
<query>
<![CDATA[select e from UserStep e]]>
</query>
</loader>
</collection>
</data>
加载数据
默认的查询语句会加载所有的 UserStep
实例,但是这里我们仅需要加载当前用户的入职步骤,且有特定的顺序。我们用 JPQL 设计器修改这个查询语句。在 Jmix UI 层级面板中选择 userStepsDc
容器,然后点击 query
属性的值。然后添加一个使用 :user
参数的 where
子句,以及一个 order by
子句:
形成的查询语句如下:
<query>
<![CDATA[select e from UserStep e
where e.user = :user
order by e.sortValue asc]]>
</query>
下一个任务是为 :user
参数提供一个值。可以在界面的 BeforeShowEvent
处理方法中实现。切换至 MyOnboardingScreen
控制器类,点击顶部操作面板的 Generate Handler 按钮,选择 Controller handlers → BeforeShowEvent
:
点击 OK。Studio 会生成一个处理方法的桩代码:
@UiController("MyOnboardingScreen")
@UiDescriptor("my-onboarding-screen.xml")
public class MyOnboardingScreen extends Screen {
@Subscribe
public void onBeforeShow(BeforeShowEvent event) {
}
}
需要获取当前登录的用户,然后将该用户设置到加载器的查询参数中。
点击操作面板的 Code Snippets 生成获取当前用户的代码:
然后注入加载器,将 :user
参数设置为当前用户并调用 load()
方法执行查询语句,将数据加载至集合数据容器:
加载数据至集合容器的代码如下:
@Autowired
private CurrentAuthentication currentAuthentication;
@Autowired
private CollectionLoader<UserStep> userStepsDl;
@Subscribe
public void onBeforeShow(BeforeShowEvent event) {
User user = (User) currentAuthentication.getUser();
userStepsDl.setParameter("user", user);
userStepsDl.load();
}
通过 Studio 生成的实体浏览或编辑界面中,数据加载默认是通过
这就是前面几章节中我们不需要手动调用 CRUD 界面中数据加载器 |
表格配置
右键点击 Jmix UI 层级面板中的 layout
元素,选择 Add Component 菜单。找到并双击 Table
组件。在 Table Properties Editor 弹窗中,选择 userStepsDc
数据容器,然后在组件面板中设置表格的宽为 100%
,高为 400px
:
可以看到,表格没有展示步骤名称的列:
<table id="userStepsTable" height="400px" width="100%"
dataContainer="userStepsDc">
<columns>
<column id="dueDate"/>
<column id="completedDate"/>
<column id="sortValue"/>
</columns>
</table>
Step
是一个关联属性,默认不包含在表格的 fetch plan 中。前一章节 中我们在用户编辑界面展示 UserSteps 表格的时候就遇到过这种情况。
在 fetch plan 中添加 step
属性,然后在表格中添加相应的列,并删除不需要的 sortValue
列:
此时,界面的 XML 如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://jmix.io/schema/ui/window"
caption="msg://myOnboardingScreen.caption">
<data>
<collection id="userStepsDc" class="com.company.onboarding.entity.UserStep">
<fetchPlan extends="_base">
<property name="step" fetchPlan="_base"/>
</fetchPlan>
<loader id="userStepsDl">
<query>
<![CDATA[select e from UserStep e
where e.user = :user
order by e.sortValue asc]]>
</query>
</loader>
</collection>
</data>
<layout>
<table id="userStepsTable" height="400px" width="100%"
dataContainer="userStepsDc">
<columns>
<column id="step.name"/>
<column id="dueDate"/>
<column id="completedDate"/>
</columns>
</table>
</layout>
</window>
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。确保为当前用户(可能是 admin
)在用户编辑界面生成几条 UserSteps。然后重新打开 My onboarding
界面,查看入职步骤:
添加生成列
本小节中,我们将为表格添加一个带有复选框的生成列,用于标记入职步骤已完成。之前章节 中,我们为用户编辑界面的 UserSteps 表格完成了类似的任务。
XML 描述中,添加 completed
的列声明:
<table id="userStepsTable" height="400px" width="100%"
dataContainer="userStepsDc">
<columns>
<column id="completed" caption="" width="50px"/>
<column id="step.name"/>
<column id="dueDate"/>
<column id="completedDate"/>
</columns>
</table>
在控制器中,注入 UiComponents
工厂并实现 columnGenerator
处理方法:
@Autowired
private UiComponents uiComponents;
@Install(to = "userStepsTable.completed", subject = "columnGenerator")
private Component userStepsTableCompletedColumnGenerator(UserStep userStep) {
CheckBox checkBox = uiComponents.create(CheckBox.class);
checkBox.setValue(userStep.getCompletedDate() != null);
checkBox.addValueChangeListener(e -> {
if (userStep.getCompletedDate() == null) {
userStep.setCompletedDate(LocalDate.now());
} else {
userStep.setCompletedDate(null);
}
});
return checkBox;
}
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开 My onboarding
界面,测试最新的改动:
添加标签
表格基本完成了,下面我们添加展示步骤总数、完成总数和超期步骤的文本标签。
点击操作面板的 Add Component,并拖拽 Containers → VBox
(垂直布局盒子)至层级面板中的 layout
元素中,放置于 userStepsTable
之前。然后在 vbox
中添加三个 Label
组件:
设置标签的 id:
<layout>
<vbox spacing="true">
<label id="totalStepsLabel"/>
<label id="completedStepsLabel"/>
<label id="overdueStepsLabel"/>
</vbox>
现在,我们可以在控制器的代码中通过编程的方式计算并设置这些标签。切换至 MyOnboardingScreen
控制器,注入三个标签和 userStepsDc
集合容器:
@Autowired
private Label totalStepsLabel;
@Autowired
private Label completedStepsLabel;
@Autowired
private Label overdueStepsLabel;
@Autowired
private CollectionContainer<UserStep> userStepsDc;
然后添加两个方法,用于计算和设置计数器:
private void updateLabels() {
totalStepsLabel.setValue("Total steps: " + userStepsDc.getItems().size());
long completedCount = userStepsDc.getItems().stream()
.filter(us -> us.getCompletedDate() != null)
.count();
completedStepsLabel.setValue("Completed steps: " + completedCount);
long overdueCount = userStepsDc.getItems().stream()
.filter(us -> isOverdue(us))
.count();
overdueStepsLabel.setValue("Overdue steps: " + overdueCount);
}
private boolean isOverdue(UserStep us) {
return us.getCompletedDate() == null
&& us.getDueDate() != null
&& us.getDueDate().isBefore(LocalDate.now());
}
最后,从两个事件处理器中调用 updateLabels()
方法:
-
在已有的
BeforeShowEvent
处理器中调用updateLabels()
:@Subscribe public void onBeforeShow(BeforeShowEvent event) { // ... updateLabels(); }
这样在界面打开时就会更新这些标签。
-
点击 Generate Handler 并选择 Data container handlers →
userStepsDc
→ItemPropertyChangeEvent
: -
在生成的处理器中调用
updateLabels()
方法:
@Subscribe(id = "userStepsDc", target = Target.DATA_CONTAINER) public void onUserStepsDcItemPropertyChange(InstanceContainer.ItemPropertyChangeEvent<UserStep> event) { updateLabels(); }
+
有了 ItemPropertyChangeEvent
处理方法,当使用表格中的复选框修改步骤的 completedDate
属性时,也会更新标签。
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开 My onboarding
界面,测试标签值:
延展布局容器中的组件
从上面的截图可以看到,界面布局还需要调整,将标签和表格之间的空白区域想办法去掉。
此时,layout
根元素的垂直可用空间被等分成了两份:vbox
和 table
。因此,table
从界面中间开始占用下半部可用空间。
一般来说,为了占满空白区域,布局容器(这里是 layout
)内的某个组件需要进行 延展(expanded)。我们可以对表格进行延展,或者还有一种方法,就是添加一个不可见的元素,并将这个元素进行延展而保留表格本身的大小不变。
我们按照后面一种方式,添加一个没有值的标签,并对其进行延展。
将 Label
拖放至 layout
元素,设置标签 id 并在 layout
元素的 expand
属性中指定:
还需要勾选 spacing
属性的复选框。这样布局容器会在组件之间添加一定的间距。
XML 如下:
<layout expand="spacer" spacing="true">
<vbox spacing="true">
...
</vbox>
<table id="userStepsTable" ...>
...
</table>
<label id="spacer"/>
</layout>
现在 layout
将会对 spacer
标签进行延展,而不会为其内部的子元素均匀地分配空间。
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开 My onboarding
界面查看布局:
保存更改并关闭界面
在这个界面内,现在我们可以修改入职步骤的状态,但是这些改动在重新打开界面后就会丢失。我们现在为界面添加两个按钮:Save
按钮用于保存更改并关闭界面;Discard
按钮用于关闭界面不保存数据。
首先,拖拽一个 Containers → HBox
(水平布局盒子),放置于 userStepstable
和 spacer
之间。然后在其内部添加两个按钮:
设置按钮的 id 和名称。对于 Save
按钮,添加 primary="true"
属性:
<hbox spacing="true">
<button id="saveButton" caption="Save" primary="true"/>
<button id="discardButton" caption="Discard"/>
</hbox>
通过 Jmix UI 组件面板 → Handlers 生成按钮点击事件的处理方法:
在控制器类中注入 DataContext
并实现点击处理方法:
@Autowired
private DataContext dataContext;
@Subscribe("saveButton")
public void onSaveButtonClick(Button.ClickEvent event) {
dataContext.commit(); (1)
close(StandardOutcome.COMMIT); (2)
}
@Subscribe("discardButton")
public void onDiscardButtonClick(Button.ClickEvent event) {
close(StandardOutcome.DISCARD); (2)
}
1 | DataContext 会跟踪数据容器中加载实体的更改。当调用 commit() 方法时,所有对实例的改动都将保存至数据库。 |
2 | close() 方法关闭界面。接收一个“输出”对象,可以由调用方处理。 |
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开 My onboarding
界面并查看按钮:
样式调整
My onboarding
界面的最后一个需求就是通过修改 Due date
单元格的字体颜色对超期的入职步骤进行高亮展示。我们将创建一个 CSS 类,并在表格中使用。
扩展默认主题
默认情况下,应用程序使用框架自带的 Helium 主题,主题为所有的 UI 组件定义了样式。需要添加我们自己的样式时,可以基于默认的主题创建一个自定义的主题。
如果你的应用程序正在运行,先通过主工具栏的 Stop()按钮停止运行。
在 Jmix 工具窗口中,右键点击 Themes 子节点,然后选择 New → Custom Theme:
Create Custom Theme 弹窗中,在 Theme name 字段输入 helium-ext
并在 Base theme 下拉框中选择 helium
:
点击 OK。
Studio 会创建新主题的文件结构:
同时也会重新配置 build.gradle
中的依赖以及在 application.properties
文件中添加两个属性:
jmix.ui.theme.name=helium-ext
jmix.ui.theme-config=com/company/onboarding/theme/helium-ext-theme.properties
打开 styles.scss
文件,添加 overdue-step
CSS 类的样式:
@import "helium-ext-defaults";
@import "addons";
@import "helium-ext";
.helium-ext {
@include addons;
@include helium-ext;
.overdue-step {
color: red;
}
}
然后我们可以在 UI 组件的 stylename
属性中使用 overdue-step
。
添加表格样式 Provider
为表格的单元格应用自定义样式,需要给表格组件定义一个 Style Provider。
打开 MyOnboardingScreen
控制器类并点击顶部操作面板的 Generate Handler 按钮。选择 Component handlers → userStepsTable
→ styleProvider
:
点击 OK。
也可以通过 Jmix UI 组件面板的 Handlers 标签页生成处理方法。 |
实现 styleProvider
:
@Install(to = "userStepsTable", subject = "styleProvider") (1)
private String userStepsTableStyleProvider(
UserStep entity, String property) { (2)
if ("dueDate".equals(property) && isOverdue(entity)) {
return "overdue-step"; (3)
}
return null; (4)
}
1 | @Install 注解表示该方法是一个 代理(delegate):一个 UI 组件(这个 case 中是表格)会在生命周期的某个阶段调用该方法。 |
2 | 这个特殊的代理(style provider)接收一个实体实例以及单元格展示的属性名称作为参数。 |
3 | 如果该处理在 dueDate 属性调用且步骤已经超期,则处理器返回自定义样式名。 |
4 | 否则,单元格按照默认样式渲染。 |
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开 My onboarding
界面,测试超期步骤的样式:
当修改自定义主题的 CSS 时,可以在运行的应用程序中进行快速测试。打开终端并执行:
然后切换至应用程序,强制重新加载网页(Chrome 中可以按下 |
小结
本节中,我们从头开发了一个能处理数据的 UI 界面。
学习内容:
-
数据加载器 的查询语句可以带参数。参数值可以在 BeforeShowEvent 中设置,或者通过其他界面或其他 UI 组件事件处理方法中设置。
-
如需触发数据加载,可以在事件处理器中调用数据加载器的
load()
方法或为界面添加 DataLoadCoordinator facet。 -
VBox 和 HBox 容器用于将内部组件进行垂直或水平摆放。根容器
layout
本身是一个垂直布局盒子。 -
UI 容器的 expand 属性指定一个内部的组件,这个组件会占满容器内部的可用空间。如果没有使用该属性,则容器的空间由内部组件均分。
-
数据上下文 的
commit()
方法将所有实体改动保存至数据库。 -
界面可以通过由 Screen 基类提供的
close()
方法编程式关闭。 -
自定义主题 可以定义 UI 组件使用的附加样式。
-
使用 style provider 修改表格内单元格的样式。
-
代码片段 工具窗口可以用来快速查找和生成使用框架 API 的代码。
参阅 界面布局规则 了解更多关于放置 UI 组件和布局容器的信息。 |