使用数据组件

在本章节将展示使用数据组件的实战例子。

声明式用法

通常,数据组件都是在界面 XML 描述中声明式地定义并绑定至可视化组件。如果使用 Studio 为实体创建界面,可以看到 XML 中的 <data> 元素,包含数据组件的声明。

下面是 Employee 实体编辑界面中的数据组件示例,Employee 有至 Department 实体的对一关系,和 EquipmentLine 实体的对多关系:

<data> (1)
    <instance id="employeeDc"
              class="ui.ex1.entity.Employee"> (2)
        <fetchPlan extends="_base"> (3)
            <property name="department" fetchPlan="_instance_name"/>
            <property name="equipment" fetchPlan="_base"/>
        </fetchPlan>
        <loader/>(4)
        <collection id="equipmentDc" property="equipment"/> (5)
    </instance>
    <collection id="departmentsDc"
                class="ui.ex1.entity.Department"
                fetchPlan="_base"> (6)
        <loader> (7)
            <query>
                <![CDATA[select e from uiex1_Department e]]>
            </query>
        </loader>
    </collection>
</data>
1 data 跟匀速定义 DataContext 实例。
2 Employee 实体的 InstanceContainer
3 可选的 fetchPlan 属性,定义从数据库预加载的对象关系图。
4 InstanceLoader,加载 Employee 实例。
5 内部 EquipmentLine 实体的 CollectionPropertyContainer。绑定至 Employee.equipment 集合属性。
6 Department 实体的 CollectionContainer
7 CollectionLoader 用指定的查询语句加载 Department 实体实例。

上面数据容器可以在可视化组件中这样使用:

<layout spacing="true" expand="editActions">
    <textField dataContainer="employeeDc" property="id"/> (1)
    <form id="form" dataContainer="employeeDc"> (2)
        <column width="350px">
            <textField id="nameField" property="name"/>
            <textField id="salaryField" property="salary"/>
            <comboBox id="positionField" property="position"/>
            <entityComboBox property="department"
                            optionsContainer="departmentsDc"/> (3)
        </column>
    </form>
    <table dataContainer="equipmentDc"> (4)
        <columns>
            <column id="name"/>
            <column id="number"/>
        </columns>
    </table>
</layout>
1 单独的控件具有 dataContainerproperty 属性。
2 form 组件会将 dataContainer 传递给 form 的字段,所以字段只需要 property 属性。
3 EntityComboBox 也有 optionsContainer 属性,获取选项列表。
4 表格只有 dataContainer 属性。

编程式用法

可以使用编程的方式在可视化组件中创建和使用数据组件。

下面的例子中,我们创建了跟上一节一样的编辑界面,使用了相同的数据和可视化组件,只不过是用纯 Java 实现的。

@UiController("uiex1_EmployeeExample.edit")
public class EmployeeEditExample extends StandardEditor<Employee> {

    @Autowired
    private DataComponents dataComponents; (1)
    @Autowired
    private UiComponents uiComponents;

    private InstanceContainer<Employee> employeeDc;
    private CollectionPropertyContainer<EquipmentLine> equipmentDc;
    private CollectionContainer<Department> departmentsDc;
    private InstanceLoader<Employee> employeeDl;
    private CollectionLoader<Department> departmentsDl;

    @Subscribe
    protected void onInit(InitEvent event) {
        createDataComponents();
        createUiComponents();
    }

    private void createDataComponents() {
        DataContext dataContext = dataComponents.createDataContext();
        getScreenData().setDataContext(dataContext); (2)

        employeeDc = dataComponents.createInstanceContainer(Employee.class);

        employeeDl = dataComponents.createInstanceLoader();
        employeeDl.setContainer(employeeDc); (3)
        employeeDl.setDataContext(dataContext); (4)
        employeeDl.setFetchPlan(FetchPlan.BASE);

        equipmentDc = dataComponents.createCollectionContainer(
                EquipmentLine.class, employeeDc, "equipment"); (5)

        departmentsDc = dataComponents.createCollectionContainer(Department.class);

        departmentsDl = dataComponents.createCollectionLoader();
        departmentsDl.setContainer(departmentsDc);
        departmentsDl.setDataContext(dataContext);
        departmentsDl.setQuery("select e from uiex1_Department e"); (6)
        departmentsDl.setFetchPlan(FetchPlan.BASE);
    }

    private void createUiComponents() {

        Form form = uiComponents.create(Form.class);
        getWindow().add(form);

        TextField<String> nameField = uiComponents.create(TextField.TYPE_STRING);
        nameField.setValueSource(new ContainerValueSource<>(employeeDc, "name")); (7)
        form.add(nameField);

        TextField<Double> salaryField = uiComponents.create(TextField.TYPE_DOUBLE);
        salaryField.setValueSource(new ContainerValueSource<>(employeeDc, "salary"));
        form.add(salaryField);

        ComboBox<Position> positionField = uiComponents.create(ComboBox.of(Position.class));
        positionField.setValueSource(new ContainerValueSource<>(employeeDc, "position"));
        form.add(positionField);

        EntityComboBox<Department> departmentField = uiComponents.create(EntityComboBox.of(Department.class));
        departmentField.setValueSource(new ContainerValueSource<>(employeeDc, "department"));
        departmentField.setOptions(new ContainerOptions<>(departmentsDc)); (8)
        form.add(departmentField);

        Table<EquipmentLine> table = uiComponents.create(Table.of(EquipmentLine.class));
        getWindow().add(table);
        getWindow().expand(table);
        table.setItems(new ContainerTableItems<>(equipmentDc)); (9)

        Button okButton = uiComponents.create(Button.class);
        okButton.setCaption("OK");
        okButton.addClickListener(clickEvent -> closeWithCommit());
        getWindow().add(okButton);

        Button cancelButton = uiComponents.create(Button.class);
        cancelButton.setCaption("Cancel");
        cancelButton.addClickListener(clickEvent -> closeWithDiscard());
        getWindow().add(cancelButton);
    }

    @Override
    protected InstanceContainer<Employee> getEditedEntityContainer() { (10)
        return employeeDc;
    }

    @Subscribe
    protected void onBeforeShow(BeforeShowEvent event) { (11)
        employeeDl.load();
        departmentsDl.load();
    }
}
1 DataComponents 是创建数据组件的工厂。
2 DataContext 实例在界面注册,以便能使用标准的提交操作。
3 employeeDl 加载器会加载数据到 employeeDc 容器。
4 employeeDl 加载器会合并加载的实体到数据上下文以便跟踪改动。
5 equipmentDc 创建为属性容器。
6 departmentsDl 加载器指定了一个查询语句。
7 ContainerValueSource 用来绑定单一字段到容器。
8 ContainerOptions 用来为查找控件提供选项。
9 ContainerTableItems 用来绑定表格到容器。
10 getEditedEntityContainer() 被重写了,用来指定容器,替代了 @EditedEntityContainer 注解。
11 在界面展示前加载数据。框架会自动设置编辑实体的 id 到 employeeDl

数据组件依赖

有时候需要加载和展示依赖同一界面上其他数据的数据。比如,在下面的图片中,左边的表格展示 employee 的列表,右边的表格展示选中 employee 的 equipment。右边的列表会在左边列表每次变更选择时刷新。

depend tables

这个例子中,Employee 实体包含了 equipment 属性,这个是一对多的集合。所以实现这个界面的最简单的方法就是使用带有 equipment 属性的 fetch plan 加载 employees 列表,并且使用 property container 来保存 equiment 列表。然后绑定左边的表格到主容器,绑定右边的表格到属性容器。

但是这个方案有一个隐藏的性能问题:会加载左边表格所有 employee 的所有 equipment,尽管每次只是给单一的 employee 展示 equipment。这就是为什么推荐只在加载单一主实体的时候使用属性容器和深度 fetch plan 处理集合属性,例如,在 employee 编辑界面。

还有,主实体也许跟依赖的实体没有直接的属性关联关系。这种情况下,上面使用属性容器的方案根本就行不通。

组织界面内数据关系的通常方法是使用带参数的查询。依赖的加载器包含一个带参数的查询语句,这个参数关联到主实体的数据,当主容器的当前实体更改时,需要手动设置参数并且触发依赖的加载器。

下面这个例子的界面包含两对相互依赖的容器/加载器以及绑定的表格。

<window xmlns="http://jmix.io/schema/ui/window"
        xmlns:c="http://jmix.io/schema/ui/jpql-condition"
        caption="msg://employeeDependTables.caption"
        focusComponent="employeesTable">
    <data>
        <collection id="employeesDc"
                    class="ui.ex1.entity.Employee"
                    fetchPlan="_base"> (1)
            <loader id="employeesDl">
                <query>
                    <![CDATA[select e from uiex1_Employee e]]>
                </query>
            </loader>
        </collection>
        <collection id="equipmentLinesDc"
                    class="ui.ex1.entity.EquipmentLine"
                    fetchPlan="_base"> (2)
            <loader id="equipmentLinesDl">
                <query>
                    <![CDATA[select e from uiex1_EquipmentLine e where e.employee = :employee]]>
                </query>
            </loader>
        </collection>
    </data>
    <facets> (3)
        <screenSettings id="settingsFacet" auto="true"/>
    </facets>
    <layout>
        <hbox id="mainBox" width="100%" height="100%" spacing="true">
            <table id="employeesTable" width="100%" height="100%"
                   dataContainer="employeesDc"> (4)
                <columns>
                    <column id="name"/>
                    <column id="salary"/>
                    <column id="position"/>
                </columns>
            </table>
            <table id="equipmentLinesTable" width="100%" height="100%"
                   dataContainer="equipmentLinesDc"> (5)
                <columns>
                    <column id="name"/>
                    <column id="number"/>
                </columns>
            </table>
        </hbox>
    </layout>
</window>
1 主容器和主加载器。
2 依赖容器和加载器。
3 由于没有使用 DataLoadCoordinator facet,数据加载器不会自动加载数据。
4 主表格。
5 从表格。
@UiController("uiex1_EmployeeDependTables")
@UiDescriptor("employee-depend-tables.xml")
@LookupComponent("employeesTable")
public class EmployeeDependTables extends StandardLookup<Employee> {

    @Autowired
    private CollectionLoader<Employee> employeesDl;

    @Autowired
    private CollectionLoader<EquipmentLine> equipmentLinesDl;

    @Subscribe
    public void onBeforeShow(BeforeShowEvent event) {
        employeesDl.load(); (1)
    }

    @Subscribe(id = "employeesDc", target = Target.DATA_CONTAINER)
    public void onEmployeesDcItemChange(InstanceContainer.ItemChangeEvent<Employee> event) {
        equipmentLinesDl.setParameter("employee", event.getItem()); (2)
        equipmentLinesDl.load();
    }
}
1 主数据加载器在 BeforeShowEvent 处理器中触发。
2 在主数据容器的 ItemChangeEvent 处理器中,为依赖的加载器设置参数并触发加载。
使用 DataLoadCoordinator facet 可以将数据组件通过声明式的方式连接,不需要写 Java 代码。

加载器使用界面参数

很多时候需要根据界面传递的参数加载界面需要的数据。下面是一个浏览界面的示例,使用了界面参数并且在加载数据时使用参数来过滤数据。

假设有两个实体:CountryCityCity 实体有 country 属性,是 Country 的引用。在 city 的浏览界面,可以接受一个 country 的实例,然后只展示该 country 的 city。

首先,看看 city 界面的 XML 描述。其数据加载器包含一个带有参数的查询:

<data>
    <collection id="citiesDc"
                class="ui.ex1.entity.City"
                fetchPlan="_base">
        <loader id="citiesDl">
            <query>
                <![CDATA[select e from uiex1_City e where e.country = :country]]>
            </query>
        </loader>
    </collection>
</data>

City 界面控制器有参数的公共 setter,然后在 BeforeShowEvent 处理器中使用了参数。

@UiController("sample_CityBrowse")
@UiDescriptor("city-browse.xml")
@LookupComponent("citiesTable")
public class CityBrowse extends StandardLookup<City> {

    @Autowired
    private CollectionLoader<City> citiesDl;

    private Country country;

    public void setCountry(Country country) {
        this.country = country;
    }

    @Subscribe
    public void onBeforeShow(BeforeShowEvent event) {
        if (country == null)
            throw new IllegalStateException("Country parameter is null");
        citiesDl.setParameter("country", country);
        citiesDl.load();
    }
}

City 界面可以从其他界面为其传递一个 country 实例并打开,示例:

@Autowired
private ScreenBuilders screenBuilders;

private void showCitiesOfCountry(Country country) {
    CityBrowse cityBrowse = screenBuilders.screen(this)
            .withScreenClass(CityBrowse.class)
            .build();
    cityBrowse.setCountry(country);
    cityBrowse.show();
}

自定义排序

UI 表格中按照实体属性排序的功能是通过 CollectionContainerSorter 实现的,需要为 CollectionContainer 设置该排序器。标准的实现是,如果数据在一页以内能显示,则在内存做数据排序,否则会使用适当的 "order by" 语句发送一个新的数据库请求。"order by" 语句是由 JpqlSortExpressionProvider bean 创建。

有些实体属性需要一个特殊的排序实现。下面我们用个例子解释一下如何自定义排序:假设有 Order 实体带有 String 类型的 number 属性,但是我们知道该属性其实是只保存数字。所以我们希望排序的顺序是 1, 2, 3, 10, 11。但是,默认的排序行为会产生这样的结果:1, 10, 11, 2, 3

首先,创建一个 CollectionContainerSorter 类的子类,在内存进行排序:

public class CustomCollectionContainerSorter extends CollectionContainerSorter {

    public CustomCollectionContainerSorter(CollectionContainer container,
                                           @Nullable BaseCollectionLoader loader,
                                           BeanFactory beanFactory) {
        super(container, loader, beanFactory);
    }

    @Override
    protected Comparator<?> createComparator(Sort sort, MetaClass metaClass) {
        MetaPropertyPath metaPropertyPath = Objects.requireNonNull(
                metaClass.getPropertyPath(sort.getOrders().get(0).getProperty()));

        if (metaPropertyPath.getMetaClass().getJavaClass().equals(Order.class)
                && "num".equals(metaPropertyPath.toPathString())) {
            boolean isAsc = sort.getOrders().get(0).getDirection() == Sort.Direction.ASC;
            return Comparator.comparing((Order e) ->
                            e.getNum() == null ? null : Integer.valueOf(e.getNum()),
                    new EntityValuesComparator<>(isAsc, metaClass, beanFactory));
        }
        return super.createComparator(sort, metaClass);
    }
}

在界面创建排序器:

public class OrderBrowseExample extends StandardLookup<Order> {

    @Autowired
    private CollectionLoader<Order> ordersDl;

    @Autowired
    private CollectionContainer<Order> ordersDc;

    @Autowired
    private BeanFactory beanFactory;

    @Subscribe
    private void onInit(InitEvent event) {
        Sorter sorter = new CustomCollectionContainerSorter(ordersDc, ordersDl, beanFactory);
        ordersDc.setSorter(sorter);
    }
}

如果排序器定义了一些全局的行为,则可以创建自定义的工厂在系统级别实例化该排序器:

@Primary
@Component("sample_SorterFactory")
public class CustomSorterFactory extends SorterFactory {

    @Override
    public Sorter createCollectionContainerSorter(CollectionContainer container,
                                                  @Nullable BaseCollectionLoader loader) {
        return new CustomCollectionContainerSorter(container, loader, beanFactory);
    }
}

最后,为数据库级别的排序创建我们自己定义的 JpqlSortExpressionProvider 实现:

@Primary
@Component("sample_JpqlSortExpressionProvider")
public class CustomSortExpressionProvider
        extends DefaultJpqlSortExpressionProvider {

    @Override
    public String getDatatypeSortExpression(MetaPropertyPath metaPropertyPath, boolean sortDirectionAsc) {
        if (metaPropertyPath.getMetaClass().getJavaClass().equals(Order.class)
                && "num".equals(metaPropertyPath.toPathString())) {
            return String.format("CAST({E}.%s BIGINT)", metaPropertyPath);
        }
        return String.format("{E}.%s", metaPropertyPath);
    }

}