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

  1. DataContext экрана можно получить в его контроллере используя инжектирование:

    @Autowired
    private DataContext dataContext;
  2. Если имеется ссылка на некоторый экран, то получить его DataContext можно с помощью класса UiControllerUtils:

    private void sampleMethod(Screen sampleScreen) {
        DataContext dataContext = UiControllerUtils.getScreenData(sampleScreen).getDataContext();
        // ...
    }
  3. 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());
}