5. UI 中处理数据
开发进行到这个阶段,我们已经有了入职步骤和部门的管理功能,以及默认的用户管理功能,并且为用户添加了 Onboarding status
属性。现在我们需要将用户与入职步骤和部门关联起来。
本节中,我们将完成:
-
为
User
实体添加department
和joiningDate
属性,并在 UI 展示。 -
创建
UserStep
实体,关联一个用户和一个入职步骤。 -
为
User
实体添加UserStep
实体集合,并在User.edit
界面展示。 -
在
User.edit
界面实现生成和保存UserStep
实例的功能。
下图展示本节要用到的实体和属性:
添加关联属性
我们已经在之前的章节完成了类似的任务:为 Department
实体添加关联至 User
实体的 HR 经理属性。现在我们需要创建一个反向连接:一个用户需要属于某个部门。
如果你的应用程序正在运行,先通过主工具栏的 Stop()按钮停止运行。
在 Jmix 工具窗口双击 User
实体并选择其最后一个属性(我们要在最后添加新属性):
在 Attributes 工具栏中,点击 Add()。
弹出的 New Attribute 对话框中,Name 字段填写 department
。然后选择:
-
Attribute type:
ASSOCIATION
-
Type:
Department
-
Cardinality:Many to One
点击 OK。
然后选中 department
属性,在 Attributes 工具栏中点击 Add to Screens()按钮:
出现的对话框中会显示 User.edit
和 User.browse
界面。我们都选上,然后点击 OK。
Studio 会在 User.browse
界面的表格组件和 User.edit
界面的表单组件中添加 department
属性。
你可能会注意,Studio 在 user-browse.xml
中添加了下面的代码:
<data readOnly="true">
<collection id="usersDc"
class="com.company.onboarding.entity.User">
<fetchPlan extends="_base">
<property name="department" fetchPlan="_base"/> <!-- added -->
</fetchPlan>
以及 user-edit.xml
:
<data>
<instance id="userDc"
class="com.company.onboarding.entity.User">
<fetchPlan extends="_base">
<property name="department" fetchPlan="_base"/> <!-- added -->
</fetchPlan>
有了这些代码,关联的部门实体会与用户实体在同一个数据库查询中加载。
即使 fetch plan 中没有包含 department ,基于关联实体的 懒加载 机制,界面也能正常工作。但是在懒加载时,关联实体是通过单独的数据库请求进行加载。因此,懒加载在浏览界面会造成性能问题,该界面首先执行一个加载所有用户的请求,然后针对每个用户,执行单独的请求加载部门(N+1 查询问题)。
|
运行应用程序可以查看新的属性。
点击主工具栏中的 Debug()按钮启动应用程序。
Studio 会生成 Liquibase 更改日志,添加了 USER_
表的 DEPARTMENT_ID
列,并创建外键和索引。确认这个改动。
Studio 会执行更改日志,然后构建并运行应用程序。
在浏览器打开 http://localhost:8080
并使用 admin
/ admin
凭证登录。
点击主菜单的 Application → Users,打开 User.browse
界面,可以看到新加的 Department 列。创建用户时,可以看到 User.edit
中的 Department 选择控件:
使用下拉框选择
默认情况下,Studio 会生成 entityPicker
组件选择关联实体。可以在 User.edit
界面中看到这样的组件。打开 user-edit.xml
并在 form
中找到 entityPicker
组件:
<layout ...>
<form id="form" dataContainer="userDc">
<column width="350px">
...
<entityPicker id="departmentField" property="department"/>
</column>
</form>
该组件支持通过一个查找界面选择实体,支持过滤、排序或者分页。但是当备选的记录相对比较少时(比如少于 1000),通过简单的下拉框列表选择会更加方便。
我们将修改 User.edit
界面,使用 entityComboBox
组件选择用户的部门。
将组件的 XML 元素修改为 entityComboBox
:
<entityComboBox id="departmentField" property="department"/>
切换至运行的应用程序,重新打开用户编辑界面。
可以看到,Department 字段现在变成了下拉框,但是列表是空的,即便已经创建了一些部门。
创建选项数据容器
我们为 entityComboBox
组件提供一组选项,用于选择关联的部门实体。选项列表包含所有的部门,按名称排序。
在操作面板点击 Add Component,选择 Data components
,然后双击 Collection
。在 Data Container Properties Editor 窗口的 Entity 字段选择 Department
,点击 OK:
然后在 Jmix UI 结构和 XML 的 data
元素下会创建 id 为 departmentsDc
的 collection
元素:
<data>
...
<collection id="departmentsDc" class="com.company.onboarding.entity.Department">
<fetchPlan extends="_base"/>
<loader id="departmentsDl">
<query>
<![CDATA[select e from Department e]]>
</query>
</loader>
</collection>
</data>
该元素定义一个 集合数据容器(collection data container),以及容器关联的一个 数据加载器(loader)。数据容器包含由加载器加载的部门实体列表,加载使用的查询语句在加载器中指定。
可以在 XML 中直接编辑查询语句,或者通过 JPQL 设计器进行编辑。在 Jmix UI 中的组件面板中,找到数据组件的 query
属性,然后点击右侧的链接打开设计器:
在 JPQL Query Designer 窗口中,切换至 ORDER 标签页并添加 name
属性:
点击 OK。
在 XML 中生成的查询语句如下:
<data>
...
<collection id="departmentsDc" class="com.company.onboarding.entity.Department">
<fetchPlan extends="_base"/>
<loader id="departmentsDl">
<query>
<![CDATA[select e from Department e
order by e.name asc]]>
</query>
</loader>
</collection>
</data>
现在需要将 entityComboBox
组件与 departmentsDc
数据容器进行关联。
在 Jmix UI 的层级面板选中 departmentField
,然后在 Jmix UI 的组件面板中为 optionsContainer
属性选择 departmentsDc
:
切换至运行的应用程序并重新打开用户编辑界面。
可以看到 Department 下拉框现在已经有了选项:
entityComboBox 组件支持直接在组件内输入的方式对选项进行过滤。过滤的过程是在服务端的内存中进行,所有的选项已经一次从数据加载出来了。
|
创建 UserStep 实体
本小节中,我们将创建 UserStep
实体,用来表示特定用户的入职步骤:
如果你的应用程序正在运行,先通过主工具栏的 Stop()按钮停止运行。
在 Jmix 工具窗口中,点击 New()→ JPA Entity 并与 之前 的步骤一样创建带有 Versioned 特性的 UserStep
实体。
为实体添加下列属性:
Name | Attribute type | Type | Cardinality | Mandatory |
---|---|---|---|---|
user |
ASSOCIATION |
User |
Many to One |
true |
step |
ASSOCIATION |
Step |
Many to One |
true |
dueDate |
DATATYPE |
LocalDate |
- |
true |
completedDate |
DATATYPE |
LocalDate |
- |
false |
sortValue |
DATATYPE |
Integer |
- |
true |
实体设计器的最终状态如下:
添加组合属性
我们来看看 User
和 UserStep
实体的关系。UserStep
实例仅当特定的 User
实例存在时才有意义(即,属于该用户)。一个 UserStep
实例不能修改其所有者;此外,其他数据模型也没有关联 UserStep
,也就是说 UserStep
实例都是包含在 User
实例内的。
在 Jmix 中,这种实体间的关系被称为 组合(composition):User
由一组 UserSteps
和其他的属性共同组成。
Jmix 中的组合实现了 DDD(Domain-Driven Design)中的聚合(Aggregate)模式。 |
在父实体中创建包含一组组合子实体的属性也很方便。
我们在 User
实体中创建 steps
属性:
如果你的应用程序正在运行,先通过主工具栏的 Stop()按钮停止运行。
在 User
实体设计器的 Attributes 工具栏中,点击 Add()。弹出的 New Attribute 对话框中,Name 字段填写 steps
,然后选择:
-
Attribute type:
COMPOSITION
-
Type:
UserStep
-
Cardinality:One to Many
注意,Mapped by 字段会自动选择 user
。这是 UserStep
实体中的一个属性,映射至一个数据库列,用于维护 UserSteps
和 Users
的关系(外键)。
点击 OK。
该属性的源代码会带有 @Composition
注解:
@Composition
@OneToMany(mappedBy = "user")
private List<UserStep> steps;
UserSteps
需要在用户编辑界面展示,因此,选中 steps
属性并点击 Attributes 工具栏中的 Add to Screens()按钮,选择 User.edit
界面,点击 OK。
Studio 会修改 user-edit.xml
:
<data>
<instance id="userDc"
class="com.company.onboarding.entity.User">
<fetchPlan extends="_base">
<property name="department" fetchPlan="_base"/>
<property name="steps" fetchPlan="_base"/> (1)
</fetchPlan>
<loader/>
<collection id="stepsDc" property="steps"/> (2)
</instance>
...
<layout ...>
<form id="form" dataContainer="userDc">
...
</form>
<groupBox id="stepsBox" ...>
<table id="stepsTable" dataContainer="stepsDc" ...> (3)
<actions>
<action id="create" type="create"/>
<action id="edit" type="edit"/>
<action id="remove" type="remove"/>
</actions>
<columns>
<column id="version"/>
<column id="dueDate"/>
<column id="completedDate"/>
<column id="sortValue"/>
</columns>
<buttonsPanel>
<button action="stepsTable.create"/>
<button action="stepsTable.edit"/>
<button action="stepsTable.remove"/>
</buttonsPanel>
</table>
</groupBox>
1 | Fetch plan 添加了 steps 属性,确保 UserSteps 与 User 一起进行预加载。 |
2 | 内部的 stepsDc 集合数据容器用于将可视化组件与 steps 集合属性做绑定。 |
3 | groupBox 内部的 table 组件用于展示 stepsDc 数据容器中的数据。 |
运行应用程序查看这些改动。
点击主工具栏中的 Debug()按钮启动应用程序。
Studio 会生成 Liquibase 更改日志,包含创建 USER_STEP
表、关联至 USER_
和 STEP
的外键约束和索引。确认这些改动。
Studio 会执行更改日志,然后运行应用程序。
应用程序准备好后,在浏览器打开 http://localhost:8080
并使用 admin
/ admin
凭证登录。
打开一个用户进行编辑。可以看到 Steps 表格展示 UserStep
实体:
如果点击 Steps 表格中的 Create 按钮,系统会抛出异常:Screen 'UserStep.edit' is not defined
。是的,我们还没有为 UserStep
实体创建编辑界面。但是实际上我们不需要创建这个界面,因为 UserStep
实例可以通过给用户分配预定义的 Step
实体生成。
为用户生成 UserSteps
本节中,我们将为编辑的 User
实体生成并展示 UserStep
实例。
添加 JoiningDate 属性
首先,为 User
实体添加 joiningDate
属性:
该属性将用于计算 UserStep
实体中的 dueDate
属性:UserStep.dueDate = User.joiningDate + Step.duration
。
如果你的应用程序正在运行,先通过主工具栏的 Stop()按钮停止运行。
在 User
实体设计器的 Attributes 工具栏中,点击 Add()。弹出的 New Attribute 对话框中,Name 字段填写 joiningDate
,然后在 Type 下拉框中选择 LocalDate
:
点击 OK。
然后选中新创建的 joiningDate
属性,在 Attributes 工具栏中点击 Add to Screens()按钮。在弹窗中选择 User.edit
和 User.browse
界面并点击 OK。
点击主工具栏中的 Debug()按钮启动应用程序。
Studio 会生成 Liquibase 更改日志,为 USER_
表添加 JOINING_DATE
列。确认此改动。
Studio 会执行更改日志,然后运行应用程序。在浏览器打开 http://localhost:8080
,登录后在用户浏览界面和编辑界面确认新属性已经添加上了。
添加自定义按钮
现在我们需要删除管理 UserSteps 的默认操作按钮,然后添加一个按钮用于启动自定义逻辑创建实体。
打开 user-edit.xml
并删除 table
内的 actions
元素和全部的 button
元素:
<table id="stepsTable" dataContainer="stepsDc" width="100%" height="200px">
<columns>
<column id="version"/>
<column id="dueDate"/>
<column id="completedDate"/>
<column id="sortValue"/>
</columns>
<buttonsPanel>
</buttonsPanel>
</table>
然后点击操作面板的 Add Component 将 Button
组件拖放至界面 XML 中的 buttonsPanel
元素上。在 Jmix UI 的组件面板选择新创建的 button
元素并设置其 id
为 generateButton
,caption
为 Generate
。切换至 Handlers 标签页,创建一个 ClickEvent
的处理器方法:
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开用户编辑界面,查看我们新添加的按钮正常展示而不是展示默认的 CRUD 按钮:
创建并保存 UserStep 实例
现在实现生成 UserStep
实例的逻辑。
在 UserEdit
控制器中添加下列字段:
public class UserEdit extends StandardEditor<User> {
@Autowired
private DataManager dataManager;
@Autowired
private DataContext dataContext;
@Autowired
private CollectionPropertyContainer<UserStep> stepsDc;
//...
}
可以通过操作面板中的 Inject 按钮注入界面中的组件和 Spring bean: |
在 generateButton
点击处理方法中添加创建和保存 UserStep
的逻辑:
@Subscribe("generateButton")
public void onGenerateButtonClick(Button.ClickEvent event) {
User user = getEditedEntity(); (1)
if (user.getJoiningDate() == null) { (2)
notifications.create()
.withCaption("Cannot generate steps for user without 'Joining date'")
.show();
return;
}
List<Step> steps = dataManager.load(Step.class)
.query("select s from Step s order by s.sortValue asc")
.list(); (3)
for (Step step : steps) {
if (stepsDc.getItems().stream().noneMatch(userStep ->
userStep.getStep().equals(step))) { (4)
UserStep userStep = dataContext.create(UserStep.class); (5)
userStep.setUser(user);
userStep.setStep(step);
userStep.setDueDate(user.getJoiningDate().plusDays(step.getDuration()));
userStep.setSortValue(step.getSortValue());
stepsDc.getMutableItems().add(userStep); (6)
}
}
}
1 | 使用 StandardEditor 父类中的 getEditedEntity() 方法获取正在编辑的 User 实体。 |
2 | 如果 joiningDate 属性未设置,展示消息并退出。 |
3 | 加载已经添加的入职步骤。 |
4 | 忽略 stepsDc 集合容器中已经存在的步骤。 |
5 | 用 DataContext.create() 方法创建新的 UserStep 实例。 |
6 | 将新的 UserStep 实例添加至 stepsDc 集合容器,以便在 UI 展示。 |
当通过 DataContext 对象创建实例时,实例自动由 DataContext 进行管理,并会在界面提交时(点击界面的 OK 按钮时)自动保存至数据库。
|
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开用户编辑界面,此时我们点击 Generate 按钮,会创建对应入职步骤的几条记录。
如果通过点击 OK 提交界面,所有创建的 UserSteps 实例都会自动保存。如果点击 Cancel,则不会保存。因为在上面的代码中,我们没有显式地保存创建的实体。而是通过 DataContext.create()
将实体合并至界面的 DataContext
内,只有在整个界面的 DataContext
提交时才会保存新创建的实例。
优化 UserSteps 表格
下面的小节中,我们将完善处理生成的 UserSteps 的 UI 界面。
内部集合的排序
你可能已经注意到,当打开一个带有生成 UserSteps 的用户界面时,这些步骤并没有按照 sortValue
属性排序:
表格展示 User
实体的 steps
集合属性,因此我们可以在数据模型级别引入排序。
打开 User
实体,选择 steps
属性并在 Order by 字段输入 sortValue
:
如果切换至 Text 标签页,可以看到 steps
属性上添加了 @OrderBy
注解:
@OrderBy("sortValue")
@Composition
@OneToMany(mappedBy = "user")
private List<UserStep> steps;
现在当加载 User
实体时,实体内部的 steps
集合会按照 UserStep.sortValue
属性进行排序。
如果应用程序正在运行,请重新启动。
打开用户编辑界面。可以看到步骤的顺序现在对了:
表格的列重排
此时,UserSteps 表格提供的信息并不是很有用。我们需要删除 Version
和 Sort value
列并添加展示步骤名称的列。
删除列很简单,在 Jmix UI 的层级面板中选择这些列并按下 Delete,或直接从 XML 代码里删除相应的元素即可。
添加列时,在 Jmix UI 的层级面板中选择 columns
元素,然后在组件面板中点击 Add → Column,会出现 Add Column 弹窗:
可以看到,这里并不允许添加步骤名称。这是因为 step
属性是一个关联属性,而我们没有定义一个合适的 fetch plan 去加载这个实体。
在 Jmix UI 的层级面板中,选择 userDc
数据容器,然后可以在 Jmix UI 组件面板中 fetchPlan
属性处或者直接在 XML 编辑器的装订线栏中点击 Edit()按钮:
Edit Fetch Plan 窗口中,选中 steps
→ step
属性,然后点击 OK:
内部的 step
属性会被添加至 fetch plan XML:
<instance id="userDc"
class="com.company.onboarding.entity.User">
<fetchPlan extends="_base">
<property fetchPlan="_base" name="department"/>
<property fetchPlan="_base" name="steps">
<property name="step" fetchPlan="_base"/>
</property>
</fetchPlan>
<loader/>
<collection id="stepsDc" property="steps"/>
</instance>
现在 UserSteps 集合会与 User
实例一起从数据库进行预加载。
在 Jmix UI 的层级面板中选择 columns
元素并在组件面板中点击 Add → Column。现在 Add Column 弹窗中包含了关联的 Step
实体和属性了:
选择 step
→ name
,然后点击 OK。新列会添加在最后面:
<table id="stepsTable" dataContainer="stepsDc" ...>
<columns>
<column id="dueDate"/>
<column id="completedDate"/>
<column id="step.name"/>
</columns>
除了 step.name
之外,也可以直接使用 step
。此时,表格列中将展示实体的 实例名称。对于 Step
实体,实例名称是从 name
属性获取,所以结果没有不同。
也可以在 XML 中直接添加 step 列而不修改 fetch plan,由于关联实体的懒加载机制,UI 界面也能正常工作。但是此时 Step 会在单独的请求中加载,集合中的每个 UserStep 会触发一次(N+1 查询性能问题)。
|
将 step.name
移到最前面,可以直接在 XML 中修改或者在 Jmix UI 层级面板中拖动元素:
<table id="stepsTable" dataContainer="stepsDc" ...>
<columns>
<column id="step.name"/>
<column id="dueDate"/>
<column id="completedDate"/>
</columns>
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开用户编辑界面,确保 Steps 表格现在展示步骤名称:
添加自定义列
本小节中,我们将实现这样一个功能:可以直接在入职步骤表格的一行中,通过勾选一个复选框将当前步骤标记为已完成。
UI 表格组件支持 生成列(generated columns),这种列的内容可以不与特定的实体属性绑定。在生成列的单元格中,可以展示任何可视化组件或者带有多个内部组件的可视化容器。
我们添加一个生成列用于展示复选框。
在 Jmix UI 层级面板中选择 columns
元素并在组件面板中点击 Add → Column。显示 Add Column 对话框:
选择 New Custom Column 并点击 OK。
在 Additional Settings for Custom Coulmn 弹窗中,Custom column id 字段输入 completed
并勾选 Create generator:
点击 OK。
Studio 会在表格的 XML 中添加 completed
列:
以及在 UserEdit
控制器中添加处理方法:
注意左侧的行标记:支持在 XML 中的列定义和控制器中的处理方法之间切换。
在控制器类中注入 UiComponents
:
@Autowired
private UiComponents uiComponents;
可以通过编辑器顶部操作面板中的 Inject 按钮注入界面中的组件和 Spring bean。 |
实现处理器方法:
@Install(to = "stepsTable.completed", subject = "columnGenerator") (1)
private Component stepsTableCompletedColumnGenerator(UserStep userStep) { (2)
CheckBox checkBox = uiComponents.create(CheckBox.class); (3)
checkBox.setValue(userStep.getCompletedDate() != null);
checkBox.addValueChangeListener(e -> { (4)
if (userStep.getCompletedDate() == null) {
userStep.setCompletedDate(LocalDate.now());
} else {
userStep.setCompletedDate(null);
}
});
return checkBox; (5)
}
1 | @Install 注解表示该方法是一个 代理(delegate):一个 UI 组件(这个 case 中是表格)会在生命周期的某个阶段调用该方法。 |
2 | 这个特殊的代理(列生成器)接收一个实体实例作为参数,该实例在表格中的一行显示。 |
3 | CheckBox 组件实例通过 UiComponents 工厂生成。 |
4 | 当点击复选框时,它的值会发生变化,复选框会调用其 ValueChangeEvent 监听器。在监听器中,设置 UserStep 实体的 completedDate 属性。 |
5 | 列生成器代理返回列单元格需要展示的可视化组件。 |
将 completed
列移至最前面,并设置 caption
属性为空,width
设置为 50px
:
<table id="stepsTable" dataContainer="stepsDc" ...>
<columns>
<column id="completed" caption="" width="50px"/>
<column id="step.name"/>
<column id="dueDate"/>
<column id="completedDate"/>
</columns>
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开用户编辑界面并点击某些行的复选框。Completed date 列会做相应的更改:
UserStep
实例的改动会在点击界面的 OK 按钮后保存到数据库。由界面的 DataContext
负责:能跟踪所有实体的改动并保存修改的实例。
响应更改
当为用户生成入职步骤、标记一个 UserStep 完成或者删除一个步骤时,Onboarding status
字段应该也做相应的调整。
我们现在实现根据 UserSteps 集合变动做出响应的逻辑。
打开 UserEdit
控制器并在顶部的操作面板中点击 Generate Handler。收起所有的树结构,然后在 Data containers handlers → stepsDc
下面选择 ItemPropertyChangeEvent
和 CollectionChangeEvent
:
点击 OK。
Studio 会生成两个方法桩代码:onStepsDcItemPropertyChange()
和 onStepsDcCollectionChange()
。实现如下:
@Subscribe(id = "stepsDc", target = Target.DATA_CONTAINER)
public void onStepsDcItemPropertyChange(InstanceContainer.ItemPropertyChangeEvent<UserStep> event) {
updateOnboardingStatus(); (1)
}
@Subscribe(id = "stepsDc", target = Target.DATA_CONTAINER)
public void onStepsDcCollectionChange(CollectionContainer.CollectionChangeEvent<UserStep> event) {
updateOnboardingStatus(); (2)
}
private void updateOnboardingStatus() {
User user = getEditedEntity(); (3)
long completedCount = user.getSteps() == null ? 0 :
user.getSteps().stream()
.filter(us -> us.getCompletedDate() != null)
.count();
if (completedCount == 0) {
user.setOnboardingStatus(OnboardingStatus.NOT_STARTED); (4)
} else if (completedCount == user.getSteps().size()) {
user.setOnboardingStatus(OnboardingStatus.COMPLETED);
} else {
user.setOnboardingStatus(OnboardingStatus.IN_PROGRESS);
}
}
1 | ItemPropertyChangeEvent 处理器在实体的属性发生更改时调用。 |
2 | CollectionChangeEvent 处理器在容器中添加或者删除数据时调用。 |
3 | 获取当前编辑的 User 实例。 |
4 | 更新 onboardingStatus 属性。由于数据绑定机制,对该属性的更新会立即展示在 UI 属性中。 |
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开用户编辑界面,然后在 UserStep 表格中做一些改动,并查看 Onboarding status
字段的值。
小结
在本节中,我们实现了下面两个功能:
-
为用户指定部门。
-
为用户生成并管理入职步骤。
学习内容:
-
关联属性应该包含在界面的 fetch plan 中,以避免 N+1 查询性能问题。
-
EntityComboBox 实体下拉框 可以用来在下拉框中选择关联实体。该组件需要一个 集合实例容器,选项列表在 optionsContainer 属性设置。
-
User
和UserStep
实体的关系是 组合 的一个很好的示例,关联实体(UserStep
)仅作为其所有者(User
)的一部分存在。这种关联通过 @Composition 注解标记。 -
关联实体的集合可以用关联属性的
@OrderBy
注解进行排序。 -
Button 按钮 组件的
ClickEvent
处理器用来处理按钮的点击事件。可以在 Jmix UI 组件面板的 Handlers 标签页自动生成。 -
编辑界面控制器 的
getEditedEntity()
方法返回正在编辑的实体。 -
Notifications 接口用来展示弹出通知消息。
-
DataManager 接口可以用来从数据库加载数据。
-
内部的嵌套关联实体集合会被加载到 CollectionPropertyContainer。容器的
getItems()
和getMutableItems()
方法可以用来遍历集合或者添加/删除集合项。 -
DataContext 跟踪实体的更改,并在用户点击界面的 OK 按钮时将改动的实例保存至数据库。
-
UI 表格支持 生成列,用来展示任意的可视化组件。
-
ItemPropertyChangeEvent 和 CollectionChangeEvent 可以用来对数据容器中的实体变更做出响应。