21.2. OperaMasks 3.2中的局部更新

OperaMasks 3.2中提供了灵活易用的局部更新方式,在兼顾带宽与性能的前提下,开发者只需要根据当前的开发场景选择合适的更新方式即可,而不需要时时刻刻捧着构件属性文档来查证某个属性是API方式更新,还是重画更新。而且开发者需要编写的代码非常简单,只需要对要更新区域、构件或属性进行标记即可,无须编写实际的更新代码。

21.2.1. 同表单自动更新

Web应用中,最常见的情况是,用户提交了一个表单(<w:form>)中的数据,而执行结果也只需要对同一个表单内的某些字段(例如<w:textField>或<h:outputText>)进行更新。由于在表单外部的输入框没有被提交,通常情况下我们反而不希望引擎自动用当前的后台数据值覆盖了这些输入框的当前值。对于这种场景,OperaMasks会自动进行处理,开发者不需要额外关注。

例如,页面代码:

<w:form>
    请输入您的名字:
    <w:textField id="name"/>
    <h:outputText id="result"/>
    <w:button id="action" label="提交"/>
</w:form>

Managed Bean代码:

@Bind
private String name, result;

@Action
private void action() {
    result = "Hi! " + name;
}

在这个例子中,我们可以发现,不需要编写任何一行额外代码,OperaMasks引擎会自动负责<h:outputText id="result">构件的更新,因为它与发起提交的命令构件<w:button id="action">位于同一个表单内。

如果存在多个<w:form>通过配置groupId属性关联提交的情况,结果是一样的,OperaMasks引擎会自动所有关联提交的表单的更新。

但是,您运行以下代码时,却会发现结果没有更新:

页面代码:

<w:form>
   <w:button id="btn"/>
</w:form>

Managed Bean代码

@Bind(id="btn", attribute="label")
private String btn_label = "Click Me";

@Action
private void btn() {
   btn_label="Clicked!"
}

这是因为,出于性能考虑,默认情况下引擎只负责同一个表单内的UIInput、UIOutput与FormUpdatable类型的子构件的Data级别更新。这句话如何理解呢?UIInput与UIOutput类型的构件,基本上就是各种表单输入框构件,与<h:outputText>等输出构件;FormUpdatable是OperaMasks中用来标记构件需要自动更新的一个标志接口,目前官方构件只有UIDataGrid与UIPanel实现了这个接口。这个默认筛选范围,能应付大部分的应用场景,并保证尽可能少占用带宽。对于超出这个范围的其他构件(例如<w:button>),就需要用户在代码中显式注册局部更新,注册的方法在后面会谈到。

那么,“Data级别更新”又是什么意思呢?正如第 21.1 节 “什么是页面局部更新”中所说,局部更新具有多种类型,而OperaMasks则根据这些类型提供了对应的更新级别,采用枚举类org.operamasks.faces.user.ajax.UpdateLevel进行标示:

public enum UpdateLevel {
    /**
     * 强制重画构件,更新所有属性。
     */
    ForceRepain,
    
    /**
     * 只更新可用轻量级方式更新的常用属性(包括构件Value值与数据模型),
     * 和通过ParticialUpdateManager显式标记为需要更新的属性。
     */
    Lite,
    
    /**
     * 只更新构件的数据模型与Value值。适用于Combo等具有独立数据模型的构件 
     * 和通过ParticialUpdateManager显式标记为需要更新的属性。
     * 对于DataGrid与DataView等通过value值指定数据模型的构件,效果与ValueOnly一致。
     */
    Data,
    
    /**
     * 只更新构件Value值,
     * 和通过ParticialUpdateManager显式标记为需要更新的属性。
     */
    ValueOnly,
    
    /**
     * 只更新通过ParticialUpdateManager显式标记为需要更新的属性。
     * 此级别通常为内部使用。
     */
    Specified,
    
    /**
     * 禁止构件更新,在此级别标记的构件不会在本次响应中更新(即使已经加入过其他类别)
     */
    Skip
}

也就是说,默认情况下使用UpdateLevel.Data作为表单自动更新的策略,如果表单中包含<w:combo>等具有独立数据模型的构件,将会同时更新数据模型,以保证下拉列表联动等特性。如果希望改变表单的更新级别,可以设置表单的updateLevel属性:

<!-- 只更新构件值,适用于在开发期就明确表单中的构件不会改变数据模型的情况 -->
<w:form updateLevel="valueOnly"> ... </w:form>

<!-- 改为手动更新,引擎不负责子构件的自动更新,必须显式指定 -->
<w:form updateLevel="Specified"> ... </w:form>

<!-- 更新所有轻量级属性,例如disabled、style、styleClass等,略为占用带宽 -->
<w:form updateLevel="Lite"> ... </w:form>

21.2.2. 注册跨表单更新

对于表单外构件的更新,以及表单内超出表单自身更新级别的局部更新,就要求我们在程序中显式地将相关的构件注册为需要更新。注册跨表单更新有两种方式,页面形式和编程API形式。

21.2.2.1. 在页面中注册更新

可以通过给页面上的提交构件添加<ajax:linkedUpdate>子构件,来注册本次提交需要关联更新的区域。例如:

页面代码:

<w:form>
    请输入您的名字:
    <w:textField id="name"/>
    <w:button id="action" label="提交">
        <ajax:linkedUpdate target="result"/>
    </w:button>
</w:form>

<h:outputText id="result"/>

Managed Bean代码:

@Bind
private String name, result;

@Action
private void action() {
    result = "Hi! " + name;
}

在上面的例子中,如果去掉<w:button id="action">的子构件<ajax:linkedUpdate>,会发现在表单<w:form>外部的<h:outputText id="result">不会自动刷新。通常来说,<ajax:linkedUpdate>构件可以作为<w:button>、<ajax:action>、<ajax:submitAction>,以及所有设置了valueChangedListener的输入框构件的直接子构件。

如果同一个提交动作需要更新数个表单外的构件,允许使用多个并列的<ajax:linkedUpdate>或在一个<ajax:linkedUpdate>使用逗号分隔的多个id指定。以下两种方式是等价的:

写法1:

<w:button>
    <ajax:linkedUpdate target="result1"/>
    <ajax:linkedUpdate target="result2"/>
</w:button>

写法2(注意逗号前后不要加入多余的空格):

<w:button>
    <ajax:linkedUpdate target="result1,result2"/>
</w:button>

如果需要指定某个区域的关联更新(例如外部的某个<w:form>,可以使用<ajax:linkedUpdate>的includeChildren属性:

<w:form id="form1">
    <w:button id="clear" label="清空表单">
        <ajax:linkedUpdate target="form2" includeChildren="true"/>
    </w:button>
</w:form>

<w:form id="form2">
    <w:textField id="name"/>
    <w:textField id="phone"/>
    <w:textField id="email"/>
</w:form>

为了避免不必要地损失性能,<ajax:linkedUpdate>的默认更新级别是ValueOnly,即只更新构件取值,如果现实场景需要指定其他更新级别,可以使用level属性,例如:

<w:button id="disabledAll" id="禁用所有输入框">
    <ajax:linkedUpdate target="form2" includeChildren="true" level="Lite"/>
</w:button>

等等,这里不是又回到了上面的问题吗?我怎么知道我做的改变需要使用哪种更新级别呢?事实上,在编写页面时,开发者往往也只能对提交后可能需要更新的区域与级别做一个大致的预测,例如已知本次处理会影响Combo的下拉列表,则可以把level设为Data等等。但是,如果在开发页面时您就明确了这次提交行为会具体修改哪些属性,那么可以使用<ajax:linkedUpdate>的attribute属性来声明,OperaMasks引擎会自动根据您所注册的属性选用合适的更新级别。例如

<w:button id="disabledAll" id="禁用所有输入框">
    <ajax:linkedUpdate target="form2" includeChildren="true" attribute="disabled"/>
</w:button>

同样,多个属性之间可以用逗号分隔。

正如上面所说,在开发页面的时候,开发者往往不知道这次提交实际上要更新哪些构件,哪些属性,这种场景下,可以在后台程序逻辑中使用编程API的方式去注册局部更新。

(关于<ajax:linkedUpdate>构件的更多属性说明,可以参考构件说明文档)

21.2.2.2. 使用API方式注册局部更新

在大部分开发场景中,开发者需要根据后台逻辑或模型数据决定哪些表单外的构件或区域需要关联更新。这时,可以使用局部的更新的API:PartialUpdateManager类来注册局部更新。在Managed Bean中,可以通过以下两种等价的方法获取PartialUpdateManager的实例:

使用静态类方法获取单例:

//id="action"的按钮的动作方法
@Action
private void action() {
    PartialUpdateManager update = PartialUpdateManager.getInstance();
    ...
    update.markUpdate("result");
}

使用@Inject注解注入:

@Inject
private PartialUpdateManager update;

@Action
private void action() {
    update.markUpdate("result");
}

PartialUpdateManager提供了两个主要方法,markUpdate与markAttributeUpdate,用于注册局部更新构件:

表 21.1. PartialUpdateManager公共方法列表

方法名称方法参数方法说明
getInstance获取PartialUpdateManager在本请求范围内唯一的实例。
markUpdateString... id把指定id的构件标记为需要更新,并使用默认更新级别
UIComponent... component把指定的构件标记为需要更新,并使用默认更新级别
Collection<UIComponent> components把指定的构件标记为需要更新,并使用默认更新级别
UpdateLevel level, String... id把指定id的构件标记为需要更新,并使用指定的更新级别
UpdateLevel level, UIComponent... component把指定的构件标记为需要更新,并使用指定的更新级别
UpdateLevel level, Collection<UIComponent> component把指定的构件标记为需要更新,并使用指定的更新级别
boolean recursive, UpdateLevel level, String... ids把指定id的构件标记为需要更新,并使用指定的更新级别。如果recursive参数为true,且指定的构件为容器构件,则自动同时更新其中的子构件
boolean recursive, UpdateLevel level, UIComponent... components把指定的构件标记为需要更新,并使用指定的更新级别。 如果recursive参数为true,且指定的构件为容器构件,则自动同时更新其中的子构件
boolean recursive, UpdateLevel level, Collection<UIComponent> components把指定的构件标记为需要更新,并使用指定的更新级别。如果recursive参数为true,且指定的构件为容器构件,则自动同时更新其中的子构件
PartialUpdateFilter filter, UpdateLevel level, String... ids把指定的构件及其子构件标记为需要更新,并使用指定的更新级别。并在加入每个子构件之前调用指定PartialUpdateFilter的isMatch方法,开发人员可以实现该方法来决定一个子构件是否应该进行局部更新注册。
PartialUpdateFilter filter, UpdateLevel level, UIComponent... components把指定的构件及其子构件标记为需要更新,并使用指定的更新级别。并在加入每个子构件之前调用指定PartialUpdateFilter的isMatch方法,开发人员可以实现该方法来决定一个子构件是否应该进行局部更新注册。
PartialUpdateFilter filter, UpdateLevel level, Collection<UIComponent> components把指定的构件及其子构件标记为需要更新,并使用指定的更新级别。并在加入每个子构件之前调用指定PartialUpdateFilter的isMatch方法,开发人员可以实现该方法来决定一个子构件是否应该进行局部更新注册。
markAttributeUpdateString id, String... attributes把指定构件的指定属性设为需要更新。如果构件已通过markUpdate方法标记,仍会更新符合更新级别的其他属性,并根据这里指定属性的性质重新选择合适的更新方式。
UIComponent component, String... attributes把指定构件的指定属性设为需要更新。如果构件已通过markUpdate方法标记,仍会更新符合更新级别的其他属性,并根据这里指定属性的性质重新选择合适的更新方式。

因此,我们可以这样写:

页面代码:

<layout:panel id="panel" title="输入名字" width="200" height="200">
<w:form>
    <h:outputLabel value="请输入您的名字:"/>
    <w:textField id="name"/>
    <w:button id="action" label="提交"/>
</w:form>
</layout:panel>

<h:outputText id="result"/>

Managed Bean代码:

@Inject
private PartialUpdateManager update;

@Bind
private String name, result;

@Bind(id="panel", attribute="title")
private String title;

@Action
private void action() {
    result = "Hi! " + name;
    title = "当前用户:" + name;
    update.markUpdate(UpdateLevel.ValueOnly, "result", "panel");
    //或者
    //update.markAttributeUpdate("result", "value");
    //update.markAttributeUpdate("panel", "title");
}

21.2.2.3. 使用ajax:updater进行重画刷新

<ajax:updater>是OperaMasks中一个历史悠久的构件,在早期的版本中,它身兼数职,既负责页面的局部刷新,又负责页面的嵌套引入。在OperaMasks 2.0引入Facelet体系与<w:iframe>构件后,<ajax:updater>引入页面的能力逐渐被Facelet与<w:iframe>分别替代了。在OperaMasks 3.2中,<ajax:updater>是一个专门用来在页面标记一个可以进行局部刷新的区域,并可以直接使用构件类AjaxUpdater的reload方法对该区域进行重画

基本上,使用<ajax:updater>与使用上述的<ajax:linkedUpdate>(或PartialUpdateManager)并把更新级别设为ForceRepaint是一样的。不同之处在于<ajax:linkedUpdate>依附在提交构件上,而且必须为更新区域找一个最外层的父构件。而<ajax:updater>则依附在需要更新的区域上,并且本身就是更新区域的一个最外层父构件。例如:

页面代码:

<w:form>
    <w:textField id="name"/>
    <w:button id="action" label="提交"/>
</w:form>

<ajax:updater id="updater">
        您的名字是: <h:outputText id="response"/>
</ajax:updater>

ManagedBean代码

@Bind
private AjaxUpdater updater;

@Bind
private String name, response;

@Action
private void action() {
    response = name;
    updater.reload();
}

由于<ajax:updater>必然是以重画的方式进行刷新,因此它的刷新效率和占用的带宽相对来说是较高的,在某些浏览器中会引起被刷新区域的轻微闪烁。因此不建议在程序中滥用,特别是不建议用来自动更新一些刷新频率较高的区域(例如使用编程方式定时刷新且周期很短)。<ajax:updater>的一个典型应用场景是在程序中动态增删了页面上的构件时,必须用<ajax:updater>重画被更改的区域。

21.2.2.4. 使用构件类上repaint方法进行重画刷新

现实开发中,也有不少场景是在开发过程中已经知道某个构件必须进行重画刷新,例如增删了它的子构件,或者改变了一个对布局有较大影响的属性。这时通过PartialUpdateManager或放入ajax:updater(仅仅包住这个构件)都显得不方便。OperaMasks中针对这种场景提供了一个更为便利的方案,可以在Managed Bean代码中绑定构件对象,然后直接调用构件对象上的repaint方法即可。例如:

@Bind(id="response")
private HtmlOutputText uiResponse;

@Bind
private String name, response;

@Action
private void action() {
    response = "Hi! " + name;
    uiResponse.repaint();
}