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 and joiningDate attributes to the User 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 the User entity and show it on the User.edit screen.

  • Implement the generation and saving of UserStep instances in the User.detail view.

The diagram below shows the entities and attributes addressed in this chapter:

data in ui diagram

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.

data in ui diagram 2

If your application is running, stop it using the Stop button (suspend) 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 (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

add attr 1

Click OK.

Select the new department attribute and click the Add to Views (add attribute to screens) button in the Attributes toolbar:

add attr 2

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 readOnly="true">
    <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 screens 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 browse screen, 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 (start debugger) 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:

add attr 3

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>
    </form>

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:

dropdown 2

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:

options container 1

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">
            <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:

options container 2

In the JPQL Query Designer window, switch to the ORDER tab and add the name attribute to the list:

options container 3

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">
            <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:

options container 4

Switch to the running application and reopen the User editor screen.

You will see that the Department dropdown now has a list of options:

dropdown 3
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:

data in ui diagram 3

If your application is running, stop it using the Stop button (suspend) in the main toolbar.

In the Jmix tool window, click New (add) → 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:

create user step 1

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:

data in ui diagram 4

If your application is running, stop it using the Stop button (suspend) in the main toolbar.

Open the User entity designer and click Add (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

composition 1

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 (add attribute to screens) 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">
        ...
    </form>
    <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 (start debugger) 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:

composition 2

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:

data in ui diagram 5

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 (suspend) in the main toolbar.

Click Add (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:

joining date 1

Click OK.

Select the newly created joiningDate attribute and click the Add to Views (add attribute to screens) button in the Attributes toolbar. Select both User.detail and User.list views in the appeared dialog and click OK.

Click the Debug button (start debugger) 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 hbox in the Jmix UI hierarchy panel and click Add Component in the context menu of the node. 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:

button 1

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:

button 2

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;

You can inject view components and Spring beans using the Inject button in the actions panel:

inject 1

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:

ordering 1

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:

ordering 2

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:

ordering 3

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 AddColumn in the Jmix UI inspector panel. The Add Column dialog appears:

columns 2

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 (edit) either in the fetchPlan property in the Jmix UI inspector panel or in the gutter of the XML editor:

columns 3

In the Edit Fetch Plan window, select stepsstep attribute and click OK:

columns 4

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 AddColumn in the Jmix UI inspector panel. The Add Column dialog now contains the related Step entity and its attributes:

columns 5

Select stepname 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:

columns 6

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.

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.

Add the following code to the InitEvent handler method:

@Subscribe
public void onInit(final InitEvent event) {
    timeZoneField.setItems(List.of(TimeZone.getAvailableIDs()));

    Grid.Column<UserStep> completedColumn = stepsDataGrid.addComponentColumn(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)
    });
    completedColumn.setFlexGrow(0); (5)
    completedColumn.setWidth("4em");
    stepsDataGrid.setColumnPosition(completedColumn, 0); (6)
}
1 The addComponentColumn() method accepts a lambda which creates a UI component to be rendered in the column. The lambda 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 lambda returns the visual component to be shown in the column cells.
5 Setting the width of the created column.
6 Setting the position of the created column.

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:

generated column 5

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 handlersstepsDc:

container listener 1

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:

  1. Ability to specify a department for a user.

  2. 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 and UserStep 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() and getMutableItems() 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.