下载
 Oracle JDeveloper 11g Studio 版
 示例代码
   标签
adf, jdeveloper, 全部

Oracle ADF 富客户端中的 Ajax 与局部页面刷新

简要介绍 Oracle ADF 11g 中的 Oracle 应用开发框架 (Oracle ADF) Faces 富客户端 (RC) 的 Ajax 和 PPR 支持

作者:Lucas Jellema Oracle ACE 总监 和 Chris Muir Oracle ACE 总监

2009 年 7 月发布

当今 Web 2.0 应用程序的多样化行为在很大程度上归功于其与服务器的异步通信及其动态局部页面更新。当用户处理页面时,应用程序会维护各种后台交互。这些交互的结果用于应用程序的动态行为,例如更新域值;为用户提供即时验证反馈;使用可选择的值筛选列表;以及使用最新事件的详细信息更新页面(例如,数据库更改、接收聊天消息或订阅感兴趣的 RSS 信源的新项)。使用 Ajax(异步 JavaScript 和 XML)和局部页面刷新 (PPR) 实际可产生一个快速响应的动态交互式活动应用程序,极富吸引力和影响力。

在这个由两部分组成的系列中,Oracle ACE 总监 Chris Muir(澳大利亚)和 Lucas Jellema(荷兰)联袂介绍了 Oracle ADF 11g 中的 Oracle 应用开发框架 (Oracle ADF) Faces 富客户端 (RC) 的 Ajax 和 PPR 支持。他们逐步介绍了 Oracle ADF Faces RC 中的 Ajax 和 PPR 的基本知识和高级内容。这两篇文章包括许多代码示例以及一个具有完整源代码的 Oracle JDeveloper 应用程序。第一篇文章讨论可通过简单声明方式使用的功能,第二篇介绍如何使用包括 Oracle ADF Faces RC 客户端 (JavaScript) API 在内的编程工具实现更为高级的客户端/服务器集成。

前提条件和准备

本文适用于 Oracle ADF 11g 和 Oracle JDeveloper 11g,您目前可从 Oracle 技术网下载该产品版本。

所讨论的示例均包含于一个 Oracle JDeveloper 11g 应用程序中,您可在此处下载该。注意,运行此应用程序不需要数据库。

S.H.O.P. 案例

为本文中的示例提供上下文的案例是 Second Hand Opportunities Plaza(简称 S.H.O.P.),这是一个类似 eBay 的 Web 社区站点,人们可以在这里进行在线旧货交易(为要出售的物品发布广告,希望找到新买主),希望可通过交易挣些钱(参见图 1)。

图 1 S.H.O.P. 徽标

异步交互和局部页面刷新简介

丰富的现代 Web 2.0 应用程序不应使用“完整页面提交和重载”方法。我们希望页面能够局部、动态地刷新,并且在我们离开域(而不仅仅在我们提交完整页面)时立即进行验证。不仅应在提交完整页面时刷新派生的计算域,还应在其他尽可能的情况下刷新。当我们在页面上进行某些选择时,相关的值列表应该立即更新(例如,选择国家/地区后,应立即看到省份或城市列表)。当服务器上发生诸如数据库更改、接收聊天消息或订阅 RSS 信源新项之类的事件时,我们希望客户端能尽快向用户提供相关信息,即使用户没有明确要求。

归纳如下:应用程序应快速对用户操作作出反应,而无需用户明确要求反馈(例如,通过单击 Submit 按钮),甚至可能无需用户知道此反应(例如,不显示忙碌光标)。而此反应动作(无论是计算域的新值、有关验证失败的消息还是基于特定选择显示相应的上下文信息)都不应导致整个页面刷新,相反,应通过刷新页面的相关部分以微妙方式应用。

异步交互是应用程序启用的无阻塞活动,是浏览器与服务器之间的后台通信,(大部分)对最终用户不可见。局部页面刷新使用异步通信的结果来刷新页面的指定区域。PPR 通过 JavaScript(有时称为动态 HTML (DHTML))来使用浏览器的文档对象模型 (DOM)。

近年来,Ajax 和 PPR 的一个不足之处一直是开发难题。除了复杂和难以自动化的测试之外,实现丰富的功能通常也是一件很麻烦的事。异步编程范例本身很复杂,因为这是 PPR 所需的 DOM 操作。JavaScript 编程本身就采用一些非结构化并且不是非常高效的开发方法,这会导致代码难以调试和维护,并且它还需要 Java Web 应用程序开发人员通常不具备的技能。许多 Web 应用程序并未充分利用丰富的 Web 2.0 选项。

借助 Oracle ADF Faces RC,实现 Ajax 和 PPR 功能已经在很大程度上提升到声明式配置和服务器端编程的层面,这意味着,这是使用 Oracle ADF Faces RC 组件开发 Web 应用程序不可或缺的一部分。由于 Oracle ADF 的内置机制,只需付诸极少的努力便可动态、即时地响应用户浏览器的活动。

在 Oracle ADF 的后端,称为 PPR 请求的异步交互在 JavaScript 中启动。该请求会传送到服务器,贯穿特定版本的 Oracle ADF (JavaServer Faces [JSF]) 生命周期,其间仅以专门的封装方式呈现必需的组件以响应用户。响应在 JavaScript 中处理,其内容用于刷新页面上的指定目标区域。虽然 PPR 请求/响应周期本身对用户不可见(它在服务器的后端发生,这不会冻结浏览器,并且对用户在页面上执行的操作没有影响),但在浏览器上处理 PPR 响应肯定会对页面产生影响。可能发生的局部页面刷新将采用服务器端应用程序在 PPR 响应中呈现的 HTML,并使用此格式刷新所涉及组件的客户端显示 — 这涉及通过 Oracle ADF Faces RC 中的基于组件的客户端 API 来操作浏览器 DOM。

内置 PPR 功能

本文的主要目的是说明如何在您自己的自定义应用程序中利用 Oracle ADF Faces RC 的 PPR 功能。必须意识到,许多 Oracle ADF Faces 组件本身已实现局部刷新功能。只需使用这些组件,即可在 PPR 基础架构之上构建应用程序。例如,Oracle ADF Faces Rich Table、Rich Tree 和 Rich Tree Table 组件以及所有数据可视化组件都使用 PPR 延后加载它们展示的数据。首次加载页面后(这些组件尚未包含任何数据),页面的加载事件将触发自动 PPR 请求,以便从服务器获取相应的数据。

对于表或其他组件,您可能会看到“Fetching Data...”消息,这表示用于获取数据的 PPR 命令正在执行,如图 2 所示。对于将 ContentDelivery 属性设置为 lazy 的表,就会发生这种情况。

图 2 正在进行中的异步数据获取

当用户滚动表时,将从服务器获取更多数据 — 通过其他 PPR 请求。类似的示例是允许其基于 PPR 的节点展开和折叠的 Rich Tree。这些特性展示出的 Ajax 的特点通过以下过程可初见端倪:用户激活(单击)控制元素,然后应用程序在本地刷新一部分页面进行响应 — 没有整个页面刷新,没有显示沙漏的空白页面,也没有浏览器锁定。例如,当您如图 3 所示对表中的记录进行排序时,仅刷新表的内容,页面上的其他内容均不刷新。

图 3 Rich Table 的一些内置局部页面刷新特性:公开详细信息、筛选以及对行进行排序

注意,除了需要客户端/服务器后台通信的内置 Oracle ADF Faces RC Ajax 功能之外,富客户端组件中还有许多动态行为,这些行为完全在客户端处理,除非开发人员显式要求向服务器发送通知。示例包括客户端验证、表列大小调整、在选项卡之间和区域之间进行切换、选项卡式面板和折叠面板、拖放以及显示和隐藏弹出窗口。

Oracle ADF Faces RC 局部页面刷新的首要步骤:使用 autoSubmit 即时提交更改的域值

对于 Oracle ADF Faces RC 中的 PPR,您要了解的第一个概念是如何在输入组件上使用 autoSubmit 属性。该属性设置为 true 时,将指示 Oracle ADF 框架自动将用户更改的组件值提交到服务器。当用户导航离开其值已更改的此类 autoSubmit 项时,将通过异步 PPR 请求提交表单,并将对组件进行正常处理 — 转换、验证、模型更新、重新呈现。当 PPR 周期结束时,服务器就会与客户端同步(至少针对这个组件同步)。

在以下示例中,我们有一个用于发布新产品广告的页面。用户可以设置产品名称、最初购买年份、价格以及描述。价格的 inputText 组件将 autoSubmit 属性设置为 true。这意味着,当用户更改价格并导航离开价格项时,将在 Oracle ADF Faces 生命周期内异步提交表单并处理价格组件。

                                 
< af:inputText id="productpriceIT" label="Price" autoSubmit="true"
value="#{advertisement.price}" >
advertisement bean 中的 setPrice() 方法将新设置的值写入控制台。
                                 
public void setPrice(float price) {


System.out.println("New value for product price = "+price);

this.price = price;
}

一旦我们导航离开 Price 域,便可看到控制台上已显示我们在域中输入的新值(参见图 4)。这演示了价格更改如何触发处理值的 PPR 请求(包括模型更新)。

图 4 在客户端输入新值时,该值将被发送到服务器并在控制台上显示

注意,当您更改产品名称(或任何其他项)时,不会触发 PPR 请求;当您在更改名称之后更改价格时,后续 PPR 请求不会处理名称,而只会处理 autoSubmit 价格组件。

假设我们要强制价格介于 $5 和 $500 之间,并且金额大于 $50 时必须是整数。我们可以在价格的 inputText 上设置 validator 属性,以调用验证输入值的方法。

                                 
< af:inputText id="productpriceIT" label="Price" autoSubmit="true"
validator="#{advertisement.checkPrice}" value="#{advertisement.price}" >

validator 方法 checkPrice() 在 advertisement bean 中实现;它会根据两个业务规则检查新值。如果违反规则,将引发异常。这将中断 JSF 生命周期,并向 FacesContext 中添加错误消息:

                                 
public void checkPrice(FacesContext facesContext, UIComponent uIComponent,
Object value) {


System.out.println("CheckPrice invoked - new price value = "+value);

float price = (Float)value;

if (price > 500) {

((EditableValueHolder)uIComponent).setValid(false);
throw new ValidatorException( new FacesMessage (FacesMessage.SEVERITY_ERROR,
"Price should not be over $500", "S.H.O.P. has a policy of not allowing transactions for more
than $500 as to retain a community spirit and prevent abuse"));
}

if (price > 50 && price - (int)price > 0) {

((EditableValueHolder)uIComponent).setValid(false);

throw new ValidatorException(new FacesMessage (FacesMessage.SEVERITY_ERROR,
"Prices over $50 should not carry cents", "Cents are just a marketing trick"));
}
}

如果我们现在运行页面并将 Price 域中的值更改为 $106.95,然后离开此域,将发生 PPR;检查价格;并拒绝价格,如 Price 域周围的红色轮廓所示(参见图 5)。

图 5 由于价格违反业务规则而在服务器中触发的错误消息

在此例中,没有调用 setPrice() 方法,因为 PPR 请求生命周期已经中断 — 在验证失败时,不会执行模型更新。

注:当然,使用一个声明式 validator 更易于演示 PPR 请求中的自动验证步骤。但是,Oracle ADF Faces RC 提供了声明式 validator 的客户端实施,因此框架会使用客户端验证工具即时验证域值,而不是依赖于 PPR 请求中的验证。在 Oracle ADF Faces RC 中,我们无法关闭客户端验证,这与 Oracle ADF Faces 10g 不同。

另一种办法是,我们只让这个 bean 执行必要的调整并发出警告,而不是通过显示表明价格值与我们的业务规则不匹配的错误消息来打扰用户。在以下示例中,setPrice() 方法调用 verifyAndCorrectPrice() 方法,以确保当金额大于 $50 时价格为整数值。通过该解决方案,可以修改先前的 checkPrice() 方法以忽略此规则。

                                 
public void setPrice(float price) {


System.out.println("New value for product price = "+price);

this.price = verifyAndCorrectPrice(price);
}


private float verifyAndCorrectPrice( float price) {

float newprice = price;

if (price > 50 && (price - (int)price)>0) { // price over $ 50 and cents involved

newprice = Math.round(price);

FacesContext.getCurrentInstance().addMessage

(
FacesContext.getCurrentInstance().getViewRoot().findComponent("productpriceIT").getClientId
(FacesContext.getCurrentInstance())

, new FacesMessage(FacesMessage.SEVERITY_WARN, "Prices over $50 are in
straight dollars - no cents","S.H.O.P. regards the use of cents in prices over $50 as an
undesirable marketing gimmick and allows only rounded numbers; the price was corrected from "+
price+ " to " + newprice)

);

}

return newprice;

}


图 6 显示了在运行页面、输入价格值 $106.95 并离开域之后将显示的内容:

图 6 异步自动提交新价格之后,显示服务器生成的警告消息的页面

现在,Price 域周围有一个橙色框,并显示告知用户价格已调整的警告消息。最好是还在 Price 域中显示新的更正后的值,这意味着在 PPR 请求结束后在客户端修改 Price 组件。看一下我们如何指示 Oracle ADF 在 PPR 周期中更新客户端组件。

介绍用于局部刷新 Oracle ADF 页面的 PartialTriggers 属性

autoSubmit 属性用于启动 PPR 请求。组件值的更改将触发框架提交表单并处理更改后的值,对其进行转换、验证和模型更新。到目前为止,这始终是单向:将值提交到服务器。但是,在 Price 域周围首先标记红色框然后标记橙色框,用于指示验证错误和更正警告时,我们就已经了解 PPR 功能。因此,即使我们没有要求,框架也会执行少许 PPR。

要求针对价格组件本身执行 PPR 很简单。我们只需指示框架,每当执行由价格组件的自动提交功能启动的 PPR 请求时,刷新价格组件。Oracle ADF 使用 partialTriggers 属性配置组件以实现局部刷新。此属性可以包含其 PPR 请求应触发组件更新的所有组件的 ID 值。在本例中,我们希望 Oracle ADF 在价格更改时对价格组件本身执行 PPR:
< af:inputText id="productpriceIT" label="Price" autoSubmit="true"
                          validator="#{advertisement.checkPrice}"
                          value="#{advertisement.price}"
                          partialTriggers="productpriceIT" /> 
                                                 

借助该指令,Oracle ADF 将在价格修改时启动 PPR 请求。在处理 PPR 响应时,将刷新 ID 为 productpriceIT 的组件本身。

图 7 导航离开 Price 域前后的页面

图 7 显示了用户导航离开 Price 域前后的页面 — PPR 要求更正价格并发出警告消息。

autoSubmit/partialTriggers 组合不仅适用于更改的组件本身,而且还可以跨组件应用,使相关组件在其相关域更改后进行刷新。例如,显示产品使用时间的 outputText 年龄组件依赖于 Year 域。每当Year(购买年限)域更改,就应刷新年龄组件。借助 autoSubmit(用于 Year)和 partialTriggers(用于 Age),一切都变得十分简单。

                               < af:inputText id="yearIT" label="Year" autoSubmit="true"

                          value="#{advertisement.year}"/>
                                                  
            < af:inputText id="ageIT" label="Age" readOnly="true" partialTriggers="yearIT" 
                        
                          value="#{advertisement.age} years " / >

如果年份更改,年份组件就会启动 PPR 请求。系统将处理 Year 中的新值,并更新模型。在浏览器中接收 PPR 响应时,将刷新在其 partialTriggers 属性中引用年份组件的所有组件。在此示例中,这适用于 Age 域,该域从年份组件派生值。

getAge() 方法在 advertisement bean 中实现,如下所示:

                   public Integer getAge() {
                                
        Short currentYear = Short.parseShort( new SimpleDateFormat("yyyy").format(new java.util.Date()));
                
        return this.getYear() == 0 ? null : currentYear - this.getYear();
    }

我们可以运行页面,输入 Year 的值,并导航离开域。PPR 运行;处理 Year 值;负责更新年龄组件,显示从新的 Year 值派生的年龄值,如图 8 所示。

图 8 PPR 在年数更改之后更新年龄组件

有趣的是,我们看到不仅刷新了页面的局部事务,而且还刷新了在 PPR 请求周期结束后返回到浏览器的响应。使用诸如 Firebug(Firefox 的一个插件)、Fiddler(用于分析 HTTP 流量的免费软件工具)甚至 Oracle JDeveloper 本身的 HTTP 分析器之类的工具,我们可以检查传送到浏览器和接收自浏览器的 PPR 请求和响应消息。PPR 请求主要是提交表单。PPR 响应并不是服务器重新呈现的整个页面,相反,它是仅包含此 PPR 请求周期要点的少量消息,在本示例中,仅意味着年龄组件的新状态(参见图 9):

图 9 局部更新 AGE 的 PPR 响应消息的内容

此微小响应的内容由 Oracle ADF Faces RC 在客户端处理,并用于刷新页面中选定的区域和组件。

我们之前已经看到 Oracle ADF Faces RC 如何仅通过正常生命周期步骤(转换、验证、更新模型、[重新]呈现组件)来处理启动 PPR 请求的组件。随后,我们将对此进行扩展,以重新呈现在其 partialTriggers 属性中引用已更改组件的组件,因为我们看到年份更改后已刷新年龄。实际上不仅仅如此:具有 partialTriggers 引用的组件不仅重新呈现,而且还在整个生命周期内采用。这意味着,如果名称组件的 partialTriggers 引用年份组件,如以下示例所示:

< af:inputText id="nameIT" label="Name" 
                                                        
                          partialTriggers="yearIT"
                                                   
                          value="#{advertisement.name}"/>

当年份更改时,PPR 生命周期不仅将处理新的年份值,还将处理新的名称值。如果服务器发现名称值出现错误或者对该值执行操作,会对 PPR 周期的结果产生影响。

例如,如果我们已实现 setName() 方法,将用户输入的名称值更改为大写,并且用户首先输入产品的名称,导航离开 Name 域(未发生任何事情),然后更改 Year 域中的值,PPR 周期将不仅从年份值派生年龄,而且还会更新产品名称并显示字母全部大写的新值,如图 10 所示。

图 10 从年份值派生年龄还将导致名称大写,因为 Name 域依赖于 Year 组件

您应该关注必要项及其 partialTriggers 依赖项。在我们刚讨论过的示例中,假设名称组件是必需的 (required="true"),并且用户在输入名称值之前输入了年份值(例如 2001),PPR 请求就会因 Name 域的验证错误(参见图 11)而结束,因为 PPR 生命周期(转换、验证、更新模型、[重新]呈现组件)已经针对这两个组件触发:

图 11 由于存在依赖性,更改年份之后名称出现验证错误

使用即时转换为最终用户带来更多便利

我们使用 PPR 的众多方式之一就是实现即时转换。JavaServer Faces 具有可以与输入元素关联的转换器和组件的概念,可以将存储在底层模型中的值转换为该值在浏览器中所对应的可人工读取的表示,反之亦然。例如,我们可以使用 DateTime 转换器设置日期值的格式,或者使用数字转换器显示特定模式的数值。如果我们将转换器与输入组件相关联,将组件上的 autoSubmit 设置为 true,并将组件的 ID 添加到其自己的 partialTriggers 属性中,那么当用户更改该值时,转换器将执行即时转换。

现在,我们将通过在描述组件上配置缩写转换器,来利用这个基于 PPR 的即时转换。此转换器将检查描述,并将一些常用的缩写替换为完整的对应词组。其中,它将出现的“agan”、“sitb”、“dbolo”、“ywli”和“huaa”替换为“as good as new”、“still in the box”、“driven by old lady only”、“you will love it”和“hardly used at all”。

ShortHandConverter 类代码显示如下:
public class ShortHandConverter  implements Converter {


      Map shorthand = new HashMap(20);


      public ShortHandConverter() {


         shorthand.put("agan","as good as new");

          shorthand.put("sitb","still in the box");

          shorthand.put("dbolo","driven by old lady only");

          shorthand.put("ywli","you will love it");

          shorthand.put("huaa","hardly used at all");

          shorthand.put("nrod","no reasonable offer declined"); [[Would it be OK to put all 
    the commas inside the close quotes, which is standard American (not British Commonwealth) 
   punctuation style?]]

         // more shorthand notations

      }


        
      public Object getAsObject(FacesContext facesContext, 

                                UIComponent uiComponent, String string){

        String description = string;     

  for (String abbr:(Set< String>)shorthand.keySet()) {

             description = description.replaceAll(abbr, (String)shorthand.get(abbr));

        }    

        return description;

      }

      public String getAsString(FacesContext facesContext, 
          
                                UIComponent uiComponent, Object object) {
                                                                
        return object.toString();
      }
    }

为了使转换器在 Oracle ADF Faces RC 应用程序中可用,需要使用如下项在 faces-config.xml 文件中进行配置:

 < converter>

   < converter-id >ShortHandConverter< /converter-id>
   
   < converter-class >otn.shop.converters.ShortHandConverter < /converter-class>
   
 < /converter >
现在,我们可以针对描述组件使用转换器,我们还将此组件配置为执行 autoSubmit 和“自动 PPR”:
   < af:inputText id="descIT" label="Description" rows="8"

                autoSubmit="true" partialTriggers="descIT"

                value="#{advertisement.description}">

      < f:converter converterId="ShortHandConverter"/>

  < /af:inputText>

观察 autoSubmit="true" 如何让组件将修改值发送到服务器以进行处理(首先通过 ShortHandConverter 进行转换),以及 partialTriggers="descIT" 如何指示 Oracle ADF 在 PPR 周期结束时刷新 Description 域。

当我们在配置了转换器的情况下运行页面时,可以用较模糊的格式输入广告性产品描述,当我们导航离开 Description 域后,此格式会立即转换为更易读的格式(参见图 12)。

图 12 使用 shorthandconverter 转换产品描述

要查看目前为止所述的功能,请在 JDeveloper 应用程序中运行本文提供的 ProductAdvertisment_step1.jspx 页面。

创建值的级联列表

我们经常在 Oracle ADF Faces RC 应用程序中使用各种类型的列表组件:下拉菜单 (selectOneChoice)、单选按钮组 (selectOneRadio) 或列表框,以及用于多个选择的移动按钮或复选框。在所有这些组件中,我们为用户提供了几个可选择而不是以自由格式输入的允许选项和值。

在许多情况下,允许值集合取决于用户选择。如果应用程序具有国家/地区列表,其提供的国家/地区集合可以通过选择洲或地区进行限制。如果选择汽车品牌,将筛选汽车型号下拉菜单中的选项列表。当然,在用户修改筛选值时,应该立即筛选值列表。

S.H.O.P. 应用程序允许用户从显示所有(相关)产品类别的列表框中选择产品类别。页面还具有产品领域下拉组件。所选的产品领域将筛选相关的产品类别列表。每当产品领域选择更改,就应该刷新产品类别列表。

至此,您应该发现以下步骤很熟悉:

  • product area 下拉菜单自动提交更改后的值。
  • 让提供类别值的 bean 考虑选定的产品领域。
  • product category 列表在 PPR 请求结束时刷新。

页面上的组件配置如下:

      < af:selectOneChoice id="areaSO" label="Area"
          
                                value="#{advertisement.productArea}"
                                                                
                                autoSubmit="true">
                                                                
              < f:selectItems value="#{areaAndCategoryProvider.productAreaSelectItems}"/>
                          
            < /af:selectOneChoice>
                        
            < af:selectOneListbox id="categorySO" label="Product Category"
                        
                                 value="#{advertisement.productArea}"
                                                                 
                                 partialTriggers="areaSO">
                                                                 
              < f:selectItems value="#{areaAndCategoryProvider.productCategorySelectItems}"/>
                          
           < /af:selectOneListbox>

   

组件使用的托管 bean 配置为 areaAndCategoryProvider:

< managed-bean>
                
    < managed-bean-name>areaAndCategoryProvider< /managed-bean-name>
        
    < managed-bean-class>otn.shop.AreaAndCategoryProvider< /managed-bean-class>
        
    < managed-bean-scope>session< /managed-bean-scope>
        
    < managed-property>
        
      < property-name>advertisement< /property-name>
          
      < property-class>otn.shop.ProductPosting< /property-class>
          
      < value>#{advertisement}< /value>
          
    < /managed-property>
        
  < /managed-bean>

基于 otn.shop.AreaAndCategoryProvider 类。该类中最令人感兴趣的方法是 getProductAreaSelectItems() 和 getProductCategorySelectItems(),它们用于返回在领域下拉菜单和类别列表框中选定的项。getProductCategorySelectItems() 方法在 advertisement bean 中使用当前选定的 productArea(如果有)筛选返回的产品类别列表。

 List< String> productAreas = new ArrayList();

    Map < String, List > productCategories = new HashMap< String, List >();

    public SelectItem[] getProductCategorySelectItems() {

        List< String> categories = new ArrayList < String>();

        if (this.getAdvertisement().getProductArea() != null &&

            this.getAdvertisement().getProductArea().length() > 0) {

            categories.addAll(productCategories.get(this.getAdvertisement().getProductArea()));

        } else {

                for (String area : this.productAreas) {

                    categories.addAll(productCategories.get(area));
                }

        }
        SelectItem[] items = new SelectItem[categories.size()];

        int i = 0;

        for (String category : categories) {

            items[i++] = new SelectItem(category, category);

        }

        return items;
    }


请参见用于初始化 productAreas 列表和 productCategories 映射的可下载源代码,此代码使用实际可读值填充了这些结构。

在我们运行页面时,可以从下拉菜单中选择产品领域,并从列表框中选择类别。一旦选择产品领域,产品类别列表框便会相应地刷新,以便仅显示适合选定领域的产品类别(参见图 13)。

图 13 针对所需产品领域进行筛选 — 选择领域将更改类别列表

这是老生常谈的一个简单示例:由其他组件限制的一组允许值。autoSubmit(用于将 PPR 中的新筛选值发送到服务器,并在准备允许值集合时使用相应的逻辑应用筛选值)和 partialTriggers 属性的结合可刷新包含允许值集合的组件。

本文提供的在 JDeveloper 应用程序中使用的 ProductAdvertisment_step2.jspx 页面演示了级联列表特性。

使用 PPR 协调颜色并提供相应的上下文

我们要使 S.H.O.P. 应用程序更鲜活一点。首先,每个产品领域应该有自己的主题颜色,该颜色要应用于包含产品领域和产品类别选择器的框。Garden 产品领域的主题颜色为碧绿色,Toys 为粉红色,Cars 为铬黄色,而 Furniture 为茶色主题。

将颜色应用于组件可在 Oracle ADF Faces RC 中完成(通过在 contentStyle 属性中设置背景颜色)。已设置 contentStyle 的 panelGroupLayout 组件通过 partialTriggers 属性关联到产品领域选择(更改)。
  < af:panelGroupLayout id="areacatPGL" layout="horizontal"
   
                       valign="top" halign="left"
                                           
                       partialTriggers="areaSO"
                                           
                       inlineStyle="backgroundcolor:
                                     #{(advertisement.productArea=='Toys')?'pink'
                                       :(advertisement.productArea=='Garden')?'green'
                                       :(advertisement.productArea=='Cars')?'silver'
                                       :(advertisement.productArea=='Furniture')?'tan':''
                                      };
                                    border-color:Gray; border-width:thin; padding:10px;">
                                                                        
     … content
         
  < /panelGroupLayout>

页面因此看起来更令人感兴趣(参见图 14):

图 14 更改颜色以实现局部页面刷新

此外,更好的是,还显示产品领域附带的小型主题图片。Images 文件夹包含四个图像 — Furniture.jpeg、Cars.jpeg、Toys.jpeg 以及 Garden.jpeg,我们将显示这些图像以反映当前的产品领域。为了达到此目的,我们将使用图像组件。它的 source 属性引用 JPEG 文件,此文件应该使用对选定产品领域的 EL 引用进行显示。partialTriggers 属性设置使该组件在产品领域选择发生改变时进行刷新。

< af:image id="areaIM"

            source="images/#{advertisement.productArea}.jpeg"

            partialTriggers="areaSO"

            inlineStyle="width:150px; border-color:Gray; border-width:2px;"/>

添加此组件会对页面产生以下影响,如图 15 所示:

图 15 添加由 ppr 操作更新的图像组件所带来的影响

按钮或链接可以通过 partialSubmit 触发局部刷新

到目前为止,我们仅讨论了由于更改标记为 autoSubmit 的组件而发送的 PPR 请求。PPR 请求还有另一个触发器:单击命令按钮或操作链接。通常,单击命令链接或按钮将导致整个页面刷新和重载。但是,当我们在按钮或链接上设置 partialSubmit 属性后,此行为将发生改变。将由按钮或链接启动 PPR 请求,而非正常的表单提交或页面导航。在正常的生命周期内,所有表单值均被提交并进行处理。注意,这不同于使用 autoSubmit,后者仅处理更改的组件本身以及在其 partialTriggers 属性中包含引用的所有组件。这还意味着,仅当为表单中的所有必需组件指定值之后,单击设置了 partialSubmit 的按钮或链接才会成功。

假设我们希望让用户能够显示或隐藏我们刚才为了说明产品领域而添加的图像。我们可以提供命令链接,用户可以单击此链接切换图像的可见性。命令链接将 partialSubmit 设置为 true。commandLink 和图像均将刷新,并在其 partialTriggers 属性中包含命令链接的 ID:

< af:commandLink id="showImageCL" 
                            text="#{areaAndCategoryProvider.showAreaImage?'Hide image':'Show image'}" 
                                   
                   actionListener="#{areaAndCategoryProvider.toggleImage}" 
                                   
                   partialSubmit="true" partialTriggers="showImageCL"/>
                                   
   < af:image id="areaIM"
   
             source="images/#{advertisement.productArea}.jpeg"
                         
             visible="#{areaAndCategoryProvider.showAreaImage}"
                         
             partialTriggers="areaSO showImageCL"
                         
             inlineStyle="height:75px; border-color:Gray; border-width:2px;"/>

页面将不会显示图像和 Show image/Hide image 命令链接。仅当选择产品领域后,才会显示“Show image”链接。单击此链接时,会显示相应的图像,并且链接将文本更改为“Hide image”,如图 16 所示。

图 16 show image 和 hide image 命令链接

顺便说明的是,我们可以仅使用客户端方法来更高效地实现此特定功能。此示例用于说明 partialSubmit 可以执行的操作。

要查看所使用的样式操作和该局部提交,请在 JDeveloper 应用程序中运行本文提供的 ProductAdvertisment_step3.jspx 页面。

在 Rich Table 组件中使用 autoSubmit 和 PPR

PPR 的另一个常见用例是计算表中的总数。在列页脚中显示所有行的计数或合计便是一个示例。当然,行级计算也很常用,例如根据单价乘以数量来计算总价。

在表中使用 PPR 与我们到目前为止所看到的用例并没有太大的差别,但却是引入局部触发器编辑器的良机,此编辑器支持选择触发组件并知道如何处理各种命名容器内部的 ID 值。例如,JSF 将向表行中的每个组件添加 :0、:1、:2 等后缀,以便清楚地标识在一列中呈现多次的组件。此外,页面上各种区域中的组件可以使用其外围容器组件的 ID 作为前缀。这意味着,直接引用触发组件的 ID 值(正如我们到目前为止使用的那样)并非始终有效。局部触发器编辑器了解命名容器、ID 创建以及构造 partialTriggers 属性的正确方式。

在图 17 中,页面上列出了 S.H.O.P. 提供的几种产品。表允许编辑表中的 YearPrice 单元格。当年份更改后,该产品列表的 Age 单元格应该执行 PPR。Price 列将显示所有列出产品的平均价格。当任意产品的 Price 单元格修改后,还应该刷新此平均值。


图 17 在修改单元格后刷新列的页脚值

让我们首先看一下 Year 单元格。更改该单元格后,应该执行 PPR 请求以刷新 Age 单元格。这意味着,我们对年份的 inputText 组件设置 autoSubmit="true"。但是,我们应该在 Age 单元格的 partialTriggers 属性中设置什么呢?对所有可能的 ID 值设置为“prodYearCol:0 prodYearCol:1 prodYearCol:2 ….”之类的值?或者,我们应该刷新整个表吗?

如果我们转到 prodAgeOT outputText 组件的 Property 选项板并找到 partialTriggers 属性,就可以从小菜单中打开 partialTriggers 编辑器(单击 PartialTriggers 字段旁边的图标即可打开此菜单)(参见图 18):

图 18 从平均价格组件的属性检查器中打开 partialtriggers 编辑器

此编辑器提供了页面上所有组件的概述,并允许我们针对 partialTriggers 属性选择这些组件。这样,我们就找到触发年龄组件刷新的年份 inputText 组件,并将其添加到选择中。结果发现,在表行内,我们只需使用不带 :0、:1 等后缀的组件 ID 便可以引用其他单元格;框架的智能足以确定应该刷新哪个单元格。

对于平均价格 outputText 也是如此:我们希望每当修改 Price 单元格时就对其进行刷新。同样,使用 PartialTriggers 编辑器,我们可以找到 priceIT 组件并将其添加到 partialTriggers 属性中(参见图 18)。结果发现,即使平均价格仅在表中(而不是在每个单独的表行中)呈现一次,也可以在 partialTriggers 属性中简单地引用 priceIT。

图 19 任何价格更改都会反映在平均价格的 PPR 更新中

如果我们希望表中的更改反映到表组件的范围之外,那么 partialTriggers 中的引用将稍为复杂些。例如,我们希望面板标题也反映平均价格:我们必须将面板标题组件配置如下:

 < af:panelHeader id="productPH" 
                  
              text="Product Postings (average price #{postingsManager.averagePrice})" 
                          
              size="-1"
                          
              partialTriggers="prodlistTable:priceIT">
   
本文提供的 JDeveloper 应用程序中的 ProductAdvertismentsList.jspx 页面就演示了表中执行的局部页面刷新。

使用自动 PPR 自动刷新数据绑定组件



无论以声明方式(通过 autoSubmit 或 partialSubmit 和 partialTriggers)还是编程方式使用 PPR,有时可能需要进行大量配置。不过,生活可以更轻松。我们可以通过在底层迭代器上指定单个属性,来指示 Oracle ADF 为基础值绑定发生更改的所有数据绑定项自动执行 PPR。

您可以针对单独的值绑定将 changeEventPolicy 属性设置为 ppr。这样做意味着,每当关联组件的值因后端业务逻辑而发生更改时,该组件将自动重新呈现。您还可以将迭代器绑定的 changeEventPolicy 设置为 ppr。当您执行此操作后,与迭代器关联的任何操作或值绑定就像将其 changeEventPolicy 设置为 ppr 那样发挥作用。这将使整个表单都能够使用 PPR,而无需单独配置每个组件。

如果您删除表单并选择包括导航控件,Oracle JDeveloper 将自动执行。Oracle JDeveloper 还会将每个导航按钮的 partialSubmit 属性设置为 true。这就是命令组件通知框架执行 PPR 的方式。当您将导航命令组件的 partialSubmit 属性设置为 true 并将迭代器的 changeEventPolicy 设置为 ppr 后,每次单击导航按钮时,表单中使用迭代器绑定的所有组件都将重新呈现。

总结

使用 Ajax 和局部页面刷新是通往具有吸引力、响应能力强的动态用户界面的 Web 2.0 应用程序的途径。Oracle ADF Faces RC 中的 PPR 功能可以轻松用于一系列常见用例。通过简单的声明式设置,我们可以将域值的更改传递到服务器。通过其他声明式设置,我们可以指示 Oracle ADF 刷新客户端的组件。这为今后更多的方案奠定了基础,如值级联列表、显示/隐藏切换、表列总数计算以及即时验证和转换。

在本系列的第 2 部分中,我们将范围扩展到更高级的业务案例,包括其他类型的客户端事件、实现自动提示功能、使用具有上下文相关详细信息的弹出窗口以及与 Google Maps 集成。

本文部分基于 SAGE Computing(澳大利亚)和 AMIS(荷兰)联合开发的 Oracle ADF 11g 培训手册中的一个单元。


Lucas Jellema AMIS(位于荷兰 Nieuwegein)的技术经理,同时还是 Oracle ACE 总监(Oracle 融合中间件)。Lucas 发表了多篇博文,撰写了多本书,并且经常在国际会议和研讨会上发表演讲。他主要从事 Oracle SOA Suite 和 ADF 技术方面的教学和咨询。

Chris Muir (http://one-size-doesnt-fit-all.blogspot.com) 是 Oracle ACE 总监(Oracle 融合中间件)和高级顾问,还是位于澳大利亚的 SAGE Computing Services 的 Oracle 培训师。他们加起来在 Oracle 开发和数据库技术方面有 40 年的经验,借助在 RDBMS、传统 Oracle 开发工具以及 Oracle Application Express、Oracle JDeveloper 和功能良好的旧式 SQL*Plus 方面的多年经验,他们都显示了丰硕的成果。