Screen Mixins

Mixins enable creating features that can be reused in multiple UI screens without the need to inherit your screens from common base classes. Mixins are implemented using Java interfaces with default methods.

Mixins have the following characteristics:

  • A screen can have multiple mixins.

  • A mixin interface can subscribe to screen events.

  • A mixin can save some state in the screen if needed.

  • A mixin can obtain screen components and infrastructure beans like Dialogs, Notifications, etc.

  • In order to parameterize its behavior, a mixin can rely on screen annotations or introduce abstract methods to be implemented by the screen.

Usage of mixins is normally as simple as implementing specific interfaces in a screen controller.

A mixin can use the following classes to work with a screen and infrastructure:

  • io.jmix.ui.screen.Extensions provides static methods for saving and retrieving a state from the screen where the mixin is used, as well as access to BeanLocator which in turn allows you to get any Spring bean.

  • io.jmix.ui.screen.UiControllerUtils provides access to the screen’s UI and data components.

Examples

The examples below demonstrate how to create and use mixins.

Banner Mixin

This is a very simple mixin that shows a label on the top of the screen.

public interface HasBanner {

    @Subscribe
    default void initBanner(Screen.InitEvent event) {

        ApplicationContext applicationContext = Extensions.getApplicationContext(event.getSource()); (1)
        UiComponents uiComponents = applicationContext.getBean(UiComponents.class); //  (2)

        Label<String> banner = uiComponents.create(Label.TYPE_STRING); (3)
        banner.setStyleName(ThemeClassNames.LABEL_H2);
        banner.setValue("Hello, world!");

        event.getSource().getWindow().add(banner, 0); (4)
    }
}
1 Gets ApplicationContext.
2 Gets factory of UI components.
3 Creates Label and sets its properties.
4 Adds a label to the screen’s root UI component.

The mixin can be used in a screen as follows:

@UiController("demo_Order.edit")
@UiDescriptor("demo-order-edit.xml")
@EditedEntityContainer("orderDc")
public class DemoOrderEdit extends StandardEditor<Order> implements HasBanner {
    // ...
}

DeclarativeLoaderParameters Mixin

The next mixin helps to establish master-detail relationships between data containers. Normally, you have to subscribe to ItemChangeEvent of the master container and set a parameter to the detail’s loader. The mixin will do it automatically if the parameter has a special name pointing to the master container.

The mixin will use a state object to pass information between event handlers. It’s done mostly for demonstration purposes because we could put all the logic in a single BeforeShowEvent handler.

First, let’s create a class for the shared state. It contains a single field for storing a set of loaders to be triggered in the BeforeShowEvent handler:

public class DeclarativeLoaderParametersState {

    private Set<DataLoader> loadersToLoadBeforeShow;

    public DeclarativeLoaderParametersState(Set<DataLoader> loadersToLoadBeforeShow) {
        this.loadersToLoadBeforeShow = loadersToLoadBeforeShow;
    }

    public Set<DataLoader> getLoadersToLoadBeforeShow() {
        return loadersToLoadBeforeShow;
    }
}

Next, create the mixin interface:

public interface DeclarativeLoaderParameters {
    Pattern CONTAINER_REF_PATTERN = Pattern.compile(":(container\\$(\\w+))");

    @Subscribe
    default void onDeclarativeLoaderParametersInit(Screen.InitEvent event) { (1)
        Screen screen = event.getSource();
        ScreenData screenData = UiControllerUtils.getScreenData(screen);(2)

        Set<DataLoader> loadersToLoadBeforeShow = new HashSet<>();

        for (String loaderId : screenData.getLoaderIds()) {
            DataLoader loader = screenData.getLoader(loaderId);
            String query = loader.getQuery();
            Matcher matcher = CONTAINER_REF_PATTERN.matcher(query);
            while (matcher.find()) {(3)
                String paramName = matcher.group(1);
                String containerId = matcher.group(2);
                InstanceContainer<?> container = screenData.getContainer(containerId);
                container.addItemChangeListener(itemChangeEvent -> {(4)
                    loader.setParameter(paramName, itemChangeEvent.getItem());(5)
                    loader.load();
                });
                if (container instanceof HasLoader) {(6)
                    loadersToLoadBeforeShow.add(((HasLoader) container).getLoader());
                }
            }
        }

        DeclarativeLoaderParametersState state =
                new DeclarativeLoaderParametersState(loadersToLoadBeforeShow);(7)
        Extensions.register(screen, DeclarativeLoaderParametersState.class, state);
    }

    @Subscribe
    default void onDeclarativeLoaderParametersBeforeShow(Screen.BeforeShowEvent event) {(8)
        Screen screen = event.getSource();
        DeclarativeLoaderParametersState state =
                Extensions.get(screen, DeclarativeLoaderParametersState.class);
        for (DataLoader loader : state.getLoadersToLoadBeforeShow()) {
            loader.load();(9)
        }
    }
}
1 Subscribes to InitEvent.
2 Gets the ScreenData object where all data containers and loaders defined in XML are registered.
3 Checks if a loader parameter matches the :container$masterContainerId pattern.
4 Extracts the master container id from the parameter name and registers a ItemChangeEvent listener for this container.
5 Reloads the detail loader for the new master item.
6 Adds the master loader to set to trigger it later in the BeforeShowEvent handler.
7 Creates the shared state object and store it in the screen using Extensions utility class.
8 Subscribe to BeforeShowEvent.
9 Triggers all master loaders found in the InitEvent handler.

In the screen XML descriptor, define master and detail containers and loaders. The detail’s loader should have a parameter with the name like :container$masterContainerId:

<collection id="ordersDc"
            class="ui.ex1.entity.Order" fetchPlan="_base">
    <loader id="ordersDl">
        <query>
            <![CDATA[select e from uiex1_Order e where e.customer = :container$customersDc]]>
        </query>
    </loader>
</collection>
<collection id="customersDc" class="ui.ex1.entity.Customer" fetchPlan="_base">
    <loader id="customersDl">
        <query>
            <![CDATA[select e from uiex1_Customer e]]>
        </query>
    </loader>
</collection>

In the screen controller, just add the mixin interface, and it will trigger the loaders appropriately:

@UiController("demo_Order.browse")
@UiDescriptor("demo-order-browse.xml")
@LookupComponent("ordersTable")
public class DemoOrderBrowse extends StandardLookup<Order> implements DeclarativeLoaderParameters {
}