DataContext

Интерфейс DataContext позволяет отслеживать изменения в сущностях, загружаемых на уровень UI. Отслеживаемые сущности помечаются как "грязные" при любом изменении значений их атрибутов, и DataContext сохраняет грязные экземпляры при вызове его метода save().

Внутри DataContext сущность с некоторым идентификатором будет представлена единственным объектом, вне зависимости от того, где и сколько раз она использована в графах сущностей, находящихся в данном контексте.

Чтобы сущность отслеживалась, ее необходимо поместить в DataContext с помощью метода merge(). Если контекст не содержит экземпляра сущности с таким же идентификатором, то контекст создает новый экземпляр и копирует в него состояние переданного. Если контекст уже содержит экземпляр сущности с таким же идентификатором, он копирует в имеющийся экземпляр состояние переданного и возвращает имеющийся экземпляр. Данный механизм позволяет всегда иметь в контексте не более одного экземпляра сущности с конкретным идентификатором.

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

Главный принцип использования метода merge() заключается в том, чтобы продолжать работать с возвращенным из метода экземпляром, забывая про переданный. В большинстве случаев возвращенный экземпляр будет другим. Единственное исключение – если в merge() передан экземпляр объекта, ранее возвращенный другим вызовом merge() или find() этого же контекста.

Пример помещения сущности в DataContext:

@ViewComponent
private CollectionContainer<Department> departmentsDc;

@Autowired
private DataManager dataManager;

@ViewComponent
private DataContext dataContext;

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"-реализация интерфейса DataContext, в которой не будут отслеживаться изменения в сущностях и, тем самым, не оказывает влияние на производительность. Экраны просмотра списков, автоматически создаваемые в Studio, по умолчанию имеют read-only data context, поэтому если вам нужно отслеживать изменения и сохранять грязные сущности в экране списка, удалите XML-атрибут readOnly="true".

Если связанная сущность не включена в фетч-план экрана, а вместо этого загружается лениво, она не помещается в DataContext экрана и поэтому ее изменения не отслеживаются. Убедитесь что все сущности, редактируемые в экране загружаются жадно путем включения их в фетч-план.

Получение DataContext

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

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

    private void sampleMethod(View sampleView) {
        DataContext dataContext = ViewControllerUtils.getViewData(sampleView).getDataContext();
        // ...
    }

Родительский DataContext

Сущности DataContext могут образовывать отношения предок-потомок. Если у экземпляра DataContext есть родительский контекст, он будет сохранять измененные сущности в своего предка вместо того, чтобы сразу отправлять их в хранилище данных. Эта особенность позволяет редактировать композитные сущности (агрегаты), когда дочерние сущности должны сохраняться только вместе с родительской. Если атрибут сущности снабжен аннотацией @Composition, фреймворк автоматически установит родительский контекст для экрана деталей этого атрибута, чтобы измененная сущность атрибута сохранялась в контекст сущности-владельца.

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

Если вы программно открываете экран деталей сущности, который должен сохранять изменения в data context текущего экрана, используйте метод withParentDataContext():

private void detailViewWithCurrentDataContextAsParent() {
    DialogWindow<DepartmentDetailView> dialogWindow = dialogWindows.detail(this, Department.class)
            .withViewClass(DepartmentDetailView.class)
            .withParentDataContext(dataContext)
            .build();
    dialogWindow.open();
}
Убедитесь что родительский контекст определен без атрибута readOnly="true". Такой контекст вызовет исключение при попытке использовать его в качестве родительского для другого контекста.

События и обработчики

В этом разделе описываются события жизненного цикла DataContext, на которые можно подписаться в контроллерах экрана.

Чтобы сгенерировать заглушку обработчика в Jmix Studio, выберите элемент данных в XML-коде дескриптора экрана или на панели структуры Jmix UI и используйте вкладку Handlers панели инспектора Jmix UI.

В качестве альтернативы вы можете воспользоваться кнопкой Generate Handler на верхней панели контроллера экрана.

SaveDelegate

По умолчанию DataContext сохраняет измененные и удаленные объекты с помощью метода DataManager.save(SaveContext). Обработчик saveDelegate позволяет настраивать логику сохранения данных, что особенно полезно при работе с DTO сущностями. Например, вы можете сохранить измененные объекты с помощью специального сервиса:

@Autowired
private DepartmentService departmentService;

@Install(target = Target.DATA_CONTEXT)
private Set<Object> saveDelegate(final SaveContext saveContext) {
    return departmentService.saveEntities(
            saveContext.getEntitiesToSave(),
            saveContext.getEntitiesToRemove());
}

Обработчик saveDelegate должен возвращать Set сохраненных экземпляров. Если это невозможно, верните исходные экземпляры из saveContext.getEntitiesToSave() или просто пустой Set. Не возвращайте удаленные экземпляры. Возвращенные экземпляры будут помещены обратно в DataContext, и экран продолжит работу с обновленным состоянием.

ChangeEvent

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

@Subscribe(target = Target.DATA_CONTEXT)
public void onChange(final DataContext.ChangeEvent event) {
    log.debug("Changed entity: " + event.getEntity());
}

PreSaveEvent

Это событие отправляется перед сохранением изменений.

В слушателе этого события можно добавлять произвольные экземпляры сущностей в сохраняемые коллекции, возвращаемые методами getModifiedInstances() и getRemovedInstances(), например:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPreSave(final DataContext.PreSaveEvent event) {
    event.getModifiedInstances().add(department);
}

Вы также можете предотвратить сохранение, используя метод события preventCommit(), например:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPreSave2(DataContext.PreSaveEvent event) {
    if (checkSomeCondition()) {
        event.preventSave();
    }
}

PostSaveEvent

Это событие отправляется после сохранения изменений.

Из слушателя этого события можно получить коллекцию сохраненных сущностей, возвращенных из DataManager или собственного save delegate. Эти сущности уже помещены в DataContext. Например:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPostSave(final DataContext.PostSaveEvent event) {
    log.debug("Saved: " + event.getSavedInstances());
}