5. Working with Data in UI
At this stage of development, the application has the Steps and Departments management and the default User management with the added Onboarding status
attribute. Now you need to link Users with Steps and Departments.
In this chapter, you will do the following:
-
Add
department
andjoiningDate
attributes to theUser
entity and show them in UI. -
Create the
UserStep
entity which links a user with an onboarding step. -
Add a collection of
UserStep
entities to theUser
entity and show it on theUser.edit
screen. -
Implement the generation and saving of
UserStep
instances in theUser.detail
view.
The diagram below shows the entities and attributes addressed in this chapter:
Adding Reference Attribute
You’ve already done a similar task when created the HR Manager attribute of the Department
entity as a reference to User
. Now you need to create a link in the opposite direction: a User should have a reference to Department.
If your application is running, stop it using the Stop button () in the main toolbar.
Double-click on the User
entity in Jmix tool window and select its last attribute (to add the new attribute to the end).
Click Add () in the Attributes toolbar.
In the New Attribute dialog, enter department
into the Name field. Then select:
-
Attribute type:
ASSOCIATION
-
Type:
Department
-
Cardinality: Many to One
Click OK.
Select the new department
attribute and click the Add to Views () button in the Attributes toolbar:
The appeared dialog window will show User.detail
and User.list
views. Select both screens and click OK.
Studio will add the department
attribute to the dataGrid
component of the User.list
view and to the formLayout
component of the User.detail
view.
You may also notice the following code added by Studio automatically to user-list-view.xml
:
<data>
<collection id="usersDc"
class="com.company.onboarding.entity.User">
<fetchPlan extends="_base">
<property name="department" fetchPlan="_base"/> <!-- added -->
</fetchPlan>
And to user-detail-view.xml
:
<data>
<instance id="userDc"
class="com.company.onboarding.entity.User">
<fetchPlan extends="_base">
<property name="department" fetchPlan="_base"/> <!-- added -->
</fetchPlan>
With this code, the referenced Department will be loaded together with the User in the same database query.
The views would work without including department in the fetch plans due to the lazy loading of references. But in this case, the references would be loaded by separate requests to the database. Lazy loading can affect performance of the list view, because then it loads a list of Users with a first request, and after that, executes separate requests for loading a Department of each User in the list (N+1 query problem).
|
Let’s run the application and see the new attribute in action.
Click the Debug button () in the main toolbar.
Studio will generate a Liquibase changelog for adding the DEPARTMENT_ID
column to the USER_
table, creating a foreign key constraint and an index. Confirm the changelog.
Studio will execute the changelog and start the application.
Open http://localhost:8080
in your web browser and log in to the application with admin
/ admin
credentials.
Click on the Users item in the Application menu. You will see the Department column in the User.list
view and Department picker field in User.detail
:
Using Dropdown for Selecting Reference
By default, Studio generates the entityPicker
component for selecting references. You can see it in the User.detail
view. Open user-detail-view.xml
and find the entityPicker
component inside the formLayout
:
<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>
This component allows you to select a related entity from a list view with filtering, sorting, and paging. But when the expected number of records is relatively small (say, less than 1000), it’s more convenient to select references from a simple dropdown list.
Let’s change the User.detail
view and use the entityComboBox
component for selecting a Department.
Change the XML element of the component to entityComboBox
and remove the nested actions
element:
<entityComboBox id="departmentField" property="department"/>
Switch to the running application and reopen the User detail view.
You will see that the Department field is now a dropdown, but it cannot be opened, even if you have created some Departments:
Creating Items Data Container
Let’s provide a list of items to the entityComboBox
component displaying the reference to Department. The list should contain all Departments ordered by name.
Click Add Component in the actions panel, select the Data components
section, and double-click the Collection
item. In the Data Container Properties Editor window, select Department
in the Entity field and click OK:
The new collection
element with departmentsDc
id will be created under the data
element in the Jmix UI hierarchy panel and in 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]]>
</query>
</loader>
</collection>
</data>
This element defines a collection data container and a loader for it. The data container will contain a list of Department entities loaded by the loader with the specified query.
You can edit the query right in the XML or with JPQL Query Designer. To start the designer, find the link next to the query
attribute in the Jmix UI inspector panel and click it:
In the JPQL Query Designer window, switch to the ORDER tab and add the name
attribute to the list:
Click OK.
The resulting query in XML will look like this:
<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>
Now you need to link the entityComboBox
component with the departmentsDc
collection container.
Select departmentField
in the Jmix UI hierarchy panel and select departmentsDc
for the itemsContainer
attribute in the inspector panel:
Switch to the running application and reopen the User editor screen.
You will see that the Department dropdown now has a list of options:
The entityComboBox component allows you to filter options by entering text into the field. But keep in mind that filtering is performed in the server memory, all options are loaded from the database at once.
|
Creating UserStep Entity
In this section, you will create the UserStep
entity which represents onboarding Steps for a particular User:
If your application is running, stop it using the Stop button () in the main toolbar.
In the Jmix tool window, click New () → JPA Entity and create UserStep
entity with Versioned trait as you did before.
Add the following attributes to the new entity:
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 |
The final state of the entity designer should look as below:
Adding Composition Attribute
Consider the relationship between User
and UserStep
entities. UserStep
instances exist only in the context of a particular User
instance (owned by it). A UserStep
instance cannot change its owner - it just doesn’t make any sense. Also, there are no links to UserStep
from other data model objects, they are completely encapsulated in a User context.
In Jmix, such a relationship is called composition: the User is composed of a collection of UserSteps, among other attributes.
Composition in Jmix implements the Aggregate pattern of Domain-Driven Design. |
It’s often convenient to create an attribute that contains the collection of composition items in the owning entity.
Let’s create the steps
attribute in the User
entity:
If your application is running, stop it using the Stop button () in the main toolbar.
Open the User
entity designer and click Add () in the Attributes toolbar. In the New Attribute dialog, enter steps
into the Name field. Then select:
-
Attribute type:
COMPOSITION
-
Type:
UserStep
-
Cardinality: One to Many
Notice that user
is selected automatically in the Mapped by field. It’s the attribute of the UserStep
entity mapped to a database column which maintains the relationship between UserSteps and Users (the foreign key).
Click OK.
The attribute source code will have the @Composition
annotation:
@Composition
@OneToMany(mappedBy = "user")
private List<UserStep> steps;
UserSteps should be displayed in the User edit screen, so select the new steps
attribute and click the Add to Views () button in the Attributes toolbar. Select User.detail
view and click OK.
Studio will modify user-detail-view.xml
as shown below:
<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 | Attribute steps of the fetch plan ensures the collection of UserSteps is loaded eagerly together with User. |
2 | The nested stepsDc collection data container enables binding of visual components to the steps collection attribute. |
3 | The dataGrid component displays data from the linked stepsDc collection container. |
Let’s run the application and see these changes in action.
Click the Debug button () in the main toolbar.
Studio will generate a Liquibase changelog for creating the USER_STEP
table, foreign key constraints, and indexes for references to USER_
and STEP
. Confirm the changelog.
Studio will execute the changelog and start the application.
Open http://localhost:8080
in your web browser and log in to the application with admin
/ admin
credentials.
Open a user for editing. You will see the data grid displaying the UserStep
entity:
If you click Create in the data grid, you will get an exception saying that View 'UserStep.detail' is not defined
. This is true - you didn’t create a detail view for the UserStep
entity. But it’s not actually needed in our application, because UserStep
instances should be generated from the predefined Step
instances for the particular User
.
Generating UserSteps for User
In this section, you will implement the generation and showing of UserStep
instances for the edited User
.
Adding JoiningDate Attribute
First, let’s add the joiningDate
attribute to the User
entity:
It will be used to calculate the dueDate
attribute of the generated UserStep
entity: UserStep.dueDate = User.joiningDate + Step.duration
.
If your application is running, stop it using the Stop button () in the main toolbar.
Click Add () in the Attributes toolbar of the User
entity designer. In the New Attribute dialog, enter joiningDate
into the Name field and select LocalDate
in the Type field:
Click OK.
Select the newly created joiningDate
attribute and click the Add to Views () button in the Attributes toolbar. Select both User.detail
and User.list
views in the appeared dialog and click OK.
Click the Debug button () in the main toolbar.
Studio will generate a Liquibase changelog for adding the JOINING_DATE
column to the USER_
table. Confirm the changelog.
Studio will execute the changelog and start the application. Open http://localhost:8080
in your web browser, log in to the application and check that the new attribute is shown in the User list and detail views.
Adding Custom Button
Now you need to remove the standard actions and buttons for managing UserSteps and add a button for starting a custom logic of creating entities.
Open user-detail-view.xml
and remove all button
elements from hbox
and the entire actions
element from dataGrid
:
<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>
Then select buttonsPanel
in the Jmix UI hierarchy panel and click Add Component in the context menu of the node. Find Components → Button
in the palette and double-click it. Set the id
of the created button
element to generateButton
and text
to Generate
in the Jmix UI inspector panel. After that, switch to the Handlers tab and create a ClickEvent
handler method:
Press Ctrl/Cmd+S and switch to the running application. Refresh the User detail view and check that the Generate button is shown instead of the standard CRUD buttons:
Creating and Saving UserStep Instances
Let’s implement the logic of generating UserStep
instances.
Add the following fields to the UserDetailView
controller:
public class UserDetailView extends StandardDetailView<User> {
@Autowired
private DataManager dataManager;
@Autowired
private Notifications notifications;
@ViewComponent
private DataContext dataContext;
@ViewComponent
private CollectionPropertyContainer<UserStep> stepsDc;
If you copy the fields above and paste them to the source code, IDE will highlight them as errors because you also need to add |
You can inject view components and Spring beans using the Inject button in the actions panel: |
Add the logic of creating and saving UserStep
objects to the generateButton
click handler method:
@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 | Use getEditedEntity() method of the base StandardDetailView class to get the User being edited. |
2 | If joiningDate attribute is not set, show a message and quit. |
3 | Load the list of registered Steps. |
4 | Skip the Step if it’s already in the stepsDc collection container. |
5 | Create new UserStep instance using DataContext.create() method. |
6 | Add the new UserStep instance to the stepsDc collection container to show it in the UI. |
When you create an entity instance through the DataContext object, the instance becomes managed by DataContext and is saved automatically when the view is saved, that is, when you click OK button of the view.
|
Press Ctrl/Cmd+S and switch to the running application. Refresh the User detail view and check that when you click the Generate button, a few records corresponding to the onboarding Steps are created.
If you save the view by clicking OK, all created UserSteps will be saved. If you click Cancel, nothing will be saved to the database. It happens because in the code above, you don’t save the created UserStep
objects directly to the database. Instead, you merge them into the view’s DataContext
by creating them through DataContext.create()
. So the new instances are saved only when the entire DataContext
is saved.
Improving UserSteps Data Grid
In the sections below, you will finalize the UI for working with generated UserSteps.
Ordering Nested Collection
You may notice that when you open a User with previously generated UserSteps, they are not ordered according to the sortValue
attribute:
The data grid displays the steps
collection attribute of the User
entity, so you can introduce ordering on the data model level.
Open the User
entity, select steps
attribute and enter sortValue
to the Order by field:
If you switch to the Text tab, you can see the @OrderBy
annotation on the steps
attribute:
@OrderBy("sortValue")
@Composition
@OneToMany(mappedBy = "user")
private List<UserStep> steps;
Now when you load the User
entity, its steps
collection will be sorted by the UserStep.sortValue
attribute.
If your application is running, restart it.
Open the User detail view. Now the ordering of UserSteps is correct:
Rearranging Data Grid Columns
Currently, the UserSteps data grid is not very informative. Let’s remove the Version
and Sort value
columns and add a column showing the Step name.
Removing a column is straightforward: just select it in the Jmix UI hierarchy panel and press Delete, or remove the element directly from XML.
To add a column, select the columns
element in the Jmix UI hierarchy panel and click Add → Column in the Jmix UI inspector panel. The Add Column dialog appears:
As you can see, it doesn’t allow you to add a Step name. This is because the step
attribute is a reference, and you didn’t define a proper fetch plan to load it.
Select the userDc
data container in the Jmix UI hierarchy panel and click the Edit button () either in the fetchPlan
property in the Jmix UI inspector panel or in the gutter of the XML editor:
In the Edit Fetch Plan window, select steps
→ step
attribute and click OK:
The nested attribute will be added to 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>
Now the collection of UserSteps will be eagerly loaded from the database together with the related Step
instances.
Select the columns
element in the Jmix UI hierarchy panel and click Add → Column in the Jmix UI inspector panel. The Add Column dialog now contains the related Step
entity and its attributes:
Select step
→ name
and click OK. The new column will be added to the end of the columns list:
<dataGrid id="stepsDataGrid" dataContainer="stepsDc" ...>
<columns>
<column property="dueDate"/>
<column property="completedDate"/>
<column property="step.name"/>
</columns>
Instead of step.name
you could use just step
. In this case, the column would display the instance name of the entity. For Step
, the instance name is obtained from the name
attribute, so the result would be the same.
You could also add the step column without modifying the fetch plan, and the UI would still work due to lazy loading of references. But then Step instances would be loaded by separate requests for each UserStep in the collection (N+1 query problem).
|
Move the step.name
column to the beginning by dragging and dropping the element in the Jmix UI hierarchy panel or editing the XML directly:
<dataGrid id="stepsDataGrid" dataContainer="stepsDc" width="100%" height="100%">
<columns>
<column property="step.name"/>
<column property="dueDate"/>
<column property="completedDate"/>
</columns>
</dataGrid>
Press Ctrl/Cmd+S and switch to the running application. Refresh the User detail view and check that the data grid now shows the Step name:
Adding Component Column
In this section, you will implement an ability to mark a UserStep completed by clicking a checkbox in the data grid row. The checkbox will be rendered in the additional column on the left side of the data grid.
Add a new column to stepsDataGrid
:
<dataGrid id="stepsDataGrid" ...>
<columns>
<column key="completed" sortable="false" width="4em" flexGrow="0"/>
This column is not bound to an entity attribute, so it has the key
attribute instead of property
.
Select the completed
column, switch to the Handlers tab of the component inspector and create the renderer
handler method:
@Supply(to = "stepsDataGrid.completed", subject = "renderer")
private Renderer<UserStep> stepsDataGridCompletedRenderer() {
return null;
}
Inject UiComponents
object into controller class:
@Autowired
private UiComponents uiComponents;
You can use Inject button in the top actions panel of the editor to inject dependencies into view controllers and Spring beans. |
Implement the stepsDataGridCompletedRenderer
method:
@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 | The method returns a Renderer object that creates a UI component to be rendered in the column. The renderer receives an entity instance for the current row. |
2 | The Checkbox component instance is created using the UiComponents factory. |
3 | When you click the checkbox, its value changes and the checkbox calls its ValueChangeEvent listener. The listener sets the completedDate attribute of the UserStep entity. |
4 | The renderer returns the visual component to be shown in the column cells. |
Press Ctrl/Cmd+S and switch to the running application. Refresh the User detail view and click checkboxes for some rows. The Completed date column will change accordingly:
The changes in UserStep
instances will be saved to the database when you click OK in the view. It’s the responsibility of the screen’s DataContext
: it tracks changes in all entities and saves to the database changed instances.
Reacting to Changes
When you generate steps for the user, mark a UserStep completed or remove a step, the Onboarding status
field should change accordingly.
Let’s implement reaction to the UserSteps collection changes.
Open UserDetailView
controller and click Generate Handler in the top actions panel. Collapse all items, then select ItemPropertyChangeEvent
and CollectionChangeEvent
items in Data containers handlers
→ stepsDc
:
Click OK.
Studio will generate two method stubs: onStepsDcItemPropertyChange()
and onStepsDcCollectionChange()
. Implement them as below:
@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 handler is invoked when an attribute of the entity changes. |
2 | CollectionChangeEvent handler is invoked when items are added to or removed from the container. |
3 | Get the currently edited User instance. |
4 | Update onboardingStatus attribute. Thanks to the data binding, the changed value will be immediately shown by the UI component. |
Press Ctrl/Cmd+S and switch to the running application. Refresh the User detail view and make some changes in the UserStep table. Watch the Onboarding status
field value.
Summary
In this section, you have implemented two features:
-
Ability to specify a department for a user.
-
Generation and management of onboarding steps for a user.
You have learned that:
-
Reference attributes should be added to a screen’s fetch plan to avoid the N+1 query problem.
-
The entityComboBox component can be used to select a related entity from a dropdown. This component requires a CollectionContainer with options to be set in the
itemsContainer
property. -
The relationship between
User
andUserStep
entities is an example of composition, when instances of the related entity (UserStep
) can exist only as a part of its owner (User
). Such a reference is marked with @Composition annotation. -
A collection of related entities can be ordered using the
@OrderBy
annotation on the reference attribute. -
The
ClickEvent
handler of the button component is used to handle button clicks. It can be generated on the Handlers tab of the Jmix UI inspector panel. -
The
getEditedEntity()
method of the entity detail view returns the entity instance being edited. -
The Notifications interface is used to show popup notifications.
-
The DataManager interface can be used to load data from the database.
-
A nested collection of related entities is loaded into a CollectionPropertyContainer. Its
getItems()
andgetMutableItems()
methods can be used to iterate over and to add/remove items to the collection. -
DataContext tracks changes in entities and saves changed instances to the database when user clicks OK in the screen.
-
The UI data grid can have columns that display arbitrary visual components.
-
ItemPropertyChangeEvent and CollectionChangeEvent can be used to react to changes in entities located in data containers.