Cannot reproduce normal Composition DetailView behaviour with dialogWindows.detail()

Jmix version: 2.4.2
Jmix Studio plugin version: 2.4.2-243
IntelliJ IDEA 2024.3 (Community Edition)
Build #IC-243.21565.193, built on November 13, 2024
Runtime version: 21.0.5+8-b631.16 x86_64 (JCEF 122.1.9)
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
GC: G1 Young Generation, G1 Concurrent GC, G1 Old Generation
Kotlin: 243.21565.193-IJ
java 17.0.10 2024-01-16 LTS
Java™ SE Runtime Environment (build 17.0.10+11-LTS-240)
Java HotSpot™ 64-Bit Server VM (build 17.0.10+11-LTS-240, mixed mode, sharing)
Operating System: macOS 12.7.6 (21H1320)
Metal Rendering: ON
File System: Case-Sensitive Journaled HFS+ (APFS)
Browser: Safari - Version 17.6 (17618.3.11.11.7, 17618)
Database: PostgreSQL 13

Hello Jmix Team

For your information, I am not able to reproduce the normal Composition DetailView behaviour using dialogWindows.detail(). I will illustrate this problem using the attached example project:

composition.zip (2.3 MB)

In my example there are two compositions; one composition is with entities ItService and Alert, and the other is with entities DataCenter and Server; an ItService has zero or more Alerts and a DataCenter has zero or more Servers.

Example 1 (OK) - Default out-of-the-box functionality

I have created a default ItServiceListView and default ItServiceDetailView for the ItService entity, and a default AlertDetailView for the Alert entity. None of these views contain any custom code; they have not been modified after they were created by the Studio wizard.

If I create a new ItService entity from the ItServiceListView and then create one or more new Alert entities from the ItServiceDetailView, then…

  1. the newly created Alert entities are displayed in the ItServiceDetailView’s dataGrid and…
  2. the newly created Alert entities are not persisted in the database until I save the ItService entity using the ItServiceDetailView’s OK button.

This is the normal expected behaviour.

Example 2 (NOK) - Custom functionality to initialise UI components

My second composition requires custom view logic to initialise some view components, therefore, I am explicitly calling dialogWindows.detail() from the dataGrid’s event handler instead of using the default action logic in example 1 above.

I have tried the following variations with different logic and none of them work correctly; the main problem is already seen in variation 1 and it is found in all of the other variations:

Variation 1 - Using newEntity()

@Subscribe("serverDataGrid.create")
public void onServerDataGridCreate(final ActionPerformedEvent event) {

    DialogWindow<ServerDetailView> dialogWindow = dialogWindows.detail(this, Server.class)
            .withViewClass(ServerDetailView.class)
            .newEntity()
            .build();   // The new view’s onInit() is called here.

    dialogWindow.getView().setDataCenterName(dataCenterDc.getItem().getName());
    dialogWindow.getView().setServerMemorySizeOptions(null);    // null for a new entity
    dialogWindow.open();
}

If I use variation 1 above, everything is initialised correctly, but as soon as save the new Server entity with the ServerDetailView’s OK button and return to the DataCenterDetailView, I receive the following error:

PSQLException: ERROR: null value in column “data_center_id” of relation “server” violates not-null constraint
Detail: Failing row contains (9459eaef-f08c-c1ed-2c80-ca37de3e5dae, srv01, 20, null, 40).

This error indicates that the system is trying to persist my new Server entity in the database, but it cannot because the DataCenter entity is new and has not been persisted yet. However, the system should not persist the new Server at all; it should only persist this new Server entity when I explicitly persist the DataCenter entity with the OK button in the DataCenterDetailView.

Why does the system try and persist this Server entity?

Variation 2 - Manually create a new Server entity and pass it with newEntity(newServer)

@Subscribe("serverDataGrid.create")
public void onServerDataGridCreate(final ActionPerformedEvent event) {

    Server newServer = dataContext.create(Server.class);
    newServer.setDataCenter(dataCenterDc.getItem());

    DialogWindow<ServerDetailView> dialogWindow = dialogWindows.detail(this, Server.class)
            .withViewClass(ServerDetailView.class)
            .newEntity(newServer)
            .build();   // The new view’s onInit() is called here.

    dialogWindow.getView().setDataCenterName(dataCenterDc.getItem().getName());
    dialogWindow.getView().setServerMemorySizeOptions(null);    // null for a new entity
    dialogWindow.open();
}

If I use variation 2 above, everything is initialised correctly, but when I save the new Server entity with the ServerDetailView’s OK button and return to the DataCenterDetailView, then both the new Server entity and also the DataCenter entity are persisted in the database, even though I have not pressed the DataCenterDetailView’s OK button. Furthermore, the new Server entity is not displayed in the DataCenter’s dataGrid; therefore, it appears that nothing happened at all.

If I press OK in the DataCenterDetailView, then I get a “may not be null” alert notification without any other information.
If I press Cancel and then “Don’t save” in the confirmation dialog, thereby closing the DataCenterDetailView, then I see the newly created DataCenter entity in the DataCenterListView, confirming that saving the Server caused the DataCenter also to be saved.

Variation 3 - Use withAfterCloseListener with afterCloseEvent.getView().getEditedEntity() to update dataContext and dataGrid

@Subscribe("serverDataGrid.create")
public void onServerDataGridCreate(final ActionPerformedEvent event) {

    Server newServer = dataContext.create(Server.class);
    newServer.setDataCenter(dataCenterDc.getItem());

    DialogWindow<ServerDetailView> dialogWindow = dialogWindows.detail(this, Server.class)
            .withViewClass(ServerDetailView.class)
            .newEntity(newServer)
           .withAfterCloseListener(afterCloseEvent -> {
                if (afterCloseEvent.closedWith(StandardOutcome.SAVE)) {
                    Server server = afterCloseEvent.getView().getEditedEntity();
                    server = dataContext.merge(server);
                    serverDc.getMutableItems().add(server);
                    dataContext.merge(server.getDataCenter());
                } else {
                    dataContext.remove(newServer);
                }
            })
            .build();   // The new view’s onInit() is called here.

    dialogWindow.getView().setDataCenterName(dataCenterDc.getItem().getName());
    dialogWindow.getView().setServerMemorySizeOptions(null);    // null for a new entity
    dialogWindow.open();
}

If I use variation 3 above, everything is initialised correctly; when I save the new Server entity with the ServerDetailView’s OK button and return to the DataCenterDetailView, then both the new Server entity and also the DataCenter entity are persisted in the database, even though I have not pressed the DataCenterDetailView’s OK button (as with variation 2), and the DataCenter’s dataGrid is updated and displays the new Server.

But when I press the DataCenterDetailView’s OK button, I receive the following error because the DataCenter was already persisted when the new Server was saved:

Unique constraint violation occurred (PK_DATA_CENTER)

Variation 4 - Use withAfterCloseListener and reload the Server with dataManager.load() to update dataContext and dataGrid

@Subscribe("serverDataGrid.create")
public void onServerDataGridCreate(final ActionPerformedEvent event) {

    Server newServer = dataContext.create(Server.class);
    newServer.setDataCenter(dataCenterDc.getItem());

    DialogWindow<ServerDetailView> dialogWindow = dialogWindows.detail(this, Server.class)
            .withViewClass(ServerDetailView.class)
            .newEntity(newServer)
           .withAfterCloseListener(afterCloseEvent -> {
                if (afterCloseEvent.closedWith(StandardOutcome.SAVE)) {
                    Server server = dataManager.load(Server.class).id(afterCloseEvent.getView().getEditedEntity().getId()).one();
                    server = dataContext.merge(server);
                    serverDc.getMutableItems().add(server);
                    dataContext.merge(server.getDataCenter());
                } else {
                    dataContext.remove(newServer);
                }
            })
            .build();   // The new view’s onInit() is called here.

    dialogWindow.getView().setDataCenterName(dataCenterDc.getItem().getName());
    dialogWindow.getView().setServerMemorySizeOptions(null);    // null for a new entity
    dialogWindow.open();
}

If I use variation 4 above, everything is initialised correctly; when I save the new Server entity with the ServerDetailView’s OK button and return to the DataCenterDetailView, then both the new Server entity and also the DataCenter entity are persisted in the database, even though I have not pressed the DataCenterDetailView’s OK button (as with variations 2, 3), the DataCenter’s dataGrid is updated and displays the new Server, and it is possible to press the DataCenterDetailView’s OK button without receiving an alert notification. This is still not the expected Composition behaviour; saving the new Server should not persist the new Server and the new DataCenter in the database.

Conclusion: None of these variations work; the normal Composition behaviour is not respected when using dialogWindows.detail() is this manner. The new Server entity should not be persisted to the database until the DataCenter entity is explicitly persisted and the new DataCenter entity should not be persisted with the new Server, when the Server is persisted.

My application is heavily dependent upon dialogWindows.detail(), so that I can initialise multiple select and other UI components.

What do I need to do to resolve these problems?

Please note, that I also need to use dialogWindows.detail() when editing an already existing entity; therefore, the dataGrid update problem needs to be solved for this case also.

Thank you in advance for your support and feedback.

Best regards
Chris

Hi Chris,

Thank you for the detailed explanation of the problem.

However, the solution is quite simple: you need to provide the current DataContext as a parent to the nested detail view. Then the nested view will save its changed entities into the parent view’s data context instead of directly to DataManager (this is explained here in the docs).

In your project, the simplest change is to set the parent DataContext and also to build the detail view providing the dataGrid in the dialogWindows.detail() method:

@ViewComponent
private DataContext dataContext;
@Autowired
private DialogWindows dialogWindows;
@ViewComponent
private DataGrid<Server> serverDataGrid;

@Subscribe("serverDataGrid.create")
public void onServerDataGridCreate(final ActionPerformedEvent event) {
    DialogWindow<ServerDetailView> dialogWindow = dialogWindows.detail(serverDataGrid)
            .withViewClass(ServerDetailView.class)
            .newEntity()
            .withParentDataContext(dataContext)
            .build();

In this case the framework will automatically initialize the back reference from Server to DataCenter, and add the created server to the dataGrid’s data container to show it in the parent view.

If you want more control, you can do this manually as follows:

@ViewComponent
private DataContext dataContext;
@Autowired
private DialogWindows dialogWindows;
@Autowired
private DataManager dataManager;
@ViewComponent
private CollectionPropertyContainer<Server> serverDc;

@Subscribe("serverDataGrid.create")
public void onServerDataGridCreate(final ActionPerformedEvent event) {

    // create your new instance
    Server server = dataManager.create(Server.class);
    // initialize back reference
    server.setDataCenter(getEditedEntity());
    
    DialogWindow<ServerDetailView> dialogWindow = dialogWindows.detail(this, Server.class)
            .withViewClass(ServerDetailView.class)
            .newEntity(server) // provide your own new instance 
            .withParentDataContext(dataContext)
            .withAfterCloseListener(serverDetailViewAfterCloseEvent -> {
                if (serverDetailViewAfterCloseEvent.closedWith(StandardOutcome.SAVE)) {
                    // find the created entity in this data context
                    Server mergedServer = dataContext.find(server);
                    // and add it to the grid's data container
                    serverDc.getMutableItems().add(mergedServer);
                }
            })
            .build();

The same can be done for an Edit action:

@Subscribe("serverDataGrid.edit")
public void onServerDataGridEdit(final ActionPerformedEvent event) {
    Server server = serverDataGrid.getSingleSelectedItem();
    if (server != null) {
        DialogWindow<ServerDetailView> dialogWindow = dialogWindows.detail(this, Server.class)
                .withViewClass(ServerDetailView.class)
                .editEntity(server)
                .withParentDataContext(dataContext)
                .build();
        dialogWindow.open();
    }
}

Regards,
Konstantin

@krivopustov

Hi Konstantin

Thank you very much for the quick reply and the solution. I had in my mind that I had already consulted the documentation regarding this several weeks ago, and did not search for it again. My mistake! I will be more careful in the future.

Happy Holidays & Best regards
Chris