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 toBeanLocator
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.
- 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 { }