3.5.1.3. Opening Screens

A screen can be opened from the main menu, by navigating to a URL, by a standard action (when working with browse and edit entity screens), or programmatically from another screen. In this section, we explain how to open screens programmatically.



Using the Screens interface

The Screens interface allows you to create and show screens of any type.

Suppose we have a screen to show a message with some special formatting:

Screen controller
@UiController("demo_FancyMessageScreen")
@UiDescriptor("fancy-message-screen.xml")
@DialogMode(forceDialog = true, width = "300px")
public class FancyMessageScreen extends Screen {

    @Inject
    private Label<String> messageLabel;

    public void setFancyMessage(String message) { (1)
        messageLabel.setValue(message);
    }

    @Subscribe("closeBtn")
    protected void onCloseBtnClick(Button.ClickEvent event) {
        closeWithDefaultAction();
    }
}
1 - a screen parameter
Screen descriptor
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Fancy Message">
    <layout>
        <label id="messageLabel" value="A message" stylename="h1"/>
        <button id="closeBtn" caption="Close"/>
    </layout>
</window>

Then we can create and open it from another screen as follows:

@Inject
private Screens screens;

private void showFancyMessage(String message) {
    FancyMessageScreen screen = screens.create(FancyMessageScreen.class);
    screen.setFancyMessage(message);
    screens.show(screen);
}

Notice how we create the screen instance, provide a parameter for it and then show the screen.

If the screen does not require any parameters from the caller code, you can create and open it in one line:

@Inject
private Screens screens;

private void showDefaultFancyMessage() {
    screens.create(FancyMessageScreen.class).show();
}

Screens is not a Spring bean, so you can only inject it to screen controllers or obtain using ComponentsHelper.getScreenContext(component).getScreens() static method.

Using the ScreenBuilders bean

The ScreenBuilders bean allows you to open all kinds of screens with various parameters.

See Initial Entity Values guide to learn how to use external initialization via ScreenBuilders.

Below is an example of using it for opening a screen and executing some code after the screen is closed (see more details here):

@Inject
private ScreenBuilders screenBuilders;
@Inject
private Notifications notifications;

private void openOtherScreen() {
    screenBuilders.screen(this)
            .withScreenClass(OtherScreen.class)
            .withAfterCloseListener(e -> {
                notifications.create().withCaption("Closed").show();
            })
            .build()
            .show();
}

Next we’ll consider working with editor and lookup screens. Note that in most cases you open such screens using standard actions (such as CreateAction or LookupAction), so you don’t have to use the ScreenBuilders API directly. However, the examples below can be useful if you don’t use standard actions but open a screen from a BaseAction or Button handler.

Example of opening a default editor for the Customer entity instance:

@Inject
private ScreenBuilders screenBuilders;

private void editSelectedEntity(Customer entity) {
    screenBuilders.editor(Customer.class, this)
            .editEntity(entity)
            .build()
            .show();
}

In this case, the editor will update the entity, but the caller screen will not receive the updated instance.

Often you need to edit an entity displayed by some Table or DataGrid component. Then you should use the following form of invocation, which is more concise and automatically updates the table:

@Inject
private GroupTable<Customer> customersTable;
@Inject
private ScreenBuilders screenBuilders;

private void editSelectedEntity() {
    screenBuilders.editor(customersTable).build().show();
}

In order to create a new entity instance and open the editor screen for it, just call the newEntity() method on the builder:

@Inject
private GroupTable<Customer> customersTable;
@Inject
private ScreenBuilders screenBuilders;

private void createNewEntity() {
    screenBuilders.editor(customersTable)
            .newEntity()
            .build()
            .show();
}

The default editor screen is determined by the following procedure:

  1. If an editor screen annotated with @PrimaryEditorScreen exists, it is used.

  2. Otherwise, an editor screen with <entity_name>.edit id is used (for example, sales_Customer.edit).

The builder provides a lot of methods to set optional parameters of the opened screen. For example, the following code creates an entity first initializing the new instance, in a particular editor opened as a dialog:

@Inject
private GroupTable<Customer> customersTable;
@Inject
private ScreenBuilders screenBuilders;

private void editSelectedEntity() {
    screenBuilders.editor(customersTable).build().show();
}

private void createNewEntity() {
    screenBuilders.editor(customersTable)
            .newEntity()
            .withInitializer(customer -> {          // lambda to initialize new instance
                customer.setName("New customer");
            })
            .withScreenClass(CustomerEdit.class)    // specific editor screen
            .withLaunchMode(OpenMode.DIALOG)        // open as modal dialog
            .build()
            .show();
}

Entity lookup screens can also be opened with various parameters.

Below is an example of opening a default lookup screen of the User entity:

@Inject
private TextField<String> userField;
@Inject
private ScreenBuilders screenBuilders;

private void lookupUser() {
    screenBuilders.lookup(User.class, this)
            .withSelectHandler(users -> {
                User user = users.iterator().next();
                userField.setValue(user.getName());
            })
            .build()
            .show();
}

If you need to set the looked up entity to a field, use the more concise form:

@Inject
private PickerField<User> userPickerField;
@Inject
private ScreenBuilders screenBuilders;

private void lookupUser() {
    screenBuilders.lookup(User.class, this)
            .withField(userPickerField)     // set result to the field
            .build()
            .show();
}

The default lookup screen is determined by the following procedure:

  1. If a lookup screen annotated with @PrimaryLookupScreen exists, it is used.

  2. Otherwise, if a screen with <entity_name>.lookup id exists, it is used (for example, sales_Customer.lookup).

  3. Otherwise, a screen with <entity_name>.browse id is used (for example, sales_Customer.browse).

As with edit screens, use the builder methods to set optional parameters of the opened screen. For example, the following code looks up the User entity using a particular lookup screen opened as a dialog:

@Inject
private TextField<String> userField;
@Inject
private ScreenBuilders screenBuilders;

private void lookupUser() {
    screenBuilders.lookup(User.class, this)
            .withScreenId("sec$User.browse")          // specific lookup screen
            .withLaunchMode(OpenMode.DIALOG)        // open as modal dialog
            .withSelectHandler(users -> {
                User user = users.iterator().next();
                userField.setValue(user.getName());
            })
            .build()
            .show();
}
Passing parameters to screens

The recommended way of passing parameters to an opened screen is to use public setters of the screen controller, as demonstrated in the example above.

With this approach, you can pass parameters to screens of any type, including entity edit and lookup screens opened using ScreenBuilders or from the main menu. The invocation of the same FancyMessageScreen using ScreenBuilders with passing the parameter looks as follows:

@Inject
private ScreenBuilders screenBuilders;

private void showFancyMessage(String message) {
    FancyMessageScreen screen = screenBuilders.screen(this)
            .withScreenClass(FancyMessageScreen.class)
            .build();
    screen.setFancyMessage(message);
    screen.show();
}

If you are opening a screen using a standard action such as CreateAction, use its screenConfigurer handler to pass parameters via screen public setters.

Another way is to define a special class for parameters and pass its instance to the standard withOptions() method of the screen builder. The parameters class must implement the ScreenOptions marker interface. For example:

import com.haulmont.cuba.gui.screen.ScreenOptions;

public class FancyMessageOptions implements ScreenOptions {

    private String message;

    public FancyMessageOptions(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

In the opened FancyMessageScreen screen, the options can be obtained in InitEvent and AfterInitEvent handlers:

@Subscribe
private void onInit(InitEvent event) {
    ScreenOptions options = event.getOptions();
    if (options instanceof FancyMessageOptions) {
        String message = ((FancyMessageOptions) options).getMessage();
        messageLabel.setValue(message);
    }
}

The invocation of the FancyMessageScreen screen using ScreenBuilders with passing ScreenOptions looks as follows:

@Inject
private ScreenBuilders screenBuilders;

private void showFancyMessage(String message) {
    screenBuilders.screen(this)
            .withScreenClass(FancyMessageScreen.class)
            .withOptions(new FancyMessageOptions(message))
            .build()
            .show();
}

As you can see, this approach requires type casting in the controller receiving the parameters, so use it wisely and prefer the type-safe setters approach explained above.

If you are opening a screen using a standard action such as CreateAction, use its screenOptionsSupplier handler to create and initialize the required ScreenOptions object.

Usage of the ScreenOptions object is the only way to get parameters if the screen is opened from a screen based on the legacy API. In this case, the options object is of type MapScreenOptions and you can handle it in the opened screen as follows:

@Subscribe
private void onInit(InitEvent event) {
    ScreenOptions options = event.getOptions();
    if (options instanceof MapScreenOptions) {
        String message = (String) ((MapScreenOptions) options).getParams().get("message");
        messageLabel.setValue(message);
    }
}
Executing code after close and returning values

Each screen sends AfterCloseEvent when it closes. You can add a listener to a screen to be notified when the screen is closed, for example:

@Inject
private Screens screens;
@Inject
private Notifications notifications;

private void openOtherScreen() {
    OtherScreen otherScreen = screens.create(OtherScreen.class);
    otherScreen.addAfterCloseListener(afterCloseEvent -> {
        notifications.create().withCaption("Closed " + afterCloseEvent.getScreen()).show();
    });
    otherScreen.show();
}

When using ScreenBuilders, the listener can be provided in the withAfterCloseListener() method:

@Inject
private ScreenBuilders screenBuilders;
@Inject
private Notifications notifications;

private void openOtherScreen() {
    screenBuilders.screen(this)
            .withScreenClass(OtherScreen.class)
            .withAfterCloseListener(afterCloseEvent -> {
                notifications.create().withCaption("Closed " + afterCloseEvent.getScreen()).show();
            })
            .build()
            .show();
}

The event object provides an information about how the screen was closed. This information can be obtained in two ways: by testing whether the screen was closed with one of standard outcomes defined by the StandardOutcome enum, or by getting the CloseAction object. The former approach is simpler while the latter is much more flexible.

Let’s consider the first approach: to close a screen with a standard outcome and test for it in the calling code.

The following screen is to be invoked:

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.screen.*;

@UiController("demo_OtherScreen")
@UiDescriptor("other-screen.xml")
public class OtherScreen extends Screen {

    private String result;

    public String getResult() {
        return result;
    }

    @Subscribe("okBtn")
    public void onOkBtnClick(Button.ClickEvent event) {
        result = "Done";
        close(StandardOutcome.COMMIT); (1)
    }

    @Subscribe("cancelBtn")
    public void onCancelBtnClick(Button.ClickEvent event) {
        close(StandardOutcome.CLOSE); (2)
    }
}
1 - on "OK" button click, set some result state and close the screen with StandardOutcome.COMMIT enum value.
2 - on "Cancel" button click, close the with StandardOutcome.CLOSE.

In the AfterCloseEvent listener, you can check how the screen was closed using the closedWith() method of the event, and read the result value if needed:

@Inject
private ScreenBuilders screenBuilders;
@Inject
private Notifications notifications;

private void openOtherScreen() {
        screenBuilders.screen(this)
                .withScreenClass(OtherScreen.class)
                .withAfterCloseListener(afterCloseEvent -> {
                    OtherScreen otherScreen = afterCloseEvent.getScreen();
                    if (afterCloseEvent.closedWith(StandardOutcome.COMMIT)) {
                        String result = otherScreen.getResult();
                        notifications.create().withCaption("Result: " + result).show();
                    }
                })
                .build()
                .show();
}

Another way of returning values from screens is using custom CloseAction implementations. Let’s rewrite the above example to use the following action class:

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.screen.StandardCloseAction;

public class MyCloseAction extends StandardCloseAction {

    private String result;

    public MyCloseAction(String result) {
        super("myCloseAction");
        this.result = result;
    }

    public String getResult() {
        return result;
    }
}

Then we can use this action when closing the screen:

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.screen.*;

@UiController("demo_OtherScreen2")
@UiDescriptor("other-screen.xml")
public class OtherScreen2 extends Screen {

    @Subscribe("okBtn")
    public void onOkBtnClick(Button.ClickEvent event) {
        close(new MyCloseAction("Done")); (1)
    }

    @Subscribe("cancelBtn")
    public void onCancelBtnClick(Button.ClickEvent event) {
        closeWithDefaultAction(); (2)
    }
}
1 - on "OK" button click, create the custom close action and set the result value in it.
2 - on "Cancel" button click, close with a default action provided by the framework.

In the AfterCloseEvent listener, you can get the CloseAction from the event and read the result value:

@Inject
private Screens screens;
@Inject
private Notifications notifications;

private void openOtherScreen() {
    Screen otherScreen = screens.create("demo_OtherScreen2", OpenMode.THIS_TAB);
    otherScreen.addAfterCloseListener(afterCloseEvent -> {
        CloseAction closeAction = afterCloseEvent.getCloseAction();
        if (closeAction instanceof MyCloseAction) {
            String result = ((MyCloseAction) closeAction).getResult();
            notifications.create().withCaption("Result: " + result).show();
        }
    });
    otherScreen.show();
}

As you can see, when values are returned through a custom CloseAction, the caller doesn’t have to know the opened screen class because it doesn’t invoke methods of the concrete screen controller. So the screen can be created by its string id.

Of course, the same approach for returning values through close actions can be used when opening screens using ScreenBuilders.