3.5.1.3. Opening Screens

A screen can be opened from the main menu, by navigating to a URL 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. 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.

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.

The most common case is when 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. 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();
}

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.

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: its getCloseAction() method returns an object with the CloseAction interface. The FrameOwner interface implemented by screen controllers contains a few constants defining CloseAction implementations used by the framework. In the application, you can use these constants or create your own implementations.

Consider a simple custom screen:

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")
    private void onOkBtnClick(Button.ClickEvent event) {
        result = "Done";
        close(WINDOW_COMMIT_AND_CLOSE_ACTION); (1)
    }

    @Subscribe("cancelBtn")
    private void onCancelBtnClick(Button.ClickEvent event) {
        closeWithDefaultAction(); (2)
    }
}
1 - on "OK" button click, set some result state and close the screen with standard WINDOW_COMMIT_AND_CLOSE_ACTION action.
2 - on "Cancel" button click, close the with a default action.

Now in the AfterCloseEvent listener we can analyze how the screen was closed, 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.getCloseAction().equals(WINDOW_COMMIT_AND_CLOSE_ACTION)) {
                        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:

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

private void openOtherScreen() {
    Screen otherScreen = screens.create("demo_OtherScreen", 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.