Using Data Components
This section provides practical examples of working with data components.
Declarative Usage
Usually, data components are defined and bound to visual components declaratively in the screen XML descriptor. If you create a screen for an entity using Studio, you can see the top-level <data>
element which contains the data component declarations.
Below is an example of data components in an edit screen for the Employee
entity that has a to-one reference to Department
and a to-many reference to the EquipmentLine
entity:
<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 | The root data element defines the DataContext instance. |
2 | InstanceContainer of the Employee entity. |
3 | The optional fetchPlan attribute defines the object graph that should be eagerly loaded from the database. |
4 | InstanceLoader that loads the Employee instance. |
5 | CollectionPropertyContainer for the nested EquipmentLine entity. It is bound to the Employee.equipment collection attribute. |
6 | CollectionContainer for the Department entity. |
7 | CollectionLoader that loads the Department entity instances using the specified query. |
The data containers defined above can be used in visual components as follows:
<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 | Standalone fields have dataContainer and property attributes. |
2 | The form component propagates dataContainer to its fields, so they need only the property attribute. |
3 | The EntityComboBox field has also the optionsContainer attribute to get the list of options. |
4 | Tables have only the dataContainer attribute. |
Programmatic Usage
Data components can be created and used in visual components programmatically.
In the example below, we create an editor screen with the same data and visual components as defined in the previous section using only Java code without any XML descriptor.
@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 is a factory to create data components. |
2 | The DataContext instance is registered in the screen for standard commit action to work properly. |
3 | The employeeDl loader will load data to employeeDc container. |
4 | The employeeDl loader will merge loaded entities into the data context for change tracking. |
5 | The equipmentDc is created as a property container. |
6 | A query is specified for the departmentsDl loader. |
7 | ContainerValueSource is used to bind single fields to containers. |
8 | ContainerOptions is used to provide options to combo boxes. |
9 | ContainerTableItems is used to bind tables to containers. |
10 | getEditedEntityContainer() is overridden to specify the container instead of @EditedEntityContainer annotation. |
11 | Loads data before showing the screen. The edited entity id will be set to employeeDl by the framework automatically. |
Dependencies Between Data Components
Sometimes you need to load and display data that depends on other data in the same screen. For example, on the image below the left table displays the list of employees and the right one displays the list of equipment for the selected employee. The right list is refreshed each time the selected item in the left list changes.
In this example, the Employee
entity contains the equipment
attribute that is a one-to-many collection. So the simplest way to implement the screen is to load the list of employees with a fetch plan containing the equipment
attribute and use a property container to hold the list of dependent equiment lines. Then bind the left table to the master container and the right table to the property container.
But this approach has the following performance implication: you will load equipment lines for all employees from the left table, even though you display the equipment lines only for a single employee. This is why we recommend using property containers and deep fetch plans with collection attributes only when loading a single master item, for example, in the employee editor screen.
Also, the master entity may have no direct property pointing to the dependent entity. In this case, the above approach with a property container would not work at all.
The common approach to organize relations between data in a screen is to use queries with parameters. The dependent loader contains a query with a parameter that links data to the master, and when the current item in the master container changes, you set the parameter and trigger the dependent loader.
Below is an example of the screen which has two dependent container/loader pairs and the tables bound to them.
<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 | Master container and loader. |
2 | Dependent container and loader. |
3 | The DataLoadCoordinator facet is not used, so the loaders will not be triggered automatically. |
4 | Master table. |
5 | Dependent table. |
@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 | The master loader is triggered in the BeforeShowEvent handler. |
2 | In the ItemChangeEvent handler of the master container, a parameter is set to the dependent loader and it is triggered. |
The DataLoadCoordinator facet allows you to link data components declaratively without writing any Java code.
|
Using Screen Parameters in Loaders
It is often required to load data in a screen depending on parameters passed to that screen. Below is an example of a browse screen that accepts a parameter and uses it to filter the loaded data.
Suppose we have two entities: Country
and City
. The City
entity has the country
attribute that is a reference to Country
. The cities browser accepts a country instance and shows cities only of this country.
First, consider the cities screen XML descriptor. Its loader contains a query with a parameter:
<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>
The cities screen controller contains a public setter for the parameter and uses this parameter in the BeforeShowEvent
handler.
@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();
}
}
The cities screen can be opened from another screen passing a country as follows:
@Autowired
private ScreenBuilders screenBuilders;
private void showCitiesOfCountry(Country country) {
CityBrowse cityBrowse = screenBuilders.screen(this)
.withScreenClass(CityBrowse.class)
.build();
cityBrowse.setCountry(country);
cityBrowse.show();
}
Custom Sorting
Sorting of UI tables by entity attributes is performed by CollectionContainerSorter
which is set for a CollectionContainer. The standard implementation sorts data in memory if it fits in one page of loaded data, otherwise it sends a new request to the database with the appropriate "order by" clause. The "order by" clause is created by the JpqlSortExpressionProvider
bean.
Some entity attributes can require a special implementation of sorting. Below we explain how to customize sorting on a simple example: suppose there is the Order
entity with a number
attribute of type String
, but we know that the attribute actually stores only numeric values. So we want the sort order to be 1
, 2
, 3
, 10
, 11
. With the default behavior, the order would be 1
, 10
, 11
, 2
, 3
.
First, create a subclass of the CollectionContainerSorter
class for sorting in memory:
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);
}
}
Create the sorter in the required screen:
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);
}
}
If your sorter defines some global behavior, create your own factory that instantiates sorters system-wide:
@Primary
@Component("sample_SorterFactory")
public class CustomSorterFactory extends SorterFactory {
@Override
public Sorter createCollectionContainerSorter(CollectionContainer container,
@Nullable BaseCollectionLoader loader) {
return new CustomCollectionContainerSorter(container, loader, beanFactory);
}
}
Also, you can create own implementation of JpqlSortExpressionProvider
for sorting at the database level:
@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);
}
}