5. UI 中处理数据
开发进行到这个阶段,我们已经有了入职步骤和部门的管理功能,以及默认的用户管理功能,并且为用户添加了 Onboarding status
属性。现在我们需要将用户与入职步骤和部门关联起来。
本节中,我们将完成:
-
为
User
实体添加department
和joiningDate
属性,并在 UI 展示。 -
创建
UserStep
实体,关联一个用户和一个入职步骤。 -
为
User
实体添加UserStep
实体集合,并在User.detail
视图展示。 -
在
User.detail
视图实现生成和保存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 views()按钮:
出现的对话框中会显示 User.detail
和 User.list
视图。我们都选上,然后点击 OK。
Studio 会在 User.list
视图的 dataGrid
组件和 User
视图的 formLayout
组件中添加 department
属性。
你可能会注意,Studio 在 user-list-view.xml
中添加了下面的代码:
<data>
<collection id="usersDc"
class="com.company.onboarding.entity.User">
<fetchPlan extends="_base">
<property name="department" fetchPlan="_base"/> <!-- added -->
</fetchPlan>
以及 user-detail-view.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.list
视图,可以看到新加的 Department 列。创建用户时,可以看到 User.detail
中的 Department 选择控件:
使用下拉框选择
默认情况下,Studio 会生成 entityPicker
组件选择关联实体。可以在 User.detail
视图中看到这样的组件。打开 user-detail-view.xml
并在 formLayout
中找到 entityPicker
组件:
<layout ...>
<formLayout id="form" dataContainer="userDc">
...
<entityPicker id="departmentField" property="department">
<actions>
<action id="entityLookup" type="entity_lookup"/>
<action id="entityClear" type="entity_clear"/>
</actions>
</entityPicker>
</formLayout>
该组件支持通过一个列表视图选择实体,支持过滤、排序或者分页。但是当备选的记录相对比较少时(比如少于 1000),通过简单的下拉框列表选择会更加方便。
我们将修改 User.detail
视图,使用 entityComboBox
组件选择用户的部门。
将组件的 XML 元素修改为 entityComboBox
并删除内部的 actions
元素:
<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" readOnly="true">
<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" readOnly="true">
<query>
<![CDATA[select e from Department e
order by e.name asc]]>
</query>
</loader>
</collection>
</data>
现在需要将 entityComboBox
组件与 departmentsDc
数据容器进行关联。
在 Jmix UI 的结构面板选中 departmentField
,然后在 Jmix UI 的组件面板中为 itemsContainer
属性选择 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 Views()按钮,选择 User.detail
视图,点击 OK。
Studio 会修改 user-detail-view.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 ...>
<formLayout id="form" dataContainer="userDc">
...
</formLayout>
<hbox id="buttonsPanel" classNames="buttons-panel">
<button action="stepsDataGrid.create"/>
<button action="stepsDataGrid.edit"/>
<button action="stepsDataGrid.remove"/>
</hbox>
<dataGrid id="stepsDataGrid" dataContainer="stepsDc" ...> (3)
<actions>
<action id="create" type="list_create"/>
<action id="edit" type="list_edit"/>
<action id="remove" type="list_remove"/>
</actions>
<columns>
<column property="version"/>
<column property="dueDate"/>
<column property="completedDate"/>
<column property="sortValue"/>
</columns>
</dataGrid>
1 | Fetch plan 添加了 steps 属性,确保 UserSteps 与 User 一起进行预加载。 |
2 | 内部的 stepsDc 集合数据容器用于将可视化组件与 steps 集合属性做绑定。 |
3 | dataGrid 组件用于展示 stepsDc 数据容器中的数据。 |
运行应用程序查看这些改动。
点击主工具栏中的 Debug()按钮启动应用程序。
Studio 会生成 Liquibase 更改日志,包含创建 USER_STEP
表、关联至 USER_
和 STEP
的外键约束和索引。确认这些改动。
Studio 会执行更改日志,然后运行应用程序。
应用程序准备好后,在浏览器打开 http://localhost:8080
并使用 admin
/ admin
凭证登录。
打开一个用户进行编辑。可以看到数据网格展示 UserStep
实体:
如果点击数据网格中的 Create 按钮,系统会抛出异常:View 'UserStep.detail' 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 Views()按钮。在弹窗中选择 User.detail
和 User.list
视图并点击 OK。
点击主工具栏中的 Debug()按钮启动应用程序。
Studio 会生成 Liquibase 更改日志,为 USER_
表添加 JOINING_DATE
列。确认此改动。
Studio 会执行更改日志,然后运行应用程序。在浏览器打开 http://localhost:8080
,登录后在用户列表视图和详情视图确认新属性已经添加上了。
添加自定义按钮
现在我们需要删除管理 UserSteps 的默认操作按钮,然后添加一个按钮用于启动自定义逻辑创建实体。
打开 user-detail-view.xml
并删除 dataGrid
内的 actions
元素和 hbox
内的全部 button
元素:
<hbox id="buttonsPanel" classNames="buttons-panel">
</hbox>
<dataGrid id="stepsDataGrid" dataContainer="stepsDc" width="100%" height="100%">
<columns>
<column property="version"/>
<column property="dueDate"/>
<column property="completedDate"/>
<column property="sortValue"/>
</columns>
</dataGrid>
然后在 Jmix UI 结构面板中选择 buttonsPanel
,右键点击节点,在菜单中选择 Add Component,添加一个按钮。在组件工具箱中找到 Components → Button
,并双击。然后在组件属性面板中,设置其 id
为 generateButton
,text
为 Generate
。切换至 Handlers 标签页,创建一个 ClickEvent
的处理器方法:
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开用户详情视图,查看我们新添加的 Generate 按钮正常展示而不是展示默认的 CRUD 按钮:
创建并保存 UserStep 实例
现在实现生成 UserStep
实例的逻辑。
在 UserDetailView
控制器中添加下列字段:
public class UserDetailView extends StandardDetailView<User> {
@Autowired
private DataManager dataManager;
@Autowired
private Notifications notifications;
@ViewComponent
private DataContext dataContext;
@ViewComponent
private CollectionPropertyContainer<UserStep> stepsDc;
}
如果直接复制粘贴上面的代码,IDE 会对这些代码报错,因为还需要添加相关类的 |
可以通过操作面板中的 Inject 按钮注入视图中的组件和 Spring bean: |
在 generateButton
点击处理方法中添加创建和保存 UserStep
的逻辑:
@Subscribe("generateButton")
public void onGenerateButtonClick(final ClickEvent<Button> event) {
User user = getEditedEntity(); (1)
if (user.getJoiningDate() == null) { (2)
notifications.create("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 | 使用 StandardDetailView 父类中的 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 集合会与 Step
实例一起从数据库进行预加载。
在 Jmix UI 的结构面板中选择 columns
元素并在组件面板中点击 Add → Column。现在 Add Column 弹窗中包含了关联的 Step
实体和属性了:
选择 step
→ name
,然后点击 OK。新列会添加在最后面:
<dataGrid id="stepsDataGrid" dataContainer="stepsDc" ...>
<columns>
<column property="dueDate"/>
<column property="completedDate"/>
<column property="step.name"/>
</columns>
除了 step.name
之外,也可以直接使用 step
。此时,表格列中将展示实体的 实例名称。对于 Step
实体,实例名称是从 name
属性获取,所以结果没有不同。
也可以在 XML 中直接添加 step 列而不修改 fetch plan,由于关联实体的懒加载机制,UI 视图也能正常工作。但是此时 Step 会在单独的请求中加载,集合中的每个 UserStep 会触发一次(N+1 查询性能问题)。
|
将 step.name
移到最前面,可以直接在 XML 中修改或者在 Jmix UI 结构面板中拖动元素:
<dataGrid id="stepsDataGrid" dataContainer="stepsDc" width="100%" height="100%">
<columns>
<column property="step.name"/>
<column property="dueDate"/>
<column property="completedDate"/>
</columns>
</dataGrid>
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开用户详情视图,确保 Steps 表格现在展示步骤名称:
添加组件列
本小节中,我们将实现:通过点击数据网格行中的一个复选框,即可标记 UserStep 完成。复选框在数据网格左侧新增一列中显示。
为 stepsDataGrid
添加一个新的列:
<dataGrid id="stepsDataGrid" ...>
<columns>
<column key="completed" sortable="false" width="4em" flexGrow="0"/>
该列没有绑定任何实体属性,因此,这里使用了 key
属性,而非 property
属性。
选中 completed
列,在组件属性面板切换至 Handlers 标签,并创建 renderer
处理方法:
@Supply(to = "stepsDataGrid.completed", subject = "renderer")
private Renderer<UserStep> stepsDataGridCompletedRenderer() {
return null;
}
在控制器类中注入 UiComponents
:
@Autowired
private UiComponents uiComponents;
可以通过编辑器顶部操作面板中的 Inject 按钮注入视图中的组件和 Spring bean。 |
实现 stepsDataGridCompletedRenderer
方法:
@Supply(to = "stepsDataGrid.completed", subject = "renderer")
private Renderer<UserStep> stepsDataGridCompletedRenderer() {
return new ComponentRenderer<>(userStep -> { (1)
Checkbox checkbox = uiComponents.create(Checkbox.class); (2)
checkbox.setValue(userStep.getCompletedDate() != null);
checkbox.addValueChangeListener(e -> { (3)
if (userStep.getCompletedDate() == null) {
userStep.setCompletedDate(LocalDate.now());
} else {
userStep.setCompletedDate(null);
}
});
return checkbox; (4)
});
}
1 | 方法返回一个 Renderer 对象,用于创建一个可以在列中渲染的 UI 组件。参数为当前行对应的实体实例。 |
2 | 使用 UiComponents 工厂创建 Checkbox 组件实例。 |
3 | 当点击复选框时,复选框的值会发生变化,并且调用其 ValueChangeEvent 监听器。监听器中,为 UserStep 实体设置了 completedDate 属性。 |
4 | 返回列单元格中显示的组件。 |
按下 Ctrl/Cmd+S 保存修改然后切换至运行中的程序。重新打开用户详情视图并点击某些行的复选框。Completed date 列会做相应的更改:
UserStep
实例的改动会在点击视图的 OK 按钮后保存到数据库。由视图的 DataContext
负责:能跟踪所有实体的改动并保存修改的实例。
响应更改
当为用户生成入职步骤、标记一个 UserStep 完成或者删除一个步骤时,Onboarding status
字段应该也做相应的调整。
我们现在实现根据 UserSteps 集合变动做出响应的逻辑。
打开 UserDetailView
控制器并在顶部的操作面板中点击 Generate Handler。收起所有的树结构,然后在 Data containers handlers → stepsDc
下面选择 ItemPropertyChangeEvent
和 CollectionChangeEvent
:
点击 OK。
Studio 会生成两个方法桩代码:onStepsDcItemPropertyChange()
和 onStepsDcCollectionChange()
。实现如下:
@Subscribe(id = "stepsDc", target = Target.DATA_CONTAINER)
public void onStepsDcCollectionChange(final CollectionContainer.CollectionChangeEvent<UserStep> event) {
updateOnboardingStatus(); (1)
}
@Subscribe(id = "stepsDc", target = Target.DATA_CONTAINER)
public void onStepsDcItemPropertyChange(final InstanceContainer.ItemPropertyChangeEvent<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 实体下拉框 可以用来在下拉框中选择关联实体。该组件需要一个 集合容器,选项列表在
itemsContainer
属性设置。 -
User
和UserStep
实体的关系是 组合 的一个很好的示例,关联实体(UserStep
)仅作为其所有者(User
)的一部分存在。这种关联通过 @Composition 注解标记。 -
关联实体的集合可以用关联属性的
@OrderBy
注解进行排序。 -
button 按钮 组件的
ClickEvent
处理器用来处理按钮的点击事件。可以在 Jmix UI 组件面板的 Handlers 标签页自动生成。 -
实体详情视图控制器的
getEditedEntity()
方法返回正在编辑的实体。 -
Notifications 接口用来展示弹出通知消息。
-
DataManager 接口可以用来从数据库加载数据。
-
内部的嵌套关联实体集合会被加载到 CollectionPropertyContainer。容器的
getItems()
和getMutableItems()
方法可以用来遍历集合或者添加/删除集合项。 -
DataContext 跟踪实体的更改,并在用户点击视图的 OK 按钮时将改动的实例保存至数据库。
-
UI 数据网格可以添加列,用来展示任意的可视化组件。
-
ItemPropertyChangeEvent 和 CollectionChangeEvent 可以用来对数据容器中的实体变更做出响应。