DataContext
Интерфейс DataContext
позволяет отслеживать изменения в сущностях, загружаемых на уровень UI. Отслеживаемые сущности помечаются как "грязные" при любом изменении их атрибутов, и DataContext
сохраняет грязные экземпляры при вызове его метода commit()
.
Внутри DataContext
сущность с некоторым идентификатором будет представлена как единственный объект, вне зависимости от того, где и сколько раз она использована в графах других объектов.
Чтобы сущность отслеживалась, ее необходимо поместить в DataContext
с помощью метода merge()
. Если контекст не содержит экземпляра сущности с таким же идентификатором, то контекст создает новый экземпляр и копирует в него состояние переданного. Если контекст уже содержит экземпляр сущности с таким же идентификатором, он копирует в имеющегося состояние переданного и возвращает. Данный механизм позволяет всегда иметь в контексте не более одного экземпляра сущности с конкретным идентификатором.
При помещении сущности в контекст методом merge()
весь граф объектов с корнем в данной сущности также помещается в контекст. То есть все связанные сущности, включая коллекции, становятся отслеживаемыми.
Главный принцип использования метода merge() заключается в том, чтобы продолжать работать с возвращенным экземпляром, забывая про переданный. В большинстве случаев возвращенный экземпляр будет другим. Единственное исключение – если в merge() передан экземпляр объекта, ранее возвращенный другим вызовом merge() или find() этого же контекста.
|
Пример помещения сущности в DataContext
:
@Autowired
private DataContext dataContext;
@Autowired
private DataManager dataManager;
@Autowired
private CollectionContainer<Department> departmentsDc;
private void loadDepartment(Id<Department> departmentId) {
Department department = dataManager.load(departmentId).one();
Department trackedDepartment = dataContext.merge(department);
departmentsDc.getMutableItems().add(trackedDepartment);
}
Для одного экрана и всех его вложенных фрагментов может существовать только один экземпляр DataContext
. Он создается автоматически, если в XML-дескрипторе экрана существует элемент <data>
.
Элемент <data>
может содержать атрибут readOnly="true"
, в этом случае будет использована специальная "no-op"-реализация, в которой не будут отслеживаться изменения в сущностях и, следовательно, улучшится быстродействие. Экраны просмотра списков, автоматически создаваемые в Studio, по умолчанию имеют read-only data context, поэтому если вам нужно отслеживать изменения и сохранять грязные сущности в браузере, удалите XML-атрибут readOnly="true"
.
Если связанная сущность не включена в fetch plan экрана, а вместо этого загружается лениво, она не помещается в DataContext экрана и поэтому ее изменения не отслеживаются. Убедитесь что все сущности, редактируемые в экране загружаются жадно путем включения их в фетч-план.
|
Получение DataContext
-
DataContext
экрана можно получить в его контроллере используя инжектирование:@Autowired private DataContext dataContext;
-
Если имеется ссылка на некоторый экран, то получить его
DataContext
можно с помощью классаUiControllerUtils
:private void sampleMethod(Screen sampleScreen) { DataContext dataContext = UiControllerUtils.getScreenData(sampleScreen).getDataContext(); // ... }
-
UI-компонент может получить
DataContext
текущего экрана следующим образом:DataContext dataContext = UiControllerUtils.getScreenData(getFrame().getFrameOwner()).getDataContext();
Родительский DataContext
Сущности DataContext
могут образовывать отношения предок-потомок. Если у экземпляра DataContext
есть родительский контекст, он будет сохранять измененные сущности в своего предка вместо того, чтобы сразу отправлять их в хранилище данных. Эта особенность позволяет редактировать композитные сущности, где дочерние сущности должны сохраняться только вместе с родительской. Если атрибут сущности снабжен аннотацией @Composition
, фреймворк автоматически установит родительский контекст для экрана редактирования этого атрибута, чтобы измененная сущность атрибута могла быть сохранена только вместе с основной сущностью.
Подобное поведение можно легко настроить вручную для любой сущности или экрана.
Если вы программно открываете экран редактирования сущности, который должен сохранять изменения в data context текущего экрана, используйте метод withParentDataContext()
builder’а:
@Autowired
private ScreenBuilders screenBuilders;
@Autowired
private DataContext dataContext;
private void editScreenWithCurrentDataContextAsParent() {
PersonEdit personEdit = screenBuilders.editor(Person.class, this)
.withScreenClass(PersonEdit.class)
.withParentDataContext(dataContext)
.build();
personEdit.show();
}
Если вы открываете простой экран с помощью бина Screens
, определите в нем сеттер, принимающий data context родительского экрана:
public class SmplScreen extends Screen {
@Autowired
private DataContext dataContext;
public void setParentDataContext(DataContext parentDataContext) {
dataContext.setParent(parentDataContext);
}
}
И используйте после создания экрана:
@Autowired
private DataContext dataContext;
@Autowired
private Screens screens;
private void openSmplScreenWithCurrentDataContextAsParent() {
SmplScreen smplScreen = screens.create(SmplScreen.class);
smplScreen.setParentDataContext(dataContext);
smplScreen.show();
}
Убедитесь, что для родительского data context не задан атрибут readOnly="true" . В противном случае при попытке использовать его как предка другого контекста будет выброшено исключение.
|
События и слушатели
В этом разделе описываются события жизненного цикла DataContext, на которые можно подписаться в контроллерах экрана.
Чтобы сгенерировать заглушку слушателя в Jmix Studio, выберите элемент data в XML-дескрипторе экрана или на панели Component Hierarchy и используйте вкладку Handlers панели Component Inspector. В качестве альтернативы вы можете воспользоваться кнопкой Generate Handler на верхней панели контроллера экрана. |
ChangeEvent
Это событие отправляется, когда DataContext
обнаруживает изменения в отслеживаемой сущности, в контекст помещается новый экземпляр или при удалении сущности.
@Subscribe(target = Target.DATA_CONTEXT)
public void onChange(DataContext.ChangeEvent event) {
log.debug("Changed entity: " + event.getEntity());
indicatorLabel.setValue("Changed");
}
PostCommitEvent
Это событие отправляется после коммита изменений.
Из слушателя этого события можно получить коллекцию закоммиченных сущностей, возвращенных из DataManager
или настраиваемого commitDelegate. Эти сущности уже помещены в DataContext
. Например:
@Subscribe(target = Target.DATA_CONTEXT)
public void onPostCommit(DataContext.PostCommitEvent event) {
log.debug("Committed: " + event.getCommittedInstances());
}
PreCommitEvent
Это событие отправляется перед коммитом изменений.
В слушателе этого события можно добавлять произвольные экземпляры сущностей в закоммиченные коллекции, возвращаемые методами getModifiedInstances()
и getRemovedInstances()
, например:
@Subscribe(target = Target.DATA_CONTEXT)
public void onPreCommit(DataContext.PreCommitEvent event) {
event.getModifiedInstances().add(user);
}
Вы также можете предотвратить коммит, используя метод события preventCommit()
, например:
@Subscribe(target = Target.DATA_CONTEXT)
public void onPreCommit(DataContext.PreCommitEvent event) {
if (checkSomeCondition()) {
event.preventCommit();
}
}
CommitDelegate
По умолчанию DataContext
сохраняет измененные и удаленные объекты с помощью метода DataManager.save(SaveContext). Слушатель commitDelegate
позволяет настраивать логику сохранения данных, что особенно полезно при работе с DTO сущностями. Например, вы можете сохранить измененные объекты с помощью специальной службы (service):
@Autowired
private SampleService service;
@Install(target = Target.DATA_CONTEXT)
private Set<Object> commitDelegate(SaveContext saveContext) {
return service.saveEntities(
saveContext.getEntitiesToSave(),
saveContext.getEntitiesToRemove());
}