Opening Screens

You can open screens from the main screen, by a standard action when working with browse and edit entity screens or programmatically from another screen. This section describes how to open screens programmatically.

Using 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. A controller can look this way:

@UiController("sample_FancyMessageScreen")
@UiDescriptor("fancy-message-screen.xml")
public class FancyMessageScreen extends Screen {

    @Autowired
    private Label<String> messageLabel; (1)

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

    @Subscribe("closeBtn")
    protected void onCloseBtnClick(Button.ClickEvent event) { (3)
        closeWithDefaultAction();
    }
}
1 Injects Label component.
2 The method receives a String screen parameter.
3 Subscribtion on the ClickEvent.

XML descriptor:

<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://fancyMessageScreen.caption">
    <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:

@Autowired
private Screens screens;

private void showFancyScreen(String message) {
    FancyMessageScreen fancyScreen = screens.create(FancyMessageScreen.class); (1)
    fancyScreen.setFancyMessage(message); (2)
    screens.show(fancyScreen); (3)
}
1 Creates a screen instance.
2 Provides a parameter for the screen.
3 Shows the screen.

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

@Autowired
private Screens screens;

private void showDefaultFancyScreen() {
    screens.create(FancyMessageScreen.class).show();
}
Pay attention that 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 ScreenBuilders Bean

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:

@Autowired
private ScreenBuilders screenBuilders;

@Autowired
private Notifications notifications;

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

Opening Editor Screens

In most cases you can open editor screens using standard actions such as CreateAction. Let’s look at the examples when you can use the ScreenBuilders API directly to open a screen from BaseAction or Button handler.

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.

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

@Autowired
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:

@Autowired
private GroupTable<Customer> customersTable;

@Autowired
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:

@Autowired
private GroupTable<Customer> customersTable;

@Autowired
private ScreenBuilders screenBuilders;

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

ScreenBuilder 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:

@Autowired
private GroupTable<Customer> customersTable;

@Autowired
private ScreenBuilders screenBuilders;

private void editSelectedEntity(Customer entity) {
    screenBuilders.editor(Customer.class, this)
            .editEntity(entity)
            .build()
            .show();
}
private void createNewEntityWithParameter() {
    screenBuilders.editor(customersTable)
            .newEntity()
            .withInitializer(customer -> { (1)
                customer.setLevel(Level.SILVER);
            })
            .withScreenClass(CustomerEdit.class) (2)
            .withOpenMode(OpenMode.DIALOG) (3)
            .build()
            .show();
}
1 Initializes a new instance.
2 Specifies an editor screen.
3 Opens as a dialog.

Opening Lookup Screens

Let’s look at some examples of working with lookup screens. As with editor screens, you mostly open such screens using standard actions such as EntityLookupAction. The examples below shows the usage of ScreenBuilders API and can be useful if you don’t use standard actions.

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.

Entity lookup screens can also be opened with various parameters. In the example below, lookup screen of the Customer entity is opened and the name of the selected customer is written to the textField:

@Autowired
private TextField userField;

@Autowired
private ScreenBuilders screenBuilders;

private void lookupCustomer() {
    screenBuilders.lookup(Customer.class, this)
            .withSelectHandler(customers -> {
                Customer customer = customers.iterator().next();
                userField.setValue(customer.getFirstName() + " " + customer.getLastName());
            })
            .build()
            .show();
}

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

@Autowired
private EntityPicker customerEntityPicker;

@Autowired
private ScreenBuilders screenBuilders;

private void lookupCustomerSelect() {
    screenBuilders.lookup(Customer.class, this)
            .withField(customerEntityPicker)
            .build()
            .show();
}

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

@Autowired
private TextField userField;

@Autowired
private ScreenBuilders screenBuilders;

private void lookupCustomerWithParameter() {
    screenBuilders.lookup(Customer.class, this)
            .withScreenId("uiex1_Customer.browse")
            .withOpenMode(OpenMode.DIALOG)
            .withSelectHandler(users -> {
                Customer customer = users.iterator().next();
                userField.setValue(customer.getFirstName() + " " + customer.getLastName());
            })
            .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 FancyMessageScreen using ScreenBuilders with passing the parameter looks as follows:

@Autowired
private ScreenBuilders screenBuilders;

private void showFancyScreen(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:

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:

@Autowired
private Label<String> messageLabel; (1)

@Subscribe
public 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:

@Autowired
private ScreenBuilders screenBuilders;

private void showFancyScreen(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:

@Autowired
private Label<String> messageLabel;

@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:

@Autowired
private Screens screens;

@Autowired
private Notifications notifications;

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

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

@Autowired
private Screens screens;

@Autowired
private Notifications notifications;

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

The event object provides 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.

  • By getting the CloseAction object. The former approach is simpler, while the latter is much more flexible.

Let’s consider the first approach: close a screen with a standard outcome and test it in the calling code. Here is the screen that we envoke:

@UiController("sample_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 By clicking the Ok button sets some result state and close the screen with StandardOutcome.COMMIT enum value.
2 By clicking the Cancel button closes the screen 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:

@Autowired
private ScreenBuilders screenBuilders;

@Autowired
private Notifications notifications;

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

Using Custom CloseAction

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

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:

@UiController("sample_NewOtherScreen")
@UiDescriptor("new-other-screen.xml")
public class NewOtherScreen 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 By clicking the Ok button creates the custom close action and sets the result value in it.
2 By clicking the Cancel button closes 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:

@Autowired
private Screens screens;

@Autowired
private Notifications notifications;

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