DataContext

DataContext is an interface for tracking changes in entities loaded to the UI layer. Tracked entities are marked as "dirty" on any modification of their attributes, and DataContext saves dirty entities when its commit() method is invoked.

Within DataContext, an entity with the given identifier is represented by a single object instance, no matter where and how many times it is used in object graphs.

In order to be tracked, an entity must be put into DataContext using its merge() method. If the context does not contain the entity with the same id, the context creates a new instance, copies the state of the passed instance to the new one and returns it. If the context already contains an instance with the same id, it copies the state of the passed instance to the existing one and returns it. This mechanism allows the context to always have only one instance of an entity with a particular identifier.

When you merge an entity, the whole object graph with the root in this entity will be merged. That is all referenced entities (including collections) will become tracked.

The main rule of using the merge() method is to continue working with the returned instance and discarding the passed one. In most cases, the returned object instance will be different. The only exception is when you pass to merge() an instance that was earlier returned from another invocation of merge() or find() of the same context.

An example of merging an entity into 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);
}

A single instance of DataContext exists for a given screen and all its nested fragments. It is created if the <data> element exists in the screen XML descriptor.

The <data> element can have readOnly="true" attribute, in that case, a special "no-op" implementation is used that actually doesn’t track entities and hence doesn’t affect performance. By default, entity browsers scaffolded by Studio have the read-only data context, so if you need to track changes and commit dirty entities in a browser, remove the readOnly="true" XML attribute.

If a referenced entity is not included in the fetch plan of the screen but loaded by lazy loading, it is not merged into the screen’s DataContext and hence not tracked for changes. Make sure all entities edited on the screen are loaded eagerly by including references to them in the fetch plan.

Obtaining DataContext

  1. DataContext of a screen can be obtained in its controller using injection:

    @Autowired
    private DataContext dataContext;
  2. If you have a reference to a screen, you can get its DataContext using the UiControllerUtils class:

    private void sampleMethod(Screen sampleScreen) {
        DataContext dataContext = UiControllerUtils.getScreenData(sampleScreen).getDataContext();
        // ...
    }
  3. A UI component can obtain DataContext of the current screen as follows:

    DataContext dataContext = UiControllerUtils.getScreenData(getFrame().getFrameOwner()).getDataContext();

Parent DataContext

DataContext instances can form parent-child relationships. If a DataContext instance has parent context, it commits changed entities to the parent instead of saving them to the data store. This feature enables editing compositions when detail entities are saved only together with the master entity. If an entity attribute is annotated with @Composition, the framework automatically sets parent context in the attribute editor screen, so the changed attribute entity will be saved to the data context of the master entity.

You can easily provide the same behavior for any entities and screens.

If you open an edit screen that should commit data to the current screen’s data context, use the withParentDataContext() method of the 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();
}

If you open a simple screen using the Screens bean, provide a setter method accepting the parent data context:

public class SmplScreen extends Screen {

    @Autowired
    private DataContext dataContext;

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

}

And use it after creating the screen:

@Autowired
private DataContext dataContext;

@Autowired
private Screens screens;

private void openSmplScreenWithCurrentDataContextAsParent() {
    SmplScreen smplScreen = screens.create(SmplScreen.class);
    smplScreen.setParentDataContext(dataContext);
    smplScreen.show();
}
Make sure that the parent data context is not defined with readOnly="true" attribute. Otherwise, you will get an exception when trying to use it as a parent for another context.

Events and Handlers

This section describes the DataContext lifecycle events that can be handled in screen controllers.

To generate a handler stub in Jmix Studio, select the data element in the screen descriptor XML or in the Component Hierarchy panel and use the Handlers tab of the Component Inspector panel.

Alternatively, you can use the Generate Handler button in the top panel of the screen controller.

ChangeEvent

This event is sent when the context detects changes in a tracked entity, a new instance is merged or an entity is removed.

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

PostCommitEvent

This event is sent after committing changes. In this event listener, you can get the collection of committed entities returned from DataManager or a custom commit delegate. These entities are already merged into the DataContext. For example:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPostCommit(DataContext.PostCommitEvent event) {
    log.debug("Committed: " + event.getCommittedInstances());
}

PreCommitEvent

This event is sent before committing changes. In this event listener, you can add arbitrary entity instances to the committed collections returned by getModifiedInstances() and getRemovedInstances() methods, for example:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPreCommit(DataContext.PreCommitEvent event) {
    event.getModifiedInstances().add(user);
}

You can also prevent the commit using the preventCommit() method of the event, for example:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPreCommit(DataContext.PreCommitEvent event) {
    if (checkSomeCondition()) {
        event.preventCommit();
    }
}

CommitDelegate

By default, DataContext commits changed and removed entities using the DataManager.save(SaveContext) method. The commitDelegate handler allows you to customize the logic of saving data, which is especially useful when working with DTO entities. For example, you can save the changed entities with a custom service:

@Autowired
private SampleService service;

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