3.5.1.4. 使用界面片段

在本章节,介绍如何定义和使用界面片段。参考界面片段事件了解如何处理界面片段的生命周期事件。



声明式使用 fragment

假设我们有用来输入地址的 fragment:

AddressFragment.java
@UiController("demo_AddressFragment")
@UiDescriptor("address-fragment.xml")
public class AddressFragment extends ScreenFragment {
}
address-fragment.xml
<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd">
    <layout>
        <textField id="cityField" caption="City"/>
        <textField id="zipField" caption="Zip"/>
    </layout>
</fragment>

然后我们可以在其它界面使用 fragment 元素来包含此 fragment,fragment 元素需要有指向 fragment id 的 screen 属性,fragment id 在其 @UiController 注解设置:

host-screen.xml
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Some Screen">
    <layout>
        <groupBox id="addressBox" caption="Address">
            <fragment screen="demo_AddressFragment"/>
        </groupBox>
    </layout>
</window>

fragment 元素可以添加在界面任意的 UI 容器中,也包含最顶上的 layout 元素。

编程式使用 fragment

同一个片段也可以通过编程的方式添加到界面,需要在 InitEventAfterInitEvent 事件处理器中添加:

host-screen.xml
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Some Screen">
    <layout>
        <groupBox id="addressBox" caption="Address"/>
    </layout>
</window>
HostScreen.java
@UiController("demo_HostScreen")
@UiDescriptor("host-screen.xml")
public class HostScreen extends Screen {

    @Inject
    private Fragments fragments; (1)

    @Inject
    private GroupBoxLayout addressBox;

    @Subscribe
    private void onInit(InitEvent event) {
        AddressFragment addressFragment = fragments.create(this, AddressFragment.class); (2)
        addressBox.add(addressFragment.getFragment()); (3)
    }
}
1 - 注入 Fragments bean,用来实例化界面片段
2 - 用 class 创建片段控制器
3 - 从控制器中获取 Fragment 可视化组件,并添加到 UI 容器中。

如果该片段有参数,可以在将界面片段添加到界面之前通过公共 setter 方法设置。之后,片段控制器的 InitEventAfterInitEvent 处理方法里面可以访问到这些参数。

给界面片段传递参数

界面片段控制器可以有公共的 setter 方法用来接收参数,和打开界面一样的做法。如果界面片段是使用编程的方式打开,可以显式的调用 setters:

@UiController("demo_HostScreen")
@UiDescriptor("host-screen.xml")
public class HostScreen extends Screen {

    @Inject
    private Fragments fragments;

    @Inject
    private GroupBoxLayout addressBox;

    @Subscribe
    private void onInit(InitEvent event) {
        AddressFragment addressFragment = fragments.create(this, AddressFragment.class);
        addressFragment.setStrParam("some value"); (1)
        addressBox.add(addressFragment.getFragment());
    }
}
1 - 在将片段添加到界面之前先传递参数。

如果片段是通过 XML 以声明的方式添加到界面,使用 properties 元素来传递参数,示例:

<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Some Screen">
    <data>
        <instance id="someDc" class="com.company.demo.entity.Demo"/>
    </data>
    <layout>
        <textField id="someField"/>
        <fragment screen="demo_AddressFragment">
            <properties>
                <property name="strParam" value="some value"/> (1)
                <property name="dataContainerParam" ref="someDc"/> (2)
                <property name="componentParam" ref="someField"/> (3)
            </properties>
        </fragment>
    </layout>
</window>
1 - 传递一个字符串参数给 setStrParam() 方法。
2 - 传递一个数据容器给 setDataContainerParam() 方法。
3 - 传递 TextField 组件给 setComponentParam() 方法。

使用 value 属性设置值,用 ref 属性指定界面组件的标识符。setters 必须使用合适类型的参数。

界面 fragment 中的数据组件

界面 fragment 可以有自己的数据容器和数据加载器,通过 XML 元素 data 定义。同时,框架会为界面及其所有 fragments 创建DataContext的单例。因此,所有加载的实体都合并到同一数据上下文,并在父界面提交的时候一起保存更改。

下面的例子中,会使用界面 fragment 自己的数据容器和加载器。

假设在 fragment 中有 City 实体,我们希望使用下拉列表框展示可选的城市而不仅仅用文本控件来展示。可以跟普通界面一样,在 fragment 的 XML 描述中定义数据组件。

address-fragment.xml
<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd">
    <data>
        <collection id="citiesDc" class="com.company.demo.entity.City" view="_base">
            <loader id="citiesLd">
                <query><![CDATA[select e from demo_City e ]]></query>
            </loader>
        </collection>
    </data>
    <layout>
        <lookupField id="cityField" caption="City" optionsContainer="citiesDc"/>
        <textField id="zipField" caption="Zip"/>
    </layout>
</fragment>

如果需要在父界面打开时加载 fragment 的数据,需要订阅父界面事件:

AddressFragment.java
@UiController("demo_AddressFragment")
@UiDescriptor("address-fragment.xml")
public class AddressFragment extends ScreenFragment {

    @Inject
    private CollectionLoader<City> citiesLd;

    @Subscribe(target = Target.PARENT_CONTROLLER) (1)
    private void onBeforeShowHost(Screen.BeforeShowEvent event) {
        citiesLd.load();
    }
}
1 - 订阅父界面的 BeforeShowEvent 事件

@LoadDataBeforeShow 对界面 fragments 无效。

使用已有的数据容器

下一个例子展示了如何在 fragment 中使用父界面的数据容器。

host-screen.xml
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Some Screen">
    <data>
        <instance id="addressDc" class="com.company.demo.entity.Address"/> (1)
    </data>
    <layout>
        <groupBox id="addressBox" caption="Address">
            <fragment screen="demo_AddressFragment"/>
        </groupBox>
    </layout>
</window>
1 - fragment 将在下面使用的数据容器
address-fragment.xml
<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd">
    <data>
        <instance id="addressDc" class="com.company.demo.entity.Address"
                  provided="true"/> (1)

        <collection id="citiesDc" class="com.company.demo.entity.City" view="_base">
            <loader id="citiesLd">
                <query><![CDATA[select e from demo_City e]]></query>
            </loader>
        </collection>
    </data>
    <layout>
        <lookupField id="cityField" caption="City" optionsContainer="citiesDc"
                     dataContainer="addressDc" property="city"/> (2)
        <textField id="zipField" caption="Zip"
                   dataContainer="addressDc" property="zip"/>
    </layout>
</fragment>
1 - provided="true" 表示使用同样 id 的容器必须存在于父界面或者嵌套的 fragment 中,也就是说必须在该 fragment 外部提供
2 - UI 组件连接到提供的数据容器

在包含 provided="true" 属性的 XML 元素中,除了 id 之外其它所有的属性都会被忽略,但是也可以加上,以便提供设计思路。