下载
 Oracle JDeveloper 和 ADF 11g
 示例代码
 
   标签
rea, adf, 全部
 
开发人员:富企业应用程序

在基于 JSF 的富企业应用程序中实现单元格高亮显示(第 2 部分)

作者:Lucas Jellema ACE 总监

在您的 JavaServer Faces 应用程序中,通过实现单元格高亮显示来帮助您的最终用户整理信息。

2008 年 11 月发表

在本文第 1 部分中,您已经用一种非常简单的方法以直观的方式,在 Oracle 应用开发框架 (ADF) Faces 应用程序中体验了单元格高亮显示的实现。如您所了解到的,只需些许努力,Oracle ADF 应用程序就可以拥有高亮显示功能,该功能通常在 Oracle Discoverer、Microsoft Excel 和 Oracle Application Express 等工具中提供。

图
图 1 第 1 部分最终结果 — 指定两个 Salary 阈值用于单元格高亮显示

然而,这样的做法局限性很大:它仅仅支持用户针对预定义数量的阈值动态设置值和颜色。每个值范围由特定颜色标识;例如,低于 2,000 的工资、2,000 到 3,000 之间的工资和 3,000 以上的工资分别显示为绿色、橙色和红色。

在第 2 部分,您将为最终用户添加更多功能:文章描述了如何添加最终用户功能以动态添加、更新和移除所需数量的阈值。您将在同一个表中添加多列高亮显示支持,使 Hiredate 列基于工作经验高亮显示(绿色明显表示出经验较少的),同时 Salary 列的高亮显示指明员工的收入类别。

文章不仅介绍了简单的阈值模式,还提供了基于表达式的筛选。最终用户可以设置一个或多个可能引用数据记录中所有属性的表达式,如 JOB <> 'MANAGER' and SAL > 3000JOB = 'CLERK' or SAL < 1500。每一个筛选表达式都可以和一种颜色相关联。记录满足的第一个表达式将决定单元格的颜色。这就好比执行多条查询与显示结果同时进行。

筹备

您最好已经阅读并实施了第 1 部分的内容。如果您没有,您将需要下载并安装 Oracle JDeveloper 11g ,还需要下载并启动一个 Oracle JDeveloper 启动应用程序。第 1 部分描述了如何针对一个数据库或者一个基于 JavaBeans 的数据存储进行实施。在本部分,您将仅在 Oracle ADF Faces 内实施,而不用考虑模型组件的实施。

  1. 启动 Oracle JDeveloper。
  2. 加载给定的 Oracle JDeveloper 应用程序 ADFCellHighlightingPart2_StartingPoint。它包含一个到达第 1 部分结束点(如上面图 1 所示)的 model-view-controller (MVC) 项目。
Model 项目包含三个类:HrmManager、EmpManager(单体)和 Emp。Emp 和 EmpManager 类从 EMP 表生成,产生一个离线(无数据库)数据源。HrmManager 类作为 DataControl 发布。因此,emps 集合将出现在 Data Control Palette 上。

ViewController 项目包括 EmpTable.jspx 页面、它的相关 PageDefinition 以及 ThresholdHighlightBean 类。

第 1 步:动态添加阈值

在第 1 部分结尾部分,您已允许用户设置每一个阈值的值并指定用于高亮显示的颜色,但是阈值的数量是预设定且固定的。您现在将在该功能基础上实现动态创建和删除阈值。是否将阈值应用于特定列,如果是,阈值数量是多少,这些都由用户来决定。

图
图 2 动态指定单元格高亮显示阈值

托管 Bean 和底层阈值逻辑

通过调用一个托管 Bean 基于 DynamicThresholdManager 类来添加和删除阈值。此 Bean 维护一个阈值列表。一个阈值是一个简单 Bean,包含两个属性:下限和颜色。值超过或等于下限的单元格将用阈值相关颜色来显示,除非该值高于其他阈值的程度远远超过值范围。

DynamicThresholdManager 类含有 addThreshold() 和 removeThreshold() 方法(功能显而易见),以及 applyHighlighting() 方法(应该将所有当前已定义阈值应用到列中的单元格时将调用该方法)。其中每一个方法都作为部分页面呈现循环的一部分进行调用,每一个方法都能使表刷新,或使高亮显示的弹出面板出现在列标题中。

当单元格显示时,它们也需要知道要显示的背景颜色是什么。contentStyle 属性的使用和以前一样,但是现在它包含一个表达式语言 (EL) 表达式,该表达式借助一个托管 Bean 基于单元格内的值和用户指定的所有阈值决定颜色。

Sal 的 inputText 组件的 contentStyle 属性的 EL 表达式如下:

<af:inputText value="#{row.sal}"
        contentStyle="background-color:#{EmpHighlighterController.reset[row.sal].color}">
        ...
</af:inputText>
EmpHighlighterController 是一个基于 ThresholdHighlighterController 类从 HashMap 扩展而来的托管 Bean。此处使用的 EL 表达式使用关键值“reset”、“row.sal”(工资值)和“color”调用 EmpHighlighterController Bean 上的 get(Object key) 方法数次。(因为该方法返回 Bean 自身)。最后一次调用指示 Bean 返回用于单元格背景的颜色。第二次调用(将 row.sal 传入)使 EmpHighlighterController Bean 可以使用当前单元格的 Salary 值,用于决定最后一次调用中的颜色。

ThresholdHighlighterController 类中的 get 方法实施如下:

private static final String reset = "RESET";
    private static final String color = "COLOR";
    public Object get(Object key) {
        if (key instanceof String) {
            String command = (String)key;
            if (reset.equalsIgnoreCase(command)) {
                operands = new ArrayList();
                return this;
            }
            else if (color.equalsIgnoreCase((String)key)) {
                return dynamicThresholdManager.getColor(operands.get(0));
            }
        }
        else {
            operands.add(key);
            return this;
        }
        // return the object itself to allow additional calls to be made to this method
        return this;
    }
它将识别字符串值重置和颜色这两个指示。在“reset”指示中,它将清除操作数列表。当收到“color”指示时,它将调用 dynamicThresholdManager 上的 getColor() 方法。

整个配置中最重要的方法显然是 dynamicThresholdManager 中的 getColor() 方法。该方法将从单元格传入的值和与所有阈值相关联的下限值比较,并且返回该值超过的最低阈值的颜色。下面是概览图:

图
图 3 调用托管 EmpHighlighterController Bean 决定单元格背景颜色。此 Bean 使用 EmpThresholdManager Bean 注入,该 Bean 保存阈值和计算单元格颜色。

adfc-config.xml 中的托管 Bean 配置将 EmpThresholdManager 注入到 EmpHighlighterController 中。
<managed-bean>
    <managed-bean-name>EmpSalThresholdManager</managed-bean-name>
    <managed-bean-class>otn.cellhighlighting.jsf.DynamicThresholdManager</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
</managed-bean>
<managed-bean>
    <managed-bean-name>EmpHighlighterController</managed-bean-name>
    <managed-bean-class>otn.cellhighlighting.jsf.ThresholdHighlighterController</managed-bean-class>
    <managed-bean-scope>request</managed-bean-scope>
    <managed-property>
    <property-name>dynamicThresholdManager</property-name>
    <property-class>otn.cellhighlighting.jsf.DynamicThresholdManager</property-class>
    <value>#{EmpSalThresholdManager}</value>
    </managed-property>
</managed-bean>
然后,对于 Salary 列的每一个单元格,再次使用单元格内的值调用 EmpHighlighterController。EmpHighlighterController Bean 调用储存在其专用 dynamicThresholdManager 属性上的 EmpThresholdManager 中的 getColor() 方法,并将方法的响应返回给单元格。在单元格内,它用于设置 contentStyle 中的背景颜色属性。

用于配置阈值的单元格高亮显示设置面板

用户通过单击列标题中的小图标时显示的弹出面板对阈值进行指定。这在列的 header facet 中进行配置,使用具有与单击动作相关联的 showPopupBehavior 的图像,如下所示:

<af:column sortProperty="sal" sortable="true">
         <f:facet name="header">
           <af:panelGroupLayout layout="horizontal">
             <af:outputText value="#{bindings.emps.hints.sal.label}"/>
<af:image shortDesc="Open Cell Highlight Settings Window"
source="/highlightIcon.gif">
<af:showPopupBehavior popupId=":CellHighlightThresholdSettings"
triggerType="click"/>
</af:image>
</af:panelGroupLayout>
</f:facet>
<af:inputText ...
</af:column>
showPopupBehavior 指的是在页面中的别处定义的弹出。弹出面板和第 1 部分中的实施多少有些不一样,因为它现在需要适应一系列动态阈值。

图
图 4 用于添加(和删除)阈值的弹出面板

在面板内,通常会有初始颜色选择器 — 即使没有定义阈值,也可以指定单元格的颜色。除了基础选择器外,还有一个按钮面板,在该面板中,可以添加新的阈值,还可以应用当前的高亮显示设置。
<af:popup id="CellHighlightThresholdSettings">
       <af:panelWindow title="Highlight Settings for Employees Table"
                       id="cellHighlightingSettingsPanel">
         <af:panelGroupLayout id="thresholdPanel" layout="horizontal" valign="top"
                    partialTriggers="addThreshold">              
           <af:panelLabelAndMessage label="Initial Color"
                                    labelStyle="vertical-align: top;">
             <af:inputColor id="initialColor" chooseId="chooseInitial"
                            value="#{EmpSalThresholdManager.initialColor}"
                            compact="true"/>
<af:chooseColor id="chooseInitial" defaultVisible="false"
lastUsedVisible="true"/>
</af:panelLabelAndMessage>
<af:iterator ... over all thresholds
... enter threshold value and color
</af:iterator>
</af:panelGroupLayout>
<af:panelGroupLayout id="hdbuttongroup" layout="horizontal">
<af:commandButton text="Add Threshhold" partialSubmit="true"
action="#{EmpSalThresholdManager.addThreshold}"
id="addThreshold"/>
<af:commandButton text="Apply Highlighting" partialSubmit="true"
action="#{EmpSalThresholdManager.applyHighlighting}"
id="doHighlight">
<af:setActionListener from="#{'::emptable'}"
to="#{EmpSalThresholdManager.containerToRefresh}"/>
</af:commandButton>
</af:panelGroupLayout>
</af:panelWindow>
</af:popup>
可以在面板中设置的初始颜色存储在 EmpSalThresholdManager Bean 的 initialColor 属性中 — 在这里,它稍后将在 getColor() 方法中使用。

按钮面板中的两个按钮都链接到 EmpSalThresholdManager Bean 中的操作方法上。其中一个添加阈值:

 List<Threshold> thresholds = new ArrayList();
    public String addThreshold() {
        Threshold threshold = new Threshold();
        Double thresholdValue = Double.POSITIVE_INFINITY;
        // if there are already thresholds, take the last one (plus 1) as starting point
        if (getThresholds().size() > 0) {
            thresholdValue =
                    (getThresholds().get(getThresholds().size() - 1).getLowLimit()) +
                    1;
        }
        threshold.setLowLimit(thresholdValue);
        thresholds.add(threshold);
        return null;
    }
       
注意 Add Threshold 按钮如何将 partialSubmit 设置为 true — 也就是说单击按钮启动部分页面刷新循环。由于它的 addThreshold ID 值包含在 thresholdPanel PanelGroupLayout 的 partialTriggers 属性中,因此当单击按钮并且服务器端处理已完成时,ppr 将使高亮显示面板的内容进行更新。刷新后,面板将包含一个新的空阈值。

至于第二个按钮 Apply Highlighting,它调用 EmpSalThresholdManager 上的 applyHighlighting() 方法。它还将通知该 Bean 它需要刷新的容器 ID 是 ::emptable。这条信息通过将此字符串发送到 Bean 上的 containerToRefresh 属性的 setActionListener 传达。

public String applyHighlighting() {
       refreshContainer();
       return null;
   }
Apply Highlighting 很简单:它只需要通知 ADF 在部分页面呈现循环结束时刷新整个表。所有信息都已收集。

refreshContainer 方法的任务是通过编程方式将表添加为部分页面刷新的目标:

private void refreshContainer() {
        AdfFacesContext adfContext = AdfFacesContext.getCurrentInstance();
        UIComponent highlightSettingsPanel =
            FacesContext.getCurrentInstance().getViewRoot().findComponent(getContainerToRefresh());
        if (highlightSettingsPanel != null) {
            adfContext.addPartialTarget(highlightSettingsPanel);
        }
    }
那么,添加更多的阈值后会是什么样?

图
图 5 创建所需数量的阈值用于 Salary 单元格高亮显示

这是如何实施在页面上的 — 哪一个 JavaServer Faces (JSF) 组件负责显示这些阈值控制?

对于每一个阈值,您需要一个 inputText 来输入阈值,还需要一个 inputColor 组件和一个 Delete 命令链接来帮助用户删除阈值。您还需要迭代所有已定义阈值。下面是以上操作的 JSF 代码:

<af:iterator value="#{EmpSalThresholdManager.thresholds}"
             var="threshold" varStatus="rowStatus">
  <af:panelGroupLayout layout="vertical">
    <af:panelLabelAndMessage label="Threshold #{rowStatus.index+1}"
                             labelStyle="vertical-align: top;">
      <af:panelGroupLayout layout="horizontal">
        <af:inputText value="#{threshold.lowLimit}" columns="6">
          <f:convertNumber pattern="####.##" type="number"/>
</af:inputText>
<af:commandLink id="deleteThreshold"
action="#{EmpSalThresholdManager.removeThreshold}"
partialSubmit="true">
<af:image source="/delete.jpeg"
inlineStyle="height:15px;"/>
<af:setActionListener from="#{rowStatus.index}"
to="#{EmpSalThresholdManager.deletedThresholdIndex}"/>
<af:setActionListener from="#{'cellHighlightingSettingsPanel'}"
to="#{EmpSalThresholdManager.containerToRefresh}"/>
</af:commandLink>
</af:panelGroupLayout>
<af:spacer height="20"/>
<af:inputColor id="sic1" chooseId="chooseThresholdColor"
value="#{threshold.color}" compact="true"/>
<af:chooseColor id="chooseThresholdColor"
defaultVisible="false"
lastUsedVisible="true"/>
</af:panelLabelAndMessage>
<af:spacer width="10" height="10"/>
</af:panelGroupLayout>
</af:iterator>
af:iterator 组件迭代 EmpSalThresholdManager.thresholds 集合中的所有元素。每一个阈值元素的必要组件都被删除。删除阈值的命令链接调用 EmpSalThresholdManager Bean 上的 removeThreshold() 方法。它传递当前阈值的索引号以便 Bean 知道要删除的阈值,同时还传递 panelWindow 的标识符以便在阈值被删除后,面板可以立即刷新:
public String removeThreshold() {
        thresholds.remove(getDeletedThresholdIndex().intValue());
        refreshContainer();
        return null;
    }

将适合的颜色应用到单元格

现在,我们只缺少一个元素。我们可以添加、编辑和删除阈值。我们可以打开和隐藏高亮显示面板,并应用高亮显示设置。然而,我们还没有讨论根据单元格内的值决定某一单元格背景颜色的 getColor() 方法。

contentStyle="background-color:#{EmpHighlighterController.reset[row.sal].color}"
由 EmpHighlighterController Bean(当它收到单元格值后会立即收到“color”指示)来决定 EmpSalThresholdManager 上的 getColor() 调用:
public String getColor(Object fieldvalue) {
        String color = initialColor();
        //TODO: make sure that the thresholds are in an ascending order
        for (Threshold threshold : getThresholds()) {
            // if this threshold is below the current row value, the previous threshold's color is the one to return
            if (isInsideThreshold(fieldvalue, threshold.getLowLimit())) {
                break;
            }
            color = getColorString(threshold.getColor());
        } // for
        return color;
    }
getColorString() 方法是一个辅助程序方法,它将 AWT Color 对象转变为用于层叠样式表 (CSS) 语言的 RGB 字符串表示:
protected String getColorString(Color rgbColor) {
        return rgbColor==null?"white":"rgb(" + rgbColor.getRed() + "," + rgbColor.getGreen() + "," +
            rgbColor.getBlue() + ")";

    }
调用 isInsideThreshold 方法确定 fieldvalue 是否低于以下阈值:
public boolean isInsideThreshold(Object value, Double thresholdValue) {
    return (thresholdValue == null) || ((Double)value < thresholdValue);
}
在这种情况下,将基于值和 thresholdValue 将是加倍值的假设来编写该方法。在这种情况下,此方法将不支持含有基于字符串、日期或自定义类型的值的单元格。但是对于工资,它将正常运行。

算法的简单总结:单元格传入它的值,getColor() 方法迭代所有阈值直到找到高于单元格值的第一个阈值,然后单元格将会使用低于该阈值的阈值颜色高亮显示。

图
图 6 完成对阈值数量、值和高亮显示颜色的控制

注:这里没有提到的假设是用户将以递增的方式创建阈值。理想情况下,DynamicThresholdManager 类中的代码应该确保阈值列表中的阈值按照升序排列。我将把它作为练习(您一定知道接下来怎么做)留给读者。

第 2 步:多列高亮显示

经过我们前面所做的设置(两个托管 Bean、一个有内容的 panelWindow 和一个单元格的 inputText 上的 contentStyle 属性),现在实现表中第二列单元格高亮显示应该很简单了。您现在将使用户能够根据基于日期的阈值高亮显示 Hiredate 单元格。

在下面的图 7 中,1982 年 1 月 1 日之前和之后的 Hiredate 单元格分别被高亮显示。同一记录中的 salary 单元格由阈值 2100 和 4000 来划分高亮显示颜色。此例中,颜色编码使您能够快速找到经验较少却拿中等或高等工资的员工。

图
图 7 多列单元格同时高亮显示

使用绑定任务流准备重用

我们将看到我们如何将我们在前面步骤里对 EmpTable 页面所做的修改作为可重用的高亮显示设置控制板的基础使用 — 借助 Oracle ADF Faces 绑定任务流实施。当您实施完此任务流后,您可以将它再次嵌入到多个不同页面的多个列中。您还可以非常快速地为 Hiredate 列添加单元格高亮显示。接下来,您只需创建并配置两个非常简单的托管 Bean 用于支持逻辑。

包含 panelWindow(您已经添加用于支持 Salary 列单元格高亮显示)的弹出窗口并不是该列特有的。您可以针对其他列(如 Hiredate 列)使用一个非常相似的弹出窗口。您只需进行少量更改 — 例如适应日期值而不是数值阈值。但是,页面很快会变得混乱,带来了没有必要的长期维护需求。您可以轻松创建一个可重用的组件,这将让我们在长期的运行中节省很多工作,并且防止在不久的将来就造成代码丑陋的尴尬。

Oracle ADF 11g 引入了任务流:用于创建可重用组件的机制。绑定任务流允许您创建可以在其他页面内作为嵌入式组件重用的组件 — 这正是我们所需要的。绑定任务流使用 af:region 标记嵌入。当我们使用绑定任务流时,EmpTable.jspx 页面中的弹出窗口将变成如下所示:

<af:popup id="CellHighlightThresholdSettings">
      <af:panelWindow title="Highlight Settings for Employees Table"
                         id="cellHighlightingSettingsPanel">
           <af:region value="#{bindings.cellhighlightthresholdspanel1.regionModel}"
                      id="cellh1"/>
</af:panelWindow>
</af:popup>
在 EmpTable 页面的 Page Definition 中,指定通过 regionModel 传递给绑定任务流的输入参数。通过这些输入参数,您能够根据其使用的特定环境调整任务流的内容和行为。

创建绑定任务流:可重用的 CellHighlightThresholdsPanel

现在,执行以下步骤:

  1. 创建一个新的名为 cellhighlight-thresholdspanel 的绑定任务流。

    图
    图 8 创建绑定任务流 cellhighlight-thresholdspanel

  2. 指定参数 thresholdManager 传递特定的 DynamicThresholdManager Bean 用于管理阈值,并指定 tableWithHighlightingBinder 传递对 Rich Table 的引用,在本任务流实例中高亮显示针对 Rich Table 定义。

    图
    图 9 为绑定任务流指定参数(名称、类和位置)

  3. 向任务流中添加一个单独的 View 活动,起名为 cellhighlightThresholdsPanel。

    图
    图 10 添加 cellHighlightThresholdsPanel View 活动到绑定任务流

  4. 双击任务流可视编辑器中的 View 活动图标,编辑 cellhighlightThresholdsPanel .jsff 片段。执行以下步骤:
    1. 将 EmpTable.jspx 中 cellHighlightingSettingsPanel panelWindow 的全部内容复制到到 cellhighlightThresholdsPanel .jsff 中的 jsp:root 标记。
    2. 将所有 EmpSalThresholdManager(Salary 列特有的 Bean)替换为 pageFlowScope.thresholdManager(传入任务流的 Bean 引用)。
    3. 修改 doHighlight 命令按钮上的 setActionListener 元素;它使用从嵌入页面传入任务流的 TableBinder 参数传递对 RichTable 的引用以刷新最新的高亮显示设置:
      <af:setActionListener from="#{pageFlowScope.tableWithHighlightingBinder.tableWithHighlighting}"
                                  to="#{pageFlowScope.thresholdManager.tableWithHighlighting}"/>
      

将绑定任务流嵌入到 EmpTable 页面中

  1. 清除内嵌在 EmpTable.jspx 页面弹出窗口中的 panelWindow cellHighlightingSettingsPanel 的内容。
  2. 从 Application Navigator 中,将绑定任务流 cellhighlight-thresholdspanel 拖到 EmpTable.jspx 的 PanelWindow 中。

    图
    图 11 将绑定任务流拖到弹出窗口的 PanelWindow 中

    现在,可视编辑器将显示弹出窗口的内容,如下所示:

    图
    图 12 放入绑定任务流后的弹出窗口

    注意 af:region 元素已创建 — 它是 Page Definition 文件(描述您之前创建的绑定任务流 cellhighlight-thresholdspanel 的使用)中的 taskFlow 元素 cellhighlightthresholdspanel1 的一个引用。
  3. 最后一步是,配置从这一代表 Salary 列的使用传入绑定任务流的输入参数。转到页面 EmpTablePageDef.xml 的 Page Definition 编辑器。

    图
    图 13 在 EmpTable 的 Page Definition 中编辑 taskFlow(使用)

    单击 taskFlow 可执行文件的 Edit 图标。现在,提供 EL 表达式来设置来两个输入参数的值。此例中的 thresholdManager Bean 是您之前使用并查看过的 #{EmpSalThresholdManager}。另一个参数 tableWithHighlightingBinder 是新的。该方法将对 RichTable 组件的引用传递到任务流,并使任务流能够在稍后的部分页面刷新中刷新该组件。(当按下 Apply Highlighting 按钮时)。

    图
    图 14 指定传入任务流的输入参数的值

    在绑定任务流内,并不知道任务流恰好嵌入的页面中存在 emptable RichTable。所以,您需要就此表来通知任务流刷新。传递该信息的方法是:

    1. 创建一个带有一个属性及其访问器的类 TableBinder:
      package otn.cellhighlighting.jsf;
      import oracle.adf.view.rich.component.rich.data.RichTable;
      public class TableBinder {
        private RichTable tableWithHighlighting;
      
    2. 为 emptable 配置一个托管 Bean:
      <managed-bean>
          <managed-bean-name>EmpTableBinder</managed-bean-name>
          <managed-bean-class>otn.cellhighlighting.jsf.TableBinder</managed-bean-class>
          <managed-bean-scope>session</managed-bean-scope>
      </managed-bean>
      
    3. 并将 emptable 绑定到此 Bean:
      <af:table id="emptable" ....
                        binding="#{EmpTableBinder.tableWithHighlighting}">
      
    经过这些步骤,希望您现在已经对 doHighlight 按钮上的 setActionListener 有所了解:它将 Richtable 引用传递到 thresholdManager。
    <af:commandButton text="Apply Highlighting" partialSubmit="true"
                          action="#{pageFlowScope.thresholdManager.applyHighlighting}"
                          id="doHighlight">
          <af:setActionListener from="#{pageFlowScope.tableWithHighlightingBinder.tableWithHighlighting}"
                                to="#{pageFlowScope.thresholdManager.tableWithHighlighting}"/>
    </af:commandButton>
    
    DynamicThresholdManager 类进行了少量更改以适应此方法:
    public class DynamicThresholdManager {
        private RichTable tableWithHighlighting;
    ...
           public String applyHighlighting() {
            refreshTableWithHighlighting();
            return null;
        }
        private void refreshTableWithHighlighting() {
            AdfFacesContext adfContext = AdfFacesContext.getCurrentInstance();
            adfContext.addPartialTarget(this.tableWithHighlighting);
        }
        public void setTableWithHighlighting(RichTable tableWithHighlighting) {
            this.tableWithHighlighting = tableWithHighlighting;
        }
    ...
    

    图
    图 15 如何结合在一起:列具有一个图标,单击该图标后会显示一个包含 panelWindow 的弹出窗口,其中包括引用由页面片段组成的绑定任务流的区域。

现在当您运行 EmpTable 页面时,您应该注意到在 EmpTable.jspx 页面自身中指定弹出内容时和之前是没有什么区别的。

受益:重用时间

目前为止,我们还没有实现任何新功能。我们可以运行 EmpTable 页面,结果与我们在第 1 步结尾处得到的结果相同。但是,我们还可以设置为重用。向 Hiredate 列添加单元格高亮显示现在已经变得非常简单了。到这里设置 Hiredate 列的工作才真正开始。

要支持 Hiredate 列的单元格高亮显示,列标题中需要包含一个激活方式与 Salary 列相同的弹出窗口图标。因此,直接复制 Salary 列的 header facet 并粘贴到 Hiredate 列中。然后修改 facet:

<f:facet name="header">
        <af:panelGroupLayout layout="horizontal">
                <af:outputText value="#{bindings.emps.hints.hiredate.label}"/>
<af:image shortDesc="Open Cell Highlight Settings Window"
source="/highlightIcon.gif">
<af:showPopupBehavior popupId=":HiredateCellHighlightThresholdSettings"
triggerType="click"/>
</af:image>
</af:panelGroupLayout>
</f:facet>
请注意两个 facet 定义中细小但很重要的改变:hiredate 标签应该在 outputText 中使用,showPopupBehavior 中的 popupId 引用应该是 HiredateCellHighlightThresholdSettings(此弹出还未被创建)。

并将 contentStyle 属性添加到 Hiredate 的 inputText 中;此属性包含从 EmpHiredateHighlighterController 中检索的单元格背景颜色的表达式:

<af:inputText value="#{row.hiredate}"
      contentStyle="background-color:#{EmpHiredateHighlighterController.reset[row.hiredate].color}">
      ...
      <f:convertDateTime pattern="#{bindings.EmpManageremps.formats.hiredate}"/>
</af:inputText>
</af:column>
添加以下弹出元素:
<af:popup id="HiredateCellHighlightThresholdSettings">
          <af:panelWindow title="Highlight Settings for Hiredate in Employees Table"
                          id="hiredateCellHighlightingSettingsPanel">
          </af:panelWindow>
</af:popup>
现在从应用程序导航器中拖出绑定任务流 cellhighlight-thresholdspanel 并放到 panelWindow 中。提供任务流输入参数的值:

图
图 16 设置传递给任务流用于 Hiredate 的参数值

为此任务流引用创建的 EmpTable 的 PageDefinition 文件中的条目如下:
<taskFlow id="cellhighlightthresholdspanel2"
              taskFlowId="/WEB-INF/cellhighlight-thresholdspanel.xml#cellhighlight-thresholdspanel"
              xmlns="http://xmlns.oracle.com/adf/controller/binding">
      <parameters>
        <parameter id="thresholdManager" value="#{EmpHiredateThresholdManager}"
                   xmlns="http://xmlns.oracle.com/adfm/uimodel"/>
<parameter id="tableWithHighlightingBinder" value="#{EmpTableBinder}"
xmlns="http://xmlns.oracle.com/adfm/uimodel"/>
</parameters>
</taskFlow>

支持 Bean 和类

我们之前使用的 DynamicThresholdManager 类仅支持它的 isInsideThreshold 方法中的加倍值。您需要将该类扩展为也可以处理日期值。通过 dateValue 属性扩展 Threshold 类。创建一个名为 EmpHiredateThresholdManager 的新类,通过覆盖两个方法来支持日期值:

public class EmpHiredateThresholdManager extends DynamicThresholdManager {
    public EmpHiredateThresholdManager() {
    }

    public boolean isInsideThreshold(Object value, Threshold threshold) {
        long thresholdDate = threshold.dateValue.getTime();
        long valueDate = ((Timestamp)value).getTime();
        return (thresholdDate > valueDate);
    }


    public String addThreshold() {
        Threshold threshold = new Threshold();
        Date thresholdValue = new Date();
        // if there are already thresholds, take the last one (plus 1) as starting point
        if (getThresholds().size() > 0) {
            thresholdValue =
                    super.getThresholds().get(super.getThresholds().size() -
                                              1).getDateValue();
        }
        threshold.setDateValue(thresholdValue);
        thresholds.add(threshold);
        return null;
    }
}
要在 Hiredate 列中支持单元格高亮显示,您只需要在 adfc-config.xml 中配置两个新的托管 Bean:
<managed-bean>
    <managed-bean-name>EmpHiredateThresholdManager</managed-bean-name>
    <managed-bean-class>otn.cellhighlighting.jsf.EmpHiredateThresholdManager</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
</managed-bean>
<managed-bean>
    <managed-bean-name>EmpHiredateHighlighterController</managed-bean-name>
    <managed-bean-class>otn.cellhighlighting.jsf.ThresholdHighlighterController</managed-bean-class>
    <managed-bean-scope>request</managed-bean-scope>
    <managed-property>
    <property-name>dynamicThresholdManager</property-name>
    <property-class>otn.cellhighlighting.jsf.DynamicThresholdManager</property-class>
    <value>#{EmpHiredateThresholdManager}</value>
    </managed-property>
</managed-bean>
就是这样。您可以运行 EmpTable 页面,并且可以像设置 Salary 一样打开弹出窗口来指定 Hiredate 列的单元格高亮显示设置:

图
图 17 现在您可以使用弹出窗口来指定 Hiredate 列和 Salary 列的高亮显示设置。

大致就是这样。Hiredate 阈值明显是日期而不是数值。因此输入日期时,您应该使用 inputDate 组件而不是普通的 inputText。最后一步是,使绑定任务流知道您使用的阈值类型,这样它才能通过显示正确的输入组件进行响应。

首先,向 Threshold Bean 中添加 dateValue 属性。然后,在绑定任务流上指定一个新的输入参数。此参数名为 thresholdDataType,用于根据任务流为其维护阈值的列的数据类型传递含有值“number”或“date”的字符串。

打开编辑器中 View 活动 cellhighlightThresholdsPanel 的页面片段。添加一个转换器组件来处理不同的 thresholdDataType 值:

<af:switcher facetName="#{pageFlowScope.thresholdDataType}"
                         defaultFacet="number">
              <f:facet name="number">
                <af:inputText value="#{threshold.lowLimit}" columns="6">
                  <f:convertNumber pattern="####.##" type="number"/>
</af:inputText>
</f:facet>
<f:facet name="date">
<af:inputDate value="#{threshold.dateValue}" >
<af:convertDateTime pattern="d-MMM-yyyy"/> 
</af:inputDate>
</f:facet>
</af:switcher>
转换器检查计算 thresholdDataType 输入参数值的 #{pageFlowScope.thresholdDataType} 的值。该值用于选择转换器显示。当无传递值时,默认值为“number”。当输入参数的值为“date”时,名称为“date”的 facet 就会显示 —“date”就是您将赋给 Hiredate 列任务流的值。

转到文件 EmpTablePageDef 并用 id cellhighlightthresholdspanel2 编辑 taskflow 元素。添加参数元素。

<parameter id="thresholdDataType" value="#{'date'}"
                   xmlns="http://xmlns.oracle.com/adfm/uimodel"/>
现在,您为 Hiredate 列创建了一个新的弹出窗口,您可以打开该弹出窗口编辑 Hiredate 单元格高亮显示阈值。

图
图 18 使用 InputDate 组件管理 Hiredate 列的阈值

现在,用户可以通过配置两个托管 Bean、放入一个区域和在 JSF 组件属性上添加一些片段来设置 Hiredate 和 Salary 的阈值。

将基于阈值的单元格高亮显示添加到其他列的操作现在变得非常简单。您可以自己尝试一下,例如,为 Deptno 列添加高亮显示。步骤如下:

  1. 配置两个托管 Bean。
  2. 创建一个带有子 panelWindow 的弹出窗口。
  3. 为列标题添加一个图标,包含用于链接图标和弹出窗口的 showPopupBehavior。
  4. 将绑定任务流拖到弹出窗口中的 panelWindow 并指定两个输入参数。
  5. 设置单元格 inputText 组件上的 contentStyle 属性。

第 3 步:基于表达式的高亮显示

最后,添加基于表达式的高亮显示。可以基于在一条记录中引用所有属性(甚至包括未显示的属性)的用户定义公式来高亮显示单元格。这使您能够清晰地标记哪些员工是 SALESMAN 并且工资超过 1500(使用表达式 [sal]> 1500 && [job]=='SALESMAN'),或者谁在部门 20 工作、职位是 CLERK 或 ANALYST 并且工资不到 2500(表达式: [deptno] ==20 && ([job]== 'CLERK' or [job]== 'ANALYST') && [sal] < 2500)。

图
图 19 基于表达式的单元格高亮显示:应用到 Ename 列的两个表达式

对记录中属性的引用包含带方括号的属性名称,例如 [sal] 和 [job]。在表达式被解析前,属性引用由其值的字符串表示所代替。结果字符串由 JSF EL 求值程序来计算。在表达式中,可以使用所有 JSF 表达式语言支持的运算符(例如 +、,、<、!=、==、and、or、())以及对托管 Bean 的引用和三重表达式。

我们可以添加一个让日期值更有意义的小功能,ExpressionManager 按照固定的 DD-MM-YYYY 格式翻译日期,并且还支持将年或月从日期中提取出来的函数。下列可笑的表达式仍然有效:

[hiredate].month==1?[job]=='CLERK' : [hiredate].year < 1982 and [sal]>2800
...这表示当该员工是在一月入职或 1982 年之前入职且工资高于 2800 时,表达式结果应该为 true(单元格将会高亮显示)。无论相信与否,符合这个条件的有五个人。

图
图 20 在基于日期的表达式中使用特殊的日期运算符(月和年运算符)

注:您可能想要添加额外的 ViewObject 属性(例如 ManagerJob、NumberOfSubordinates 或 SalaryRankInDepartment)在表达式中使用。

经过您之前对基于阈值的多列单元格高亮显示所进行的操作,您已经为您现在构建一个基于表达式的高亮显示打下了坚实的基础。

创建新的类并配置托管 Bean

您用来保存阈值并实施 getColor() 方法(决定每个单元格颜色)的 DynamicThresholdManager 类需要进行扩展以处理表达式。要实现这一目的,需创建从 DynamicThresholdManager 中扩展出来的 otn.cellhighlighting.jsf.DynamicHighlightExpressionManager 类。

向字符串类型的 Threshold Bean 添加一个属性 expression

addThreshold() 方法在 DynamicHighlightExpressionManager 中被覆盖。注意如何使表达式的初始设置为 false — 一个永远不会得出 true 结果的有效 EL 表达式。

public String addThreshold() {
        Threshold threshold = new Threshold();
        threshold.setExpression("false");
        thresholds.add(threshold);
        return null;
    }
同时覆盖 getColor() 方法来检查表达式某一行的运算结果,而不是处理阈值和单一的属性值:
public String getColor(Object input) {
        // check whether the input (a Row object)
        // confirms with one of the expressions
        // if so, the color associated with the expression is returned
        String color = null;
        for (Threshold threshold : getThresholds()) {
            if (evaluateExpression(threshold.getExpression(), (Row)input)) {
                color = getColorString(threshold.getColor());
                return color;
            }
        }
        return null;
    }
实际表达式“magic”包含在 evaluateExpression(String 表达式,Row 行)方法中。此方法将每一个属性占位符 ([attribute name]) 替换为当前行中该属性的实际值。然后结果表达式计算结果为 true 或 false。结果 Boolean 从方法返回。

图
图 21 用表达式高亮显示决定单元格颜色。该行被传送到 HighlightController Bean,它计算表达式并为第一个结果为 true 的表达式返回颜色。

evaluateExpression 中的大多数代码行都需要处理多种数据类型的属性。此方法最鲜明的本质是仅处理长值,如下所示:
    private boolean evaluateExpression(String expression, Row row) {
        // replace [attribute name] with 'value of the attribute in the row'
        if (expression != null && expression != "") {
            String exp = expression;
            // iterate over all attributes in row; make for each the suitable replacement
            int i = 0;
            for (String att : row.getAttributeNames()) {
                AttributeDef attdef =
                    row.getStructureDef().getAttributeDef(i++);
                if (attdef.getJavaType().getName().equals("java.lang.Long")) {
                    Long value = (Long)row.getAttribute(attdef.getName());
                    if (value != null)
                        exp =
exp.replace("[" + attdef.getName() + "]", value.toString());
                    else
                        exp =
exp.replace("[" + attdef.getName() + "]", "8747436436");
                }
                if (attdef.getJavaType().getName().equals("oracle.jbo.domain.Date")) {
                    oracle.jbo.domain.Date value =
                        (oracle.jbo.domain.Date)row.getAttribute(attdef.getName());
                    if (value != null) {
                        java.text.SimpleDateFormat displayDateFormat =
                            new java.text.SimpleDateFormat("dd-MM-yyyy");

                        exp =
exp.replace("[" + attdef.getName() + "]", displayDateFormat.format(value.dateValue()));
                    } else
                        exp =
exp.replace("[" + attdef.getName() + "]", "8747436436");
                }
                ...support for other data types such as String, Double, Date & Timestamp, Number
            }
            return (Boolean)getExpressionValue("#{" + exp + "}");
        }
        return false;
    }
辅助程序方法 getExpressionValue 对包含 EL 表达式的字符串进行实际运算,并返回运算结果:
public Object getExpressionValue(String el) {
    FacesContext ctx = FacesContext.getCurrentInstance();
    Application app = ctx.getApplication();
    Object result = app.evaluateExpressionGet(ctx, el, Object.class);
    return result;
}
结果是,即使当您将 Oracle ADF 集合作为表放到页面上时 Oracle JDeveloper 调用了变量行,在表组件中使用的 Oracle ADF 集合模型公开的变量也不是一个 Row 对象。
  <af:table id="emptable"
      value="#{bindings.emps.collectionModel}" var="row"
      ...
您需要一些逻辑来获取 evaluateExpression 方法需要的 Row。为此,修改 ThresholdHighlighterController 类方法中的 get()。添加对 FacesCtrlHierNodeBinding 类型(集合模型上循环中变量的类型,真实 Row 可以从这里提取)密钥的支持:
public Object get(Object key) {
    if (key instanceof FacesCtrlHierNodeBinding) {
        FacesCtrlHierNodeBinding ding = (FacesCtrlHierNodeBinding)key;
        operands.add(ding.getRow());
    } else if (key instanceof String) {
   ...
两个托管 Bean 的配置必须支持 Ename 的基于表达式的单元格高亮显示。一个是 ExpressionManager,另一个是 HighlighterController:
<managed-bean>
  <managed-bean-name>EmpEnameExpressionManager</managed-bean-name>
  <managed-bean-class>otn.cellhighlighting.jsf.DynamicHighlightExpressionManager</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>
<managed-bean>
  <managed-bean-name>EmpEnameHighlighterController</managed-bean-name>
  <managed-bean-class>otn.cellhighlighting.jsf.ThresholdHighlighterController</managed-bean-class>
  <managed-bean-scope>request</managed-bean-scope>
  <managed-property>
  <property-name>dynamicThresholdManager</property-name>
  <property-class>otn.cellhighlighting.jsf.DynamicThresholdManager</property-class>
  <value>#{EmpEnameExpressionManager}</value>
  </managed-property>
</managed-bean>
对日期值的额外支持(年和月的运算符)通过 evaluateExpression 方法中的一小段额外代码来实现:
if (attdef.getJavaType().getName().equals("java.sql.Timestamp")) {
     Date value = 
         (Date)row.getAttribute(attdef.getName());
     if (value != null) {
         exp = 
         exp.replace("[" + attdef.getName() + "].month", monthDateFormat.format(value));
         exp = 
         exp.replace("[" + attdef.getName() + "].year", yearDateFormat.format(value));
         exp = 
         exp.replace("[" + attdef.getName() + "]", "'"+displayDateFormat.format(value)+"'");
     } else
         exp = 
 exp.replace("[" + attdef.getName() + "]", "'8747436436'");
 }
where the DateFormat variables are class-level constants:
  private static java.text.SimpleDateFormat displayDateFormat = 
             new java.text.SimpleDateFormat("dd-MM-yyyy");
  private static java.text.SimpleDateFormat yearDateFormat = 
             new java.text.SimpleDateFormat("yyyy");
  private static java.text.SimpleDateFormat monthDateFormat = 
             new java.text.SimpleDateFormat("MM");

JSF 页面的更改

绑定任务流需要支持表达式。我们将以下 facet 添加到我们之前引入的转换器组件中:

            
<f:facet name="expression">
   <af:inputText value="#{threshold.expression}" rows="3" />                
</f:facet>
这意味着如果传入绑定任务流的 thresholdDataType 输入参数值等于“expression”,那么从 Threshold Bean 中的表达式属性中设置和获取值的三行 inputText 将显示。

表达式自身不会有初始颜色;颜色仅在表达式结果在某一行为 true 时才会被应用。生成的初始颜色属性 panelLabelAndMessage 设置为仅在 datatype 不等于“expression”时显示颜色。

<af:panelLabelAndMessage label="Initial Color#{pageFlowScope.thresholdDataType}"
                   labelStyle="vertical-align: top;"
                   rendered="#{pageFlowScope.thresholdDataType==null or pageFlowScope.thresholdDataType!='expression'}">
现在名为“Add Threshold”的按钮上的标签也要符合表达式:
<af:commandButton text="Add #{pageFlowScope.thresholdDataType=='expression'?'Expression':'Threshhold'}" partialSubmit="true"
                  action="#{pageFlowScope.thresholdManager.addThreshold}"
                  id="addThreshold"/>
在 EmpTable.jspx 页面上,我们需要添加一个带有 panelWindow 的弹出元素,并再次将绑定任务流拖放到该 panelWindow 中。页面中的代码如下:
<af:popup id="EnameCellHighlightThresholdSettings">
          <af:panelWindow title="Highlight Settings for Ename in Employees Table"
                          id="enameCellHighlightingSettingsPanel">
            <af:region value="#{bindings.cellhighlightthresholdspanel3.regionModel}"
                       id="cellh3"/>
</af:panelWindow>
</af:popup> 
Ename 列的列标题引用此弹出窗口:
<af:column sortProperty="ename" sortable="true">
            <f:facet name="header">
              <af:panelGroupLayout layout="horizontal">
                <af:outputText value="#{bindings.emps.hints.ename.label}"/>
<af:image shortDesc="Open Cell Highlight Settings Window"
source="/highlightIcon.gif">
<af:showPopupBehavior popupId=":EnameCellHighlightThresholdSettings"
triggerType="click"/>
</af:image>
</af:panelGroupLayout>
</f:facet>
要使 EmpEnameHighlighterController 生成的颜色真正应用到单元格高亮显示中,您需要为 Ename inputText 组件指定 contentStyle 属性:
<af:inputText value="#{row.ename}" simple="true"
     contentStyle="background-color:#{EmpEnameHighlighterController.reset[row].color}"
完成这些更改后,您可以运行 EmpTable.jspx。可以看到 Ename 列现在也有高亮显示图标了。将光标放到该图标上将会显示高亮显示设置面板,此时用于指定表达式。

结论

在这个关于 Oracle ADF 单元格高亮显示的系列(两篇)文章的第一篇中,您已经了解了如何快速地向 Oracle ADF Faces 数据表中添加静态高亮显示。使用 EL 表达式让高亮显示变得简单。之后,您扩展了该功能,为最终用户提供了更多控制:通过一个托管 Bean,您使两个阈值和相关联颜色动态化,还向用户提供了一个可以操作这些设置的面板,并将此面板嵌入到一个仅在用户需要时显示的弹出窗口中,从而创建了一个更友好的用户界面。

第 2 部分通过几种方式扩展高亮显示功能。我们在同一个表中添加了对多列高亮显示的支持,使 Hiredate 列高亮显示反映工作经验(绿色为经验较少的),使 Salary 列指明员工的收入类别,同时还实现了动态阈值定义:用户可以添加所需数量的阈值,而首次安装中只支持两个。最后,最令人感兴趣的是您添加了基于表达式的高亮显示。可以基于在一条记录中引用所有属性(甚至包括未显示的属性)的公式来高亮显示单元格。

您所做的这些工作可以归结为几个非常简单的 Java 类和一个很普通的绑定任务流。要将单元格高亮显示(基于阈值的高亮显示和基于表达式的高亮显示)添加到您自己的应用程序中,您只需要在您的项目中添加这些类和绑定任务流,并配置所需的托管 Bean。您应该在几分钟内就可以完成在任意 Oracle ADF 表的某一列中对此功能的添加。

一如往常,改进当前成果的方法有几种。一些显著的改进:

  • 跨会话持续存储阈值(当用户希望反复使用同一颜色配置时,需要以永久方式存储阈值和/或表达式的定义,并使这些定义在以后的会话中可用)
  • 更高级表达式,包含附加运算符和函数以及对其他数据类型的支持
  • 更美观的弹出面板和更易操作的部分页面刷新(显示/隐藏面板)
  • 单元格高亮显示扩展到行高亮显示
  • 按颜色排序或筛选(客户端)

本文演示了 JSF 表达式语言的功能,并展示了一些实用的 Ajax/部分页面呈现技术。您也感受到了 Oracle ADF 11g 的丰富功能,并重用了绑定工作流。

 请阅读本系列的第 1 部分




Lucas Jellema 是荷兰 Nieuwegein AMIS 的 CTO 和 Oracle ACE 总监(开发人员工具)。他参加的项目主要涉及 ADF 和 SOA。此外,他经常出席甲骨文全球大会和 JavaOne,同时也是一位经常发表文章的网志作者