使用 Fragments

在本章节,介绍如何定义和使用 界面 fragments。参阅 ScreenFragment Events 了解如何处理 fragment 的生命周期事件。

声明式使用

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

AddressFragment.java
@UiController("sample_AddressFragment")
@UiDescriptor("address-fragment.xml")
public class AddressFragment extends ScreenFragment {
}
address-fragment.xml
<fragment xmlns="http://jmix.io/schema/ui/fragment">
    <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://jmix.io/schema/ui/window"
        caption="msg://hostScreen.caption">
    <layout>
        <groupBox id="addressBox" caption="Address">
            <fragment screen="sample_AddressFragment"/>
        </groupBox>
    </layout>
</window>

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

编程式使用

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

host-screen.xml
<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://hostScreen.caption">
    <layout>
        <groupBox id="addressBox" caption="Address"/>
    </layout>
</window>
HostScreen.java
@UiController("sample_HostScreen")
@UiDescriptor("host-screen.xml")
public class HostScreen extends Screen {

    @Autowired
    private Fragments fragments; (1)

    @Autowired
    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,用来实例化界面 fragment。
2 用类创建 fragment 控制器
3 从控制器中获取 Fragment 可视化组件,并添加到 UI 容器中。
如果 fragment 有参数,可以在将 fragment 添加到界面之前通过公共 setter 方法设置。之后,fragment 控制器的 InitEventAfterInitEvent 处理方法里面可以访问到这些参数。

传递参数

Fragment 控制器可以有公共的 setter 方法用来接收参数,与打开界面时传参一样。示例:

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

    @Autowired
    private TextField<String> zipcodeField;

    private String zipcode;

    public void setZipcode(String zipcode) { (1)
        this.zipcode = zipcode;
    }

    public void setAddressContainer(InstanceContainer<Address> dataContainer) { (1)
        //
    }

    @Subscribe
    public void onInit(InitEvent event) {
        zipcodeField.setInputPrompt(zipcode); (2)
    }

}
1 Setter 方法,支持为界面 fragment 传递参数。
2 参数使用示例。

如果 fragment 是以编程式打开,需要显式调用 setters:

HostScreen.java
@Autowired
private Fragments fragments;

@Autowired
private GroupBoxLayout addressBox;

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

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

host-screen.xml
<fragment id="addressFragment" screen="sample_AddressFragment">
    <properties>
        <property name="zipcode" value="2779001"/> (1)
        <property name="addressContainer" ref="addressDc"/> (2)
    </properties>
</fragment>
1 传递一个字符串参数给 setZipcode() 方法。
2 传递一个数据容器给 setAddressContainer() 方法。

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

使用数据组件

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

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

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

address-fragment.xml
<fragment xmlns="http://jmix.io/schema/ui/fragment">
    <data>
        <collection id="countriesDc" class="ui.ex1.entity.Country">
            <fetchPlan extends="_base"/>
            <loader id="countriesDl">
                <query>
                    <![CDATA[select e from uiex1_Country e]]>
                </query>
            </loader>
        </collection>
    </data>
    <facets>
        <dataLoadCoordinator auto="true"/>
    </facets>
    <layout>
        <entityComboBox id="countryField" caption="Country" optionsContainer="countriesDc"/>
        <textField id="zipField" caption="Zip"/>
    </layout>
</fragment>

当父界面打开时,数据会加载到 fragment。

共用数据容器

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

host-screen.xml
<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://hostScreen.caption">
    <data>
        <instance id="addressDc"
                  class="ui.ex1.entity.Address"> (1)
            <fetchPlan extends="_base"/>
            <loader/>
        </instance>
    </data>
    <facets>
        <dataLoadCoordinator auto="true"/>
    </facets>
    <layout>
        <groupBox id="addressBox" caption="Address">
            <fragment screen="sample_AddressFragment"/>
        </groupBox>
    </layout>
</window>
1 下面的 fragment 将使用的数据容器
address-fragment.xml
<fragment xmlns="http://jmix.io/schema/ui/fragment">
    <data>
        <instance id="addressDc" class="ui.ex1.entity.Address"
                  provided="true"/> (1)
        <collection id="countriesDc" class="ui.ex1.entity.Country" provided="true"/>
    </data>
    <layout>
        <entityComboBox id="countryField" caption="Country" optionsContainer="countriesDc"
                        dataContainer="addressDc" property="country"/> (2)
        <textField id="streetField"
                   property="street"/>
        <textField id="zipcodeField"
                   property="zipcode"/>
    </layout>
</fragment>
1 provided="true" 表示使用同样 id 的容器必须存在于父界面或者外部嵌套的 fragment 中。
2 UI 组件连接到提供的数据容器

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

Fragment 中也可以定义由父界面提供的加载器。外部提供的加载器 id 必须与原 id 一致,并使用 provided="true" 属性。示例:

<loader id="addressDl" provided="true"/>