3.5.1.5. 界面 Mixins

通过 Mixin 可以创建能在多个UI界面中重复使用的功能,而且不需要从公共基类继承界面。Mixin 通过 Java 接口实现,使用了接口的默认方法。

Mixin 有如下特性:

  • 一个界面可以有多个 Mixin。

  • Mixin 接口可以订阅 界面事件

  • Mixin 可以在界面中保存一些状态,如果需要的话。

  • Mixin 也可以获取界面组件和基础架构 bean,比如 DialogsNotifications 等。

  • 如果需要参数化 mixin 的行为,mixin 可以依赖界面的注解或者引入抽象方法交由界面实现。

使用 mixin 与在界面控制器中实现特定的接口一样简单。下面的示例中,CustomerEditor 界面使用了由 HasCommentsHasHistoryHasAttachments 接口实现的 mixin 功能:

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

Mixin 可以使用以下类来处理界面和界面基础架构:

  • com.haulmont.cuba.gui.screen.Extensions 提供静态方法,用来保存和获取 mixin 使用的界面状态,还能访问 BeanLocator,这可以用来获取任何 Spring bean。

  • UiControllerUtils 提供对界面UI和数据组件的访问。

下面是展示如何创建和使用 mixin 的示例。

Banner mixin

这个是非常简单的例子,用来在界面顶端展示一个标签。

  1. package com.company.demo.web.mixins;
  2. import com.haulmont.cuba.core.global.BeanLocator;
  3. import com.haulmont.cuba.gui.UiComponents;
  4. import com.haulmont.cuba.gui.components.Label;
  5. import com.haulmont.cuba.gui.screen.*;
  6. import com.haulmont.cuba.web.theme.HaloTheme;
  7. public interface HasBanner {
  8. @Subscribe
  9. default void initBanner(Screen.InitEvent event) {
  10. BeanLocator beanLocator = Extensions.getBeanLocator(event.getSource()); (1)
  11. UiComponents uiComponents = beanLocator.get(UiComponents.class); (2)
  12. Label<String> banner = uiComponents.create(Label.TYPE_STRING); (3)
  13. banner.setStyleName(HaloTheme.LABEL_H2);
  14. banner.setValue("Hello, world!");
  15. event.getSource().getWindow().add(banner, 0); (4)
  16. }
  17. }
1- 获取 BeanLocator
2- 获取 UI 组件的工厂。
3- 创建 Label 并设置其属性。
4- 将标签添加到界面的根 UI 组件中。

在界面中可以这样使用该 mixin:

  1. package com.company.demo.web.customer;
  2. import com.company.demo.web.mixins.HasBanner;
  3. import com.haulmont.cuba.gui.screen.*;
  4. import com.company.demo.entity.Customer;
  5. @UiController("demo_Customer.edit")
  6. @UiDescriptor("customer-edit.xml")
  7. // ...
  8. public class CustomerEdit extends StandardEditor<Customer> implements HasBanner {
  9. // ...
  10. }

DeclarativeLoaderParameters mixin

下面这个 mixin 可以帮助在数据容器之间创建主从关系。通常的做法,是需要订阅主容器的 ItemChangeEvent 事件,将改动的主容器内容设置为从容器的数据加载器参数,如数据组件之间的依赖所述。但是如果参数是指向主容器的特殊名称,mixin 能自动完成此功能。

Mixin 会使用状态对象在事件处理器之间传递信息。这里为了演示,我们将逻辑分散开,但实际上所有的逻辑都可以在一个 BeforeShowEvent 处理器中完成。

首先,为共享状态创建一个类。包含单一字段,用来保存将在 BeforeShowEvent 处理器中触发的一组数据加载器:

  1. package com.company.demo.web.mixins;
  2. import com.haulmont.cuba.gui.model.DataLoader;
  3. import java.util.Set;
  4. public class DeclarativeLoaderParametersState {
  5. private Set<DataLoader> loadersToLoadBeforeShow;
  6. public DeclarativeLoaderParametersState(Set<DataLoader> loadersToLoadBeforeShow) {
  7. this.loadersToLoadBeforeShow = loadersToLoadBeforeShow;
  8. }
  9. public Set<DataLoader> getLoadersToLoadBeforeShow() {
  10. return loadersToLoadBeforeShow;
  11. }
  12. }

接下来,创建 mixin 接口:

  1. package com.company.demo.web.mixins;
  2. import com.haulmont.cuba.gui.model.*;
  3. import com.haulmont.cuba.gui.screen.*;
  4. import java.util.HashSet;
  5. import java.util.Set;
  6. import java.util.regex.Matcher;
  7. import java.util.regex.Pattern;
  8. public interface DeclarativeLoaderParameters {
  9. Pattern CONTAINER_REF_PATTERN = Pattern.compile(":(container\\$(\\w+))");
  10. @Subscribe
  11. default void onDeclarativeLoaderParametersInit(Screen.InitEvent event) { (1)
  12. Screen screen = event.getSource();
  13. ScreenData screenData = UiControllerUtils.getScreenData(screen); (2)
  14. Set<DataLoader> loadersToLoadBeforeShow = new HashSet<>();
  15. for (String loaderId : screenData.getLoaderIds()) {
  16. DataLoader loader = screenData.getLoader(loaderId);
  17. String query = loader.getQuery();
  18. Matcher matcher = CONTAINER_REF_PATTERN.matcher(query);
  19. while (matcher.find()) { (3)
  20. String paramName = matcher.group(1);
  21. String containerId = matcher.group(2);
  22. InstanceContainer<?> container = screenData.getContainer(containerId);
  23. container.addItemChangeListener(itemChangeEvent -> { (4)
  24. loader.setParameter(paramName, itemChangeEvent.getItem()); (5)
  25. loader.load();
  26. });
  27. if (container instanceof HasLoader) { (6)
  28. loadersToLoadBeforeShow.add(((HasLoader) container).getLoader());
  29. }
  30. }
  31. }
  32. DeclarativeLoaderParametersState state =
  33. new DeclarativeLoaderParametersState(loadersToLoadBeforeShow); (7)
  34. Extensions.register(screen, DeclarativeLoaderParametersState.class, state);
  35. }
  36. @Subscribe
  37. default void onDeclarativeLoaderParametersBeforeShow(Screen.BeforeShowEvent event) { (8)
  38. Screen screen = event.getSource();
  39. DeclarativeLoaderParametersState state =
  40. Extensions.get(screen, DeclarativeLoaderParametersState.class);
  41. for (DataLoader loader : state.getLoadersToLoadBeforeShow()) {
  42. loader.load(); (9)
  43. }
  44. }
  45. }
1- 订阅 InitEvent
2- 获取 ScreenData 对象,其中注册了 XML 中定义的所有数据容器和加载器。
3- 检查加载器的参数是否符合 :container$masterContainerId 模式的定义。
4- 从参数名中抽取主容器id,然后为该容器注册一个 ItemChangeEvent 监听器。
5- 使用新的主实体重新加载从实体数据加载器。
6- 将主加载器添加到集合中,以便之后在 BeforeShowEvent 处理器中能触发。
7- 创建共享状态对象,使用 Extensions 工具类将该对象保存在界面中。
8- 订阅 BeforeShowEvent 事件。
9- 触发在 InitEvent 处理器中找到的所有主加载器。

在界面 XML 描述中定义主从容器以及数据加载器。从加载器需要带有一个参数,其名称类似 :container$masterContainerId

  1. <collection id="countriesDc"
  2. class="com.company.demo.entity.Country" view="_local">
  3. <loader id="countriesDl">
  4. <query><![CDATA[select e from demo_Country e]]></query>
  5. </loader>
  6. </collection>
  7. <collection id="citiesDc"
  8. class="com.company.demo.entity.City" view="city-view">
  9. <loader id="citiesDl">
  10. <query><![CDATA[
  11. select e from demo_City e
  12. where e.country = :container$countriesDc
  13. ]]></query>
  14. </loader>
  15. </collection>

在界面控制器中,只需要添加 mixin 接口,然后就能自动触发加载器了:

  1. package com.company.demo.web.country;
  2. import com.company.demo.entity.Country;
  3. import com.company.demo.web.mixins.DeclarativeLoaderParameters;
  4. import com.haulmont.cuba.gui.screen.*;
  5. @UiController("demo_Country.browse")
  6. @UiDescriptor("country-browse.xml")
  7. @LookupComponent("countriesTable")
  8. public class CountryBrowse extends StandardLookup<Country>
  9. implements DeclarativeLoaderParameters {
  10. }