5. Работа с данными в пользовательском интерфейсе

На данном этапе разработки в приложении есть управление шагами и отделами, а также управление пользователями с добавленным атрибутом Onboarding status. Теперь вам нужно связать пользователей с шагами онбординга и отделами.

В этой главе вы сделаете следующее:

  • Добавите атрибуты department и joiningDate к сущности User и отобразите их в пользовательском интерфейсе.

  • Создадите сущность UserStep, которая связывает пользователя с шагом онбординга.

  • Добавите коллекцию сущностей UserStep к сущности User и покажите ее на экране User.edit.

  • Реализуете генерацию и сохранение экземпляров сущности UserStep на экране User.edit.

На приведенной ниже диаграмме показаны сущности и атрибуты, рассматриваемые в этой главе:

data in ui diagram

Добавление ссылочного атрибута

Вы уже выполняли аналогичную задачу, когда создавали атрибут HR-менеджера сущности Department в качестве ссылки на User. Теперь вам нужно создать ссылку в обратном направлении: у пользователя должна быть ссылка на отдел.

data in ui diagram 2

Если ваше приложение запущено, остановите его с помощью кнопки Stop (suspend) на главной панели инструментов.

Дважды щелкните на сущности User в окне инструментов Jmix и выберите ее последний атрибут (чтобы добавить новый атрибут в конец).

Нажмите Add (add) на панели Attributes.

В диалоговом окне New Attribute введите department в поле Name. Затем выберите:

  • Attribute type: ASSOCIATION

  • Type: Department

  • Cardinality: Many to One

add attr 1

Нажмите на кнопку OK.

Выберите новый атрибут department и нажмите на кнопку Add to Screens (add attribute to screens) на панели Attributes :

add attr 2

В появившемся диалоговом окне будут показаны экраны User.edit и User.browse. Выберите оба экрана и нажмите на кнопку OK.

Studio добавит атрибут department в компонент таблицы экрана User.browse и в компонент формы экрана User.edit.

Вы также можете заметить следующий код, автоматически добавленный студией в user-browse.xml:

<data readOnly="true">
    <collection id="usersDc"
                class="com.company.onboarding.entity.User">
        <fetchPlan extends="_base">
            <property name="department" fetchPlan="_base"/> <!-- added -->
        </fetchPlan>

И в user-edit.xml:

<data>
    <instance id="userDc"
              class="com.company.onboarding.entity.User">
        <fetchPlan extends="_base">
            <property name="department" fetchPlan="_base"/> <!-- added -->
        </fetchPlan>

С помощью этого кода указанный отдел будет загружен вместе с пользователем в одном запросе к базе данных.

Экраны будут работать и без включения department в фетч-план благодаря загрузке связанных сущностей по требованию (lazy loading). Но в этом случае ссылки будут загружаться отдельными запросами к базе данных. Отложенная загрузка может повлиять на производительность экрана просмотра, потому что сначала экран загружает список пользователей первым запросом, а после этого выполняет отдельные запросы для загрузки отдела каждого пользователя в списке (проблема N+1 запросов).

Давайте запустим приложение и посмотрим на новый атрибут в действии.

Нажмите кнопку Debug (start debugger) на главной панели инструментов.

Studio сгенерирует Liquibase changelog для добавления столбца DEPARTMENT_ID в таблицу USER_, создания ограничения внешнего ключа и индекса. Подтвердите changelog.

Студия исполнит файл changelog и запустит приложение.

Откройте http://localhost:8080 в вашем веб-браузере и войдите в приложение с учетными данными администратора (admin / admin).

Раскройте меню Application и нажмите на подпункт Users. Вы увидите колонку Department на экране User.browse и поле выбора Department на экране User.edit:

add attr 3

Использование выпадающего списка для выбора ссылки

По умолчанию Studio генерирует компонент entityPicker для выбора ссылок. Вы можете увидеть это на экране User.edit. Откройте user-edit.xml и найдите компонент entityPicker внутри компонента form:

<layout ...>
    <form id="form" dataContainer="userDc">
        <column width="350px">
            ...
            <entityPicker id="departmentField" property="department"/>
        </column>
    </form>

Этот компонент позволяет вам выбрать связанную сущность на экране поиска с фильтрацией, сортировкой и пейджингом. Но когда ожидаемое количество записей относительно невелико (скажем, менее 1000), удобнее выбирать ссылки из простого выпадающего списка.

Давайте изменим экран User.edit и используем компонент entityComboBox для выбора отдела.

Измените XML-элемент компонента на entityComboBox:

<entityComboBox id="departmentField" property="department"/>

Переключитесь на запущенное приложение и снова откройте экран редактирования пользователя.

Вы увидите, что поле Department теперь является выпадающим списком, но его список пуст, даже если вы создали несколько отделов.

dropdown 2

Создание опций контейнера данных

Давайте предоставим список опций компоненту entityComboBox, отображающему ссылку на отдел. Список должен содержать все отделы, упорядоченные по названию.

Нажмите на кнопку Add Component на панели действий, выберите раздел Data components и дважды щелкните на элементе Collection. В окне Data Container Properties Editor выберите Department в поле Entity и нажмите на кнопку OK:

options container 1

Новый элемент collection с именем departmentsDc будет создан под элементом data на панели иерархии Jmix UI и в 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>

Этот элемент определяет контейнер коллекции данных и загрузчик для него. Контейнер данных будет содержать список сущностей Department, загруженных загрузчиком с указанным запросом.

Вы можете отредактировать запрос прямо в XML или использовать конструктор JPQL. Чтобы открыть конструктор, щелкните по ссылке напротив атрибута query, находящейся на панели инспектора Jmix UI:

options container 2

В окне JPQL Query Designer перейдите на вкладку ORDER и добавьте атрибут name в список:

options container 3

Нажмите на кнопку OK.

Результирующий запрос в формате XML будет выглядеть следующим образом:

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

Теперь вам нужно связать компонент entityComboBox с контейнером коллекции departmentsDc.

Выберите departmentField на панели иерархии Jmix UI и departmentsDc для атрибута optionsContainer на панели инспектора Jmix UI:

options container 4

Переключитесь на запущенное приложение и снова откройте экран редактирования пользователя.

Вы увидите, что в раскрывающемся списке Department теперь есть список опций:

dropdown 3
Компонент entityComboBox позволяет пользователю фильтровать опции, вводя текст в поле. Но имейте в виду, что фильтрация выполняется в памяти сервера, и все опции загружаются из базы данных сразу.

Создание сущности UserStep

В этом разделе вы создадите сущность UserStep, которая представляет собой шаг онбординга для конкретного пользователя:

data in ui diagram 3

Если ваше приложение запущено, остановите его с помощью кнопки Stop (suspend) на главной панели инструментов.

В окне инструментов Jmix нажмите New (add) → JPA Entity и создайте сущность UserStep с чертой Versioned, как вы делали раньше.

Добавьте следующие атрибуты к новой сущности:

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

Конечное состояние дизайнера сущностей должно выглядеть следующим образом:

create user step 1

Добавление атрибута-композиции

Рассмотрим взаимосвязь между сущностями User и UserStep. Экземпляры UserStep существуют только в контексте конкретного экземпляра сущности User (принадлежат ему). Экземпляр UserStep не может сменить своего владельца - это не имеет никакого смысла. Кроме того, ссылок на UserStep из других объектов модели данных нет, они полностью инкапсулированы в контексте User.

В Jmix такая взаимосвязь называется композицией: пользователь (User), среди прочих атрибутов, включает в себя набор пользовательских шагов (UserStep).

Композиция в Jmix реализует шаблон проектирования Aggregate подхода Domain-Driven Design.

Часто бывает удобно создать атрибут, содержащий коллекцию элементов композиции в сущности-владельце.

Давайте создадим атрибут steps в сущности User:

data in ui diagram 4

Если ваше приложение запущено, остановите его с помощью кнопки Stop (suspend) на главной панели инструментов.

Нажмите кнопку Add (add) на панели Attributes дизайнера сущности User. В диалоговом окне New Attribute введите steps в поле Name. Затем выберите:

  • Attribute type: COMPOSITION

  • Type: UserStep

  • Cardinality: One to Many

composition 1

Обратите внимание, что user выбирается автоматически в поле Mapped by. Это атрибут сущности UserStep, сопоставленный столбцу базы данных, который поддерживает связь между UserSteps и Users (внешний ключ).

Нажмите на кнопку OK.

Исходный код атрибута будет иметь аннотацию @Composition:

@Composition
@OneToMany(mappedBy = "user")
private List<UserStep> steps;

Шаги пользователя должны отображаться на экране редактирования пользователя, поэтому выберите новый атрибут steps и нажмите кнопку Add to Screens (add attribute to screens) на панели Attributes. Выберите User.edit и нажмите на кнопку OK.

Студия изменит user-edit.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 ...>
    <form id="form" dataContainer="userDc">
        ...
    </form>
    <groupBox id="stepsBox" ...>
        <table id="stepsTable" dataContainer="stepsDc" ...> (3)
            <actions>
                <action id="create" type="create"/>
                <action id="edit" type="edit"/>
                <action id="remove" type="remove"/>
            </actions>
            <columns>
                <column id="version"/>
                <column id="dueDate"/>
                <column id="completedDate"/>
                <column id="sortValue"/>
            </columns>
            <buttonsPanel>
                <button action="stepsTable.create"/>
                <button action="stepsTable.edit"/>
                <button action="stepsTable.remove"/>
            </buttonsPanel>
        </table>
    </groupBox>
1 Атрибут steps фетч-плана гарантирует, что коллекция пользовательских шагов загружается жадно (eager fetching) вместе с пользователем (User).
2 Вложенный контейнер коллекции данных stepsDc позволяет привязывать визуальные компоненты к атрибуту-коллекции steps.
3 Компонент table, заключенный в groupBox, отображает данные из связанного контейнера коллекции stepsDc.

Давайте запустим приложение и посмотрим на эти изменения в действии.

Нажмите на кнопку Debug (start debugger) на главной панели инструментов.

Studio сгенерирует Liquibase changelog для создания таблицы USER_STEP, ограничения внешнего ключа и индексов для ссылок на USER_ и STEP. Подтвердите список изменений.

Студия исполнит файл changelog и запустит приложение.

Откройте http://localhost:8080 в вашем веб-браузере и войдите в приложение с учетными данными администратора (admin / admin).

Откройте экран редактирования пользователя. Вы увидите таблицу Steps, отображающую сущности UserStep:

composition 2

Если вы нажмете Create в таблице Steps, вы получите исключение, сообщающее: Screen 'UserStep.edit' is not defined. Это правда - вы не создавали экран редактирования для сущности UserStep. Но на самом деле это и не нужно, потому что экземпляры UserStep должны быть сгенерированы из предопределенных экземпляров сущности Step для конкретного пользователя.

Генерация пользовательских шагов

В этом разделе вы реализуете генерацию и отображение экземпляров сущности UserStep для редактируемой сущности User.

Добавление атрибута joiningDate

Во-первых, давайте добавим атрибут joiningDate к сущности User:

data in ui diagram 5

Он будет использоваться для вычисления атрибута dueDate сгенерированной сущности UserStep по следующей формуле: UserStep.dueDate = User.joiningDate + Step.duration.

Если ваше приложение запущено, остановите его с помощью кнопки Stop (suspend) на главной панели инструментов.

Нажмите на кнопку Add (add) на панели Attributes дизайнера сущности User. В диалоговом окне New Attribute введите joiningDate в поле Name и выберите LocalDate в поле Type:

joining date 1

Нажмите на кнопку OK.

Выберите ранее созданный атрибут joiningDate и нажмите кнопку Add to Screens (add attribute to screens) на панели Attributes. Выберите оба экрана User.edit и User.browse в появившемся диалоговом окне и нажмите OK.

Нажмите на кнопку Debug (start debugger) на главной панели инструментов.

Studio сгенерирует Liquibase changelog для добавления столбца JOINING_DATE в таблицу USER_. Подтвердите changelog.

Студия исполнит changelog и запустит приложение. Откройте http://localhost:8080 в вашем веб-браузере, войдите в приложение и убедитесь, что новый атрибут отображается на экранах редактирования пользователя.

Добавление пользовательской кнопки

Теперь вам нужно удалить стандартные действия и кнопки для управления шагами пользователя и добавить кнопку для запуска пользовательской логики создания сущностей.

Откройте user-edit.xml и удалите элемент actions и все элементы button из таблицы:

<table id="stepsTable" dataContainer="stepsDc" width="100%" height="200px">
    <columns>
        <column id="version"/>
        <column id="dueDate"/>
        <column id="completedDate"/>
        <column id="sortValue"/>
    </columns>
    <buttonsPanel>
    </buttonsPanel>
</table>

Затем нажмите на кнопку Add Component на панели действий и перетащите компонент Button на элемент buttonsPanel в XML-дескриптор экрана. Затем выберите созданный элемент button и в панели инспектора Jmix UI укажите свойству id значение generateButton, а свойству caption - значение Generate. После этого перейдите на вкладку Handlers и создайте метод обработчика ClickEvent:

button 1

Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Снова откройте экран редактирования пользователя и убедитесь, что вместо стандартных CRUD-кнопок отображается кнопка Generate:

button 2

Создание и сохранение экземпляров UserStep

Давайте реализуем логику генерации экземпляров UserStep.

Добавьте следующие поля в контроллер UserEdit:

public class UserEdit extends StandardEditor<User> {

    @Autowired
    private DataManager dataManager;

    @Autowired
    private DataContext dataContext;

    @Autowired
    private CollectionPropertyContainer<UserStep> stepsDc;

Вы можете инжектировать компоненты экрана и бины Spring с помощью кнопки Inject на панели действий:

inject 1

Добавьте логику создания и сохранения объектов UserStep в метод обработки нажатия кнопки generateButton:

@Subscribe("generateButton")
public void onGenerateButtonClick(Button.ClickEvent event) {
    User user = getEditedEntity(); (1)

    if (user.getJoiningDate() == null) { (2)
        notifications.create()
                .withCaption("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 Используйте метод getEditedEntity() базового класса StandardEditor, чтобы получить редактируемого пользователя.
2 Если атрибут joiningDate не установлен, показать сообщение и завершить работу.
3 Загрузить список зарегистрированных шагов.
4 Пропустить Step, если он уже находится в контейнере коллекции stepsDc.
5 Создать новый экземпляр UserStep, используя метод DataContext.create().
6 Добавить новый экземпляр UserStep в контейнер коллекции stepsDc, чтобы отобразить его в пользовательском интерфейсе.
Когда вы создаете экземпляр сущности с помощью объекта DataContext, данный экземпляр далее отслеживается в DataContext и автоматически сохраняется при коммите экрана, то есть при нажатии кнопки OK на экране.

Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Снова откройте экран редактирования пользователя и убедитесь, что при нажатии кнопки Generate создается несколько записей, соответствующих шагам онбординга.

Если вы закоммитите экран, нажав кнопку OK, все созданные экземпляры UserStep будут сохранены. Если вы нажмете кнопку Cancel, в базе данных ничего сохранено не будет. Это происходит потому, что в приведенном выше коде вы не сохраняете созданные экземпляры UserStep непосредственно в базу данных. Вместо этого вы объединяете их в DataContext экрана, создавая их с помощью DataContext.create(). Таким образом, новые экземпляры сохраняются только тогда, когда весь DataContext закоммичен.

Улучшение таблицы Steps

В нижеприведенных разделах вы доработаете пользовательский интерфейс для работы со сгенерированными пользовательскими шагами.

Упорядочивание вложенной коллекции

Вы можете заметить, что когда вы открываете пользователя с ранее сгенерированными пользовательскими шагами, они не упорядочены в соответствии с атрибутом sortValue:

ordering 1

В таблице отображается атрибут коллекции steps сущности User, поэтому вы можете ввести порядок на уровне модели данных.

Откройте сущность User, выберите атрибут steps и введите sortValue в поле Order by:

ordering 2

Если вы переключитесь на вкладку Text, вы сможете увидеть аннотацию @OrderBy у атрибута steps:

@OrderBy("sortValue")
@Composition
@OneToMany(mappedBy = "user")
private List<UserStep> steps;

Теперь, когда вы загружаете сущность User, его коллекция steps будет отсортирована по атрибуту UserStep.sortValue.

Если ваше приложение запущено, перезапустите его.

Откройте экран редактирования пользователя. Теперь порядок пользовательских шагов правильный:

ordering 3

Перестановка колонок таблицы

В настоящее время таблица пользовательских шагов не очень информативна. Давайте удалим колонки Version и Sort value и добавим колонку, показывающую название шага.

Удалить колонку просто: выберите ее на панели иерархии Jmix UI и нажмите Delete или удалите элемент непосредственно из XML.

Чтобы добавить колонку, выберите элемент columns на панели иерархии Jmix UI и нажмите AddColumn на панели инспектора Jmix UI. Появится диалоговое окно Add Column:

columns 2

Как вы можете видеть, это не позволяет вам добавить название шага. Это связано с тем, что атрибут step является ссылкой, и вы не определили надлежащий фетч-план для его загрузки.

Выберите контейнер данных userDc на панели иерархии Jmix UI и нажмите кнопку Edit (edit) либо в свойстве fetchPlan на панели инспектора Jmix UI, либо в маркере строки редактора XML:

columns 3

В окне Edit Fetch Plan выберите атрибут stepsstep и нажмите на кнопку OK:

columns 4

Вложенный атрибут будет добавлен в фетч-план в редакторе 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>

Теперь коллекция пользовательских шагов будет жадно загружена из базы данных вместе с экземпляром пользователя.

Выберите элемент columns на панели иерархии Jmix UI и нажмите AddColumn на панели инспектора Jmix UI. Диалоговое окно Add Column теперь содержит связанную сущность Step и ее атрибуты:

columns 5

Выберите stepname и нажмите на кнопку OK. Новая колонка будет добавлен в конец списка колонок:

<table id="stepsTable" dataContainer="stepsDc" ...>
    <columns>
        <column id="dueDate"/>
        <column id="completedDate"/>
        <column id="step.name"/>
    </columns>

Вместо step.name вы могли бы использовать просто step. В этом случае в колонке будет отображаться имя экземпляра сущности. Для сущности Step имя экземпляра получается из атрибута name, поэтому результат будет таким же.

Вы также можете добавить колонку step непосредственно в XML без изменения фетч-плана, и пользовательский интерфейс все равно будет работать из-за отложенной загрузки ссылок. Но тогда экземпляры сущности Step будут загружаться отдельными запросами для каждого экземпляра UserStep в коллекции (проблема N+1 запросов).

Передвиньте колонку step.name в начало, перетаскивая элемент на панели иерархии Jmix UI или редактируя XML напрямую:

<table id="stepsTable" dataContainer="stepsDc" ...>
    <columns>
        <column id="step.name"/>
        <column id="dueDate"/>
        <column id="completedDate"/>
    </columns>

Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Снова откройте экран редактирования пользователя и убедитесь, что в таблице Steps теперь отображается название шага:

columns 6

Добавление генерируемой колонки

В этом разделе вы реализуете возможность отмечать выполненный пользователем шаг, устанавливая флажок в строке таблицы.

Подходящий компонент должен иметь так называемые генерируемые колонки, не связанные с определенными атрибутами сущности. В ячейке генерируемой колонки вы можете отобразить любой визуальный компонент или контейнер с несколькими компонентами внутри.

Давайте добавим генерируемую колонку, в которой отображается флажок.

Выберите элемент columns на панели иерархии Jmix UI и нажмите AddColumn на панели инспекторе Jmix UI. Появится диалоговое окно Add Column:

generated column 1

Выберите New Custom Column и нажмите на кнопку OK.

В диалоговом окне Additional Settings for Custom Column введите completed в поле Custom column id и установите флажок Create generator:

generated column 2

Нажмите на кнопку OK.

Studio добавит колонку completed в таблицу XML:

generated column 3

и метод обработчика для контроллера UserEdit:

generated column 4

Обратите внимание на маркеры строк слева: они позволяют вам переключаться между определением колонки в XML и ее методом-обработчиком в контроллере.

Инжектируйте объект UiComponents в класс контроллера:

@Autowired
private UiComponents uiComponents;
Вы можете использовать кнопку Inject на верхней панели действий дизайнера, чтобы инжектировать зависимости в контроллеры экрана и бины Spring.

Реализуйте метод обработчика:

@Install(to = "stepsTable.completed", subject = "columnGenerator") (1)
private Component stepsTableCompletedColumnGenerator(UserStep userStep) { (2)
    CheckBox checkBox = uiComponents.create(CheckBox.class); (3)
    checkBox.setValue(userStep.getCompletedDate() != null);
    checkBox.addValueChangeListener(e -> { (4)
        if (userStep.getCompletedDate() == null) {
            userStep.setCompletedDate(LocalDate.now());
        } else {
            userStep.setCompletedDate(null);
        }
    });
    return checkBox; (5)
}
1 Аннотация @Install указывает, что метод является делегатом: UI компонент (в данном случае таблица) вызывает его на каком-то этапе своего жизненного цикла.
2 Этот конкретный делегат (генератор колонок) получает экземпляр сущности, который отображается в строке таблицы в качестве аргумента.
3 Экземпляр компонента CheckBox создается с помощью фабрики компонентов UiComponents.
4 Когда вы нажимаете на флажок, его значение изменяется, и флажок вызывает свой слушатель ValueChangeEvent. Слушатель устанавливает атрибут completedDate у сущности UserStep.
5 Делегат генератора колонки возвращает визуальный компонент, который будет отображаться в ячейках колонки.

Переместите колонку completed наверх, установите для свойства caption значение пустой строки и для width значение 50px:

<table id="stepsTable" dataContainer="stepsDc" ...>
    <columns>
        <column id="completed" caption="" width="50px"/>
        <column id="step.name"/>
        <column id="dueDate"/>
        <column id="completedDate"/>
    </columns>

Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Снова откройте экран редактирования пользователя и установите флажки для некоторых строк. Колонка Completed date изменится соответствующим образом:

generated column 5

Изменения в экземплярах UserStep будут сохранены в базе данных, когда вы нажмете OK на экране. За это отвечает объект экрана DataContext: он отслеживает изменения во всех сущностях и сохраняет в базе данных измененные экземпляры.

Реагирование на изменения

Когда пользователь создает шаги, отмечает завершенный шаг или удаляет шаг, поле Onboarding status должно соответствующим образом измениться.

Давайте реализуем реакцию на изменения коллекции пользовательских шагов.

Откройте контроллер UserEdit и нажмите Generate Handler на верхней панели действий. Сверните все элементы, затем выберите элементы ItemPropertyChangeEvent и CollectionChangeEvent в Data containers handlersstepsDc:

container listener 1

Нажмите на кнопку OK.

Studio сгенерирует два заглушки метода: onStepsDcItemPropertyChange() и onStepsDcCollectionChange(). Реализуйте их, как показано ниже:

@Subscribe(id = "stepsDc", target = Target.DATA_CONTAINER)
public void onStepsDcItemPropertyChange(InstanceContainer.ItemPropertyChangeEvent<UserStep> event) {
    updateOnboardingStatus(); (1)
}

@Subscribe(id = "stepsDc", target = Target.DATA_CONTAINER)
public void onStepsDcCollectionChange(CollectionContainer.CollectionChangeEvent<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 и переключитесь на запущенное приложение. Снова откройте экран редактирования пользователя и внесите некоторые изменения в таблицу шагов пользователя. Посмотрите на значение поля Onboarding status.

Резюме

В этом разделе вы реализовали две функции:

  1. Возможность указать отдел для пользователя.

  2. Генерация и управление шагами онбординга для пользователя.

Вы узнали, что:

  • Ссылочные атрибуты должны быть добавлены в фетч-план экрана, чтобы избежать проблемы N+1 запросов.

  • Компонент EntityComboBox можно использовать для выбора связанной сущности из выпадающего списка. Для этого компонента требуется контейнер коллекции, содержащий элементы списка (опции). Он должен должен быть установлен в свойстве optionsContainer компонента.

  • Взаимосвязь между сущностями User и UserStep является примером композиции, когда экземпляры связанной сущности (UserStep) могут существовать только как часть ее владельца (User). Такая ссылка помечается аннотацией @Composition.

  • Коллекцию связанных сущностей можно упорядочить, используя аннотацию @OrderBy в ссылочном атрибуте.

  • Обработчик событий ClickEvent компонента Button используется для обработки нажатий кнопок. Его можно сгенерировать на вкладке Handlers панели инспектора Jmix UI.

  • Метод getEditedEntity() контроллера экрана редактирования возвращает редактируемый экземпляр сущности.

  • Интерфейс Notifications используется для отображения всплывающих уведомлений.

  • Интерфейс DataManager можно использовать для загрузки данных из базы данных.

  • Вложенная коллекция связанных сущностей загружается в CollectionPropertyContainer. Его методы getItems() и getMutableItems() можно использовать для перебора и добавления/удаления элементов в коллекцию.

  • DataContext отслеживает изменения в сущностях и сохраняет измененные экземпляры в базе данных, когда пользователь нажимает OK на экране.

  • Таблица пользовательского интерфейса может иметь генерируемые колонки, которые отображают произвольные визуальные компоненты.

  • ItemPropertyChangeEvent и CollectionChangeEvent можно использовать для реагирования на изменения в объектах, расположенных в контейнерах данных.