5. Работа с данными в пользовательском интерфейсе
На данном этапе разработки в приложении есть управление шагами и отделами, а также управление пользователями с добавленным атрибутом Onboarding status
. Теперь вам нужно связать пользователей с шагами онбординга и отделами.
В этой главе вы сделаете следующее:
-
Добавите атрибуты
department
иjoiningDate
к сущностиUser
и отобразите их в пользовательском интерфейсе. -
Создадите сущность
UserStep
, которая связывает пользователя с шагом онбординга. -
Добавите коллекцию сущностей
UserStep
к сущностиUser
и покажите ее на экранеUser.edit
. -
Реализуете генерацию и сохранение экземпляров сущности
UserStep
на экранеUser.edit
.
На приведенной ниже диаграмме показаны сущности и атрибуты, рассматриваемые в этой главе:
Добавление ссылочного атрибута
Вы уже выполняли аналогичную задачу, когда создавали атрибут HR-менеджера сущности Department
в качестве ссылки на User
. Теперь вам нужно создать ссылку в обратном направлении: у пользователя должна быть ссылка на отдел.
Если ваше приложение запущено, остановите его с помощью кнопки Stop () на главной панели инструментов.
Дважды щелкните на сущности User
в окне инструментов Jmix и выберите ее последний атрибут (чтобы добавить новый атрибут в конец).
Нажмите Add () на панели Attributes.
В диалоговом окне New Attribute введите department
в поле Name. Затем выберите:
-
Attribute type:
ASSOCIATION
-
Type:
Department
-
Cardinality:
Many to One
Нажмите на кнопку OK.
Выберите новый атрибут department
и нажмите на кнопку Add to Screens () на панели Attributes :
В появившемся диалоговом окне будут показаны экраны 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 () на главной панели инструментов.
Studio сгенерирует Liquibase changelog для добавления столбца DEPARTMENT_ID
в таблицу USER_
, создания ограничения внешнего ключа и индекса. Подтвердите changelog.
Студия исполнит файл changelog и запустит приложение.
Откройте http://localhost:8080
в вашем веб-браузере и войдите в приложение с учетными данными администратора (admin
/ admin
).
Раскройте меню Application и нажмите на подпункт Users. Вы увидите колонку Department
на экране User.browse
и поле выбора Department
на экране User.edit
:
Использование выпадающего списка для выбора ссылки
По умолчанию 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
теперь является выпадающим списком, но его список пуст, даже если вы создали несколько отделов.
Создание опций контейнера данных
Давайте предоставим список опций компоненту entityComboBox
, отображающему ссылку на отдел. Список должен содержать все отделы, упорядоченные по названию.
Нажмите на кнопку Add Component на панели действий, выберите раздел Data components и дважды щелкните на элементе Collection
. В окне Data Container Properties Editor выберите Department
в поле Entity и нажмите на кнопку OK:
Новый элемент 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:
В окне JPQL Query Designer перейдите на вкладку ORDER и добавьте атрибут name
в список:
Нажмите на кнопку 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:
Переключитесь на запущенное приложение и снова откройте экран редактирования пользователя.
Вы увидите, что в раскрывающемся списке Department
теперь есть список опций:
Компонент entityComboBox позволяет пользователю фильтровать опции, вводя текст в поле. Но имейте в виду, что фильтрация выполняется в памяти сервера, и все опции загружаются из базы данных сразу.
|
Создание сущности UserStep
В этом разделе вы создадите сущность UserStep
, которая представляет собой шаг онбординга для конкретного пользователя:
Если ваше приложение запущено, остановите его с помощью кнопки Stop () на главной панели инструментов.
В окне инструментов Jmix нажмите New () → 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 |
Конечное состояние дизайнера сущностей должно выглядеть следующим образом:
Добавление атрибута-композиции
Рассмотрим взаимосвязь между сущностями User
и UserStep
. Экземпляры UserStep
существуют только в контексте конкретного экземпляра сущности User
(принадлежат ему). Экземпляр UserStep
не может сменить своего владельца - это не имеет никакого смысла. Кроме того, ссылок на UserStep
из других объектов модели данных нет, они полностью инкапсулированы в контексте User
.
В Jmix такая взаимосвязь называется композицией: пользователь (User
), среди прочих атрибутов, включает в себя набор пользовательских шагов (UserStep
).
Композиция в Jmix реализует шаблон проектирования Aggregate подхода Domain-Driven Design. |
Часто бывает удобно создать атрибут, содержащий коллекцию элементов композиции в сущности-владельце.
Давайте создадим атрибут steps
в сущности User
:
Если ваше приложение запущено, остановите его с помощью кнопки Stop () на главной панели инструментов.
Нажмите кнопку Add () на панели Attributes дизайнера сущности User
. В диалоговом окне New Attribute введите steps
в поле Name. Затем выберите:
-
Attribute type:
COMPOSITION
-
Type:
UserStep
-
Cardinality: One to Many
Обратите внимание, что user
выбирается автоматически в поле Mapped by. Это атрибут сущности UserStep
, сопоставленный столбцу базы данных, который поддерживает связь между UserSteps и Users (внешний ключ).
Нажмите на кнопку OK.
Исходный код атрибута будет иметь аннотацию @Composition
:
@Composition
@OneToMany(mappedBy = "user")
private List<UserStep> steps;
Шаги пользователя должны отображаться на экране редактирования пользователя, поэтому выберите новый атрибут steps
и нажмите кнопку Add 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 () на главной панели инструментов.
Studio сгенерирует Liquibase changelog для создания таблицы USER_STEP
, ограничения внешнего ключа и индексов для ссылок на USER_
и STEP
. Подтвердите список изменений.
Студия исполнит файл changelog и запустит приложение.
Откройте http://localhost:8080
в вашем веб-браузере и войдите в приложение с учетными данными администратора (admin
/ admin
).
Откройте экран редактирования пользователя. Вы увидите таблицу Steps, отображающую сущности UserStep
:
Если вы нажмете Create в таблице Steps, вы получите исключение, сообщающее: Screen 'UserStep.edit' is not defined
. Это правда - вы не создавали экран редактирования для сущности UserStep
. Но на самом деле это и не нужно, потому что экземпляры UserStep
должны быть сгенерированы из предопределенных экземпляров сущности Step
для конкретного пользователя.
Генерация пользовательских шагов
В этом разделе вы реализуете генерацию и отображение экземпляров сущности UserStep
для редактируемой сущности User
.
Добавление атрибута joiningDate
Во-первых, давайте добавим атрибут joiningDate
к сущности User
:
Он будет использоваться для вычисления атрибута dueDate
сгенерированной сущности UserStep
по следующей формуле: UserStep.dueDate = User.joiningDate + Step.duration
.
Если ваше приложение запущено, остановите его с помощью кнопки Stop () на главной панели инструментов.
Нажмите на кнопку Add () на панели Attributes дизайнера сущности User
. В диалоговом окне New Attribute введите joiningDate
в поле Name и выберите LocalDate
в поле Type:
Нажмите на кнопку OK.
Выберите ранее созданный атрибут joiningDate
и нажмите кнопку Add to Screens () на панели Attributes. Выберите оба экрана User.edit
и User.browse
в появившемся диалоговом окне и нажмите OK.
Нажмите на кнопку Debug () на главной панели инструментов.
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
:
Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Снова откройте экран редактирования пользователя и убедитесь, что вместо стандартных CRUD-кнопок отображается кнопка Generate:
Создание и сохранение экземпляров UserStep
Давайте реализуем логику генерации экземпляров UserStep
.
Добавьте следующие поля в контроллер UserEdit
:
public class UserEdit extends StandardEditor<User> {
@Autowired
private DataManager dataManager;
@Autowired
private DataContext dataContext;
@Autowired
private CollectionPropertyContainer<UserStep> stepsDc;
Вы можете инжектировать компоненты экрана и бины Spring с помощью кнопки Inject на панели действий: |
Добавьте логику создания и сохранения объектов 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
:
В таблице отображается атрибут коллекции steps
сущности User
, поэтому вы можете ввести порядок на уровне модели данных.
Откройте сущность User
, выберите атрибут steps
и введите sortValue
в поле Order by:
Если вы переключитесь на вкладку Text, вы сможете увидеть аннотацию @OrderBy
у атрибута steps
:
@OrderBy("sortValue")
@Composition
@OneToMany(mappedBy = "user")
private List<UserStep> steps;
Теперь, когда вы загружаете сущность User
, его коллекция steps
будет отсортирована по атрибуту UserStep.sortValue
.
Если ваше приложение запущено, перезапустите его.
Откройте экран редактирования пользователя. Теперь порядок пользовательских шагов правильный:
Перестановка колонок таблицы
В настоящее время таблица пользовательских шагов не очень информативна. Давайте удалим колонки Version
и Sort value
и добавим колонку, показывающую название шага.
Удалить колонку просто: выберите ее на панели иерархии Jmix UI и нажмите Delete или удалите элемент непосредственно из XML.
Чтобы добавить колонку, выберите элемент columns
на панели иерархии Jmix UI и нажмите Add → Column на панели инспектора Jmix UI. Появится диалоговое окно Add Column:
Как вы можете видеть, это не позволяет вам добавить название шага. Это связано с тем, что атрибут step
является ссылкой, и вы не определили надлежащий фетч-план для его загрузки.
Выберите контейнер данных userDc
на панели иерархии Jmix UI и нажмите кнопку Edit () либо в свойстве fetchPlan
на панели инспектора Jmix UI, либо в маркере строки редактора XML:
В окне Edit Fetch Plan выберите атрибут steps
→ step
и нажмите на кнопку OK:
Вложенный атрибут будет добавлен в фетч-план в редакторе 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 и нажмите Add → Column на панели инспектора Jmix UI. Диалоговое окно Add Column теперь содержит связанную сущность Step
и ее атрибуты:
Выберите step
→ name
и нажмите на кнопку 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
на панели иерархии Jmix UI и нажмите Add → Column на панели инспекторе Jmix UI. Появится диалоговое окно Add Column:
Выберите New Custom Column и нажмите на кнопку OK.
В диалоговом окне Additional Settings for Custom Column введите completed
в поле Custom column id и установите флажок Create generator:
Нажмите на кнопку OK.
Studio добавит колонку completed
в таблицу XML:
и метод обработчика для контроллера UserEdit
:
Обратите внимание на маркеры строк слева: они позволяют вам переключаться между определением колонки в 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 изменится соответствующим образом:
Изменения в экземплярах UserStep
будут сохранены в базе данных, когда вы нажмете OK на экране. За это отвечает объект экрана DataContext
: он отслеживает изменения во всех сущностях и сохраняет в базе данных измененные экземпляры.
Реагирование на изменения
Когда пользователь создает шаги, отмечает завершенный шаг или удаляет шаг, поле Onboarding status
должно соответствующим образом измениться.
Давайте реализуем реакцию на изменения коллекции пользовательских шагов.
Откройте контроллер UserEdit
и нажмите Generate Handler на верхней панели действий. Сверните все элементы, затем выберите элементы ItemPropertyChangeEvent
и CollectionChangeEvent
в Data containers handlers → stepsDc
:
Нажмите на кнопку 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
.
Резюме
В этом разделе вы реализовали две функции:
-
Возможность указать отдел для пользователя.
-
Генерация и управление шагами онбординга для пользователя.
Вы узнали, что:
-
Ссылочные атрибуты должны быть добавлены в фетч-план экрана, чтобы избежать проблемы 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 можно использовать для реагирования на изменения в объектах, расположенных в контейнерах данных.