3.5.3.3. DataContext

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

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

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

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

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

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

@Inject
private DataContext dataContext;

private void loadCustomer(Id<Customer, UUID> customerId) {
    Customer customer = dataManager.load(customerId).one();
    Customer trackedCustomer = dataContext.merge(customer);
    customersDc.getMutableItems().add(trackedCustomer);
}

Для одного экрана и всех его вложенных фрагментов может существовать только один экземпляр DataContext. Он создаётся автоматически, если в XML-дескрипторе экрана существует элемент <data>.

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

Получение DataContext

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

@Inject
private DataContext dataContext;

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

DataContext dataContext = UiControllerUtils.getScreenData(screenOrFrame).getDataContext();

UI-компонента может получить DataContext текущего экрана следующим образом:

DataContext dataContext = UiControllerUtils.getScreenData(getFrame().getFrameOwner()).getDataContext();
Родительский DataContext

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

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

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

@Inject
private ScreenBuilders screenBuilders;
@Inject
private DataContext dataContext;

private void editFooWithCurrentDataContextAsParent() {
    FooEdit fooEdit = screenBuilders.editor(Foo.class, this)
            .withScreenClass(FooEdit.class)
            .withParentDataContext(dataContext)
            .build();
    fooEdit.show();
}

Если вы открываете простой экран с помощью бина Screens, определите в нём сеттер, принимающий data context родительского экрана:

public class FooScreen extends Screen {

    @Inject
    private DataContext dataContext;

    public void setParentDataContext(DataContext parentDataContext) {
        dataContext.setParent(parentDataContext);
    }
}

Этот метод вы сможете использовать при создании экрана:

@Inject
private Screens screens;
@Inject
private DataContext dataContext;

private void openFooScreenWithCurrentDataContextAsParent() {
    FooScreen fooScreen = screens.create(FooScreen.class);
    fooScreen.setParentDataContext(dataContext);
    fooScreen.show();
}

Убедитесь, что для родительского data context не задан атрибут readOnly="true". В противном случае при попытке использовать его как предка другого контекста будет выброшено исключение.