3.5.1.5. 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. In the example below, the CustomerEditor screen acquires functionality of mixins implemented by the HasComments, HasHistory, HasAttachments interfaces:

public class CustomerEditor extends StandardEditor<Customer>
                            implements HasComments, HasHistory, HasAttachments {
    // ...
}

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

  • com.haulmont.cuba.gui.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.

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

Below are examples that demonstrate how to create and use mixins.

Banner mixin

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

package com.company.demo.web.mixins;

import com.haulmont.cuba.core.global.BeanLocator;
import com.haulmont.cuba.gui.UiComponents;
import com.haulmont.cuba.gui.components.Label;
import com.haulmont.cuba.gui.screen.*;
import com.haulmont.cuba.web.theme.HaloTheme;

public interface HasBanner {

    @Subscribe
    default void initBanner(Screen.InitEvent event) {
        BeanLocator beanLocator = Extensions.getBeanLocator(event.getSource()); (1)
        UiComponents uiComponents = beanLocator.get(UiComponents.class); (2)

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

        event.getSource().getWindow().add(banner, 0); (4)
    }
}
1 - get BeanLocator.
2 - get factory of UI components.
3 - create Label and set its properties.
4 - add label to the screen’s root UI component.

The mixin can be used in a screen as follows:

package com.company.demo.web.customer;

import com.company.demo.web.mixins.HasBanner;
import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;

@UiController("demo_Customer.edit")
@UiDescriptor("customer-edit.xml")
// ...
public class CustomerEdit extends StandardEditor<Customer> 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, as described in Dependencies Between Data Components. 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:

package com.company.demo.web.mixins;

import com.haulmont.cuba.gui.model.DataLoader;
import java.util.Set;

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:

package com.company.demo.web.mixins;

import com.haulmont.cuba.gui.model.*;
import com.haulmont.cuba.gui.screen.*;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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 - subscribe to InitEvent.
2 - get the ScreenData object where all data containers and loaders defined in XML are registered.
3 - check if a loader parameter matches the :container$masterContainerId pattern.
4 - extract the master container id from the parameter name and register a ItemChangeEvent listener for this container.
5 - reload the detail loader for the new master item.
6 - add the master loader to set to trigger it later in the BeforeShowEvent handler.
7 - create the shared state object and store it in the screen using Extensions utility class.
8 - subscribe to BeforeShowEvent.
9 - trigger 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="countriesDc"
            class="com.company.demo.entity.Country" view="_local">
    <loader id="countriesDl">
        <query><![CDATA[select e from demo_Country e]]></query>
    </loader>
</collection>
<collection id="citiesDc"
            class="com.company.demo.entity.City" view="city-view">
    <loader id="citiesDl">
        <query><![CDATA[
        select e from demo_City e
        where e.country = :container$countriesDc
        ]]></query>
    </loader>
</collection>

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

package com.company.demo.web.country;

import com.company.demo.entity.Country;
import com.company.demo.web.mixins.DeclarativeLoaderParameters;
import com.haulmont.cuba.gui.screen.*;

@UiController("demo_Country.browse")
@UiDescriptor("country-browse.xml")
@LookupComponent("countriesTable")
public class CountryBrowse extends StandardLookup<Country>
                           implements DeclarativeLoaderParameters {
}