综合 JSF 和 Dojo 小部件创建更好的用户体验
 

2009-06-25 作者:Dan Wang,Wei Huang 来源:IBM

 
本文内容包括:
作为一种 Web 框架,JavaServer Faces (JSF) 提供了端到端的生命周期管理和具有完整事件处理和数据绑定的丰富组件模型。Dojo 是一种十分流行的 Asynchronous JavaScript + XML (Ajax) 库,可为 Web2.0 应用程序提供丰富的小部件和美妙效果。借助 JSF 和 Dojo 技术,您就可以通过在服务器端使用 JSF 集成特性、在客户机端使用 Dojo 用户界面创建一种更好的用户体验。本文介绍了这一过程并向您展示了如何能轻松构建 Web 应用程序来为您的用户提供更好的用户体验。

简介

JSF 是一种流行的 Web 框架、相当稳定并已被广泛应用于 Java™ Platform, Enterprise Edition (Java EE) 领域。Dojo 是一种功能最为强大的 Web 2.0 库之一,可用来为您的 Web 应用程序创建丰富的界面。

通过综合使用这两种技术,您就能享受到两种技术的好处。在服务器端,好处体现在对组件端对端的生命周期管理、后端 bean 数据捆绑和事件处理。在客户机端,您将能利用 Dojo 的丰富小部件、实时动画(比如淡出和滑变效果)以及拖放功能。此外,借助 Dojo 框架的 API 支持,您还能通过将更多的逻辑放在客户机端提高性能。

如下所示的是本文利用 JSF 和 Dojo 技术的方式:

  • 延迟捆绑 JSF 组件和客户机端的 Dojo 小部件
  • 构建一个定制的 JSF 组件来启用 Dojo 小部件
  • 对 JSF 组件进行延缓注入和解析为 Dojo 小部件

本文的示例 JSF 应用程序由两个页面组成:Create Project 和 Project result。图 1 和图 2 展示了这两个页面。为了方便理解,图中所示的这些示例将用在本文描述的所有方式当中以便展示如何综合 Dojo 和一个 JSF 应用程序(您也可以查看图 1 的 放大 版本)。

图 1. Create Project
项目创建页面的屏幕快照

图 2. Project result
项目结果页面的屏幕快照

这个场景其实非常简单。用户可以输入一些信息来创建一个项目,而当他们单击提交按钮时,项目信息就会显示。所以您的任务就是将输入框、文本区域和其他的 JSF 组件转变为 Dojo 小部件。

方式 1:延迟捆绑 JSF 组件和客户机端的 Dojo 小部件

这种方式是一种最简单的将 Dojo 样式应用到 JSF 组件的方式。我们只需使用 JavaScript 来绑定 JSF 组件和客户机端的 Dojo 小部件。通过使用 JavaScript,数据就可被传递并能在 JSF 组件和 Dojo 小部件间保持一致。

首先,先来看看初始的 JSF 页面。清单 1 显示了 JSF 代码(清单 1 中的代码已被格式化以便合适本文。单击 这里 可以查看全部代码)。

清单 1. 最初的项目创建 JSF 页面
 
				
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://www.ibm.com/jsf/html_extended" prefix="hx"%>
<%@ taglib uri="http://java.sun.com/jstl/core" prefix="c" %>

<body>
<f:view>
     <h2>Make your JSF application Dojoable</h2>
     <h3>Create Project</h3>
     <h:form id="project">
     <table>
          <tbody>
               <tr>
                    <td>Project Name:</td>
                    <td><h:inputText id="projectName" 
                          value="#{projectFormBean.projectName}" size="5"/></td>
               </tr>
               <tr>
                    <td>Project description:</td>
                    <td><h:inputTextarea id="projectDescription" 
                          value="#{projectFormBean.projectDescription}" rows="2" 
                          cols="15"/></td>
               </tr>
               <tr>
                    <td colspan=2>
                         <h:commandButton id="button_submit" action="success" 
                          value="Submit" type="submit"></h:commandButton>
                    </td>
               </tr>
          </tbody>
     </table>
     </h:form>
</f:view>
</body>           

此页面有两个输入字段:项目名和项目描述。这两个字段均与后端表单 bean projectFormBean 绑定。

让我们开始用一个 Dojo 样式来改变此页面。

步骤 1. 隐藏 JSF 组件并添加 Dojo 小部件

我们需要隐藏初始 JSF 组件并将其更改为不可见,然后再在此页面内添加相关的 Dojo 小部件。第一步是导入 Dojo 库并声明需要的是哪个小部件,如清单 2 所示(清单 2 中所示的代码已被格式化以便合适本文。要获得完整的代码,可以单击 这里)。

清单 2. 导入 Dojo 库并声明这些小部件
 
<style type="text/css">
    @import url("${pageContext.request.contextPath}/script/
     dojo_lib/dijit/themes/tundra/tundra.css");
     @import url("${pageContext.request.contextPath}/script/dojo_lib/
     dojo/resources/dojo.css");
</style>
<script type="text/javascript" src="${pageContext.request.contextPath}/script/
     dojo_lib/dojo/dojo.js" 
    djConfig="parseOnLoad: true, isDebug:false"></script>

<script type="text/javascript">
   dojo.require("dojo.parser");
   dojo.require("dijit.form.ValidationTextBox");
   dojo.require("dijit.form.Textarea");
</script>            

在本例中,我们使用 tundra 主题作为默认样式,所以必须首先导入 tundra.css 和 dojo.css。接下来,需要导入 dojo.js 文件,该文件是 Dojo 的核心 js 脚本文件。最后,必须声明想要使用哪些小部件。在本例中,我们使用 validationTextBoxTextarea。Dojo 采用的是按需机制。它将根据声明只加载所需的文件。因此,它能提高应用程序的性能并减少网络传输。

下一步是将 JSF 组件更改为不可见并添加 Dojo 小部件以便显示。修改后的 jsp 如清单 3 所示。

清单 3. 将 JSF 组件更改为不可见并添加 Dojo 小部件
 
<tr>
<td>Project name:</td>
<td><h:inputHidden value="#{projectFormBean.projectName}"/><f:verbatim> 
     <input type="text" name="projectName"
 dojoType="dijit.form.ValidationTextBox"
 regExp="[\w]+"
 required="true"
 invalidMessage="Invalid project name."/></f:verbatim>
</td>
</tr>
<tr>
<td>Project description: </td>
<td><h:inputHidden value="#{projectFormBean.projectDescription}"/><f:verbatim> 
 <textarea dojoType="dijit.form.Textarea" style="width:80%">
     </textarea></f:verbatim>
</td>
</tr>            

我们将原始 JSF 组件更改为 inputHidden,但仍与后端 projectFormBean 数据绑定。在这个被隐藏的组件之后,添加相应的 Dojo 小部件。对于 “project name” 字段,我们使用 validationTextBox 来在客户机端验证输入框,对于 “project description” 字段,我们使用 dojo textarea,它无需滚动条就能自动扩展。

步骤 2. 在页面呈现阶段将数据从 JSF 组件复制到 Dojo 小部件

之前,我们用 Dojo 小部件替换了 JSF 组件,但是页面内的数据并没有显示在 Dojo 小部件内。接下来,我们将展示如何将数据从 JSF 组件复制到 Dojo 小部件。

在呈现阶段,我们使用 JavaScript 来从 JSF 组件提取信息并将其设置给相关的 Dojo 小部件。清单 4 显示了所需的 JavaScript 代码。


清单 4. 在呈现阶段将数据从 JSF 组件复制到 Dojo 小部件
 
<script type="text/javascript">   
   function dojoInit(){
        dijit.registry.byClass('dijit.form.ValidationTextBox').forEach(function(pane){
          pane.setValue(pane.domNode.previousSibling.value);
     });
     
     dijit.registry.byClass('dijit.form.Textarea').forEach(function(pane){
          pane.setValue(pane.domNode.previousSibling.value);
     });
   }
   
   dojo.addOnLoad(dojoInit);
</script>            

dojoInit 函数内,可以看到按类名查询过的所有 Dojo 小部件。对于每个小部件,都可在 Dom 树内找到其前一个姊妹节点,即 JSF 组件。然后,从 JSF 组件提取数据并将其设置给所查询的 Dojo 小部件。这样一来,就能同步 JSF 组件和 Dojo 小部件的数据了。最后,使用 dojo.addOnLoad 函数以确保 dojoInit 函数在呈现阶段被调用。

注意:在使用 Dojo Toolkit 时,应该避免使用 <body onload="...">window.onload,因为 Dojo 自身的初始化机制已经使用了 window.onload。如果再次使用此机制就会干扰 Dojo 的初始化例程。Dojo 已经提供了 dojo.addOnLoad,这让您能够在 Dojo 完成其自身初始化后再加载函数。

步骤 3. 在页面提交阶段将数据从 Dojo 小部件复制到 JSF 组件

现在,dojo 小部件就可以用页面内的正确数据加以显示了。但是如果某个用户更改了数据,这些更改如何反映到相应的后端表单 bean 呢?在本步骤中,我们将展示如何在页面提交阶段同步 Dojo 小部件和 JSF 组件间的数据。

首先,必须创建一个 JavaScript 函数来将数据从 Dojo 小部件复制到 JSF 组件。所需代码如清单 5 所示。

清单 5. 将数据从 Dojo 小部件复制到 JSF 组件
 
function setDojoValue(){
     dijit.registry.byClass('dijit.form.ValidationTextBox').forEach(function(pane){
          pane.domNode.previousSibling.value = pane.getValue();
     });
     
     dijit.registry.byClass('dijit.form.Textarea').forEach(function(pane){
          pane.domNode.previousSibling.value = pane.getValue();
     });
}            

此函数类似于步骤 2 所示的 dojoInit 函数。我们仍使用 dojo 查询方法来按某个特定的类名寻找所有 dojo 小部件。之后,我们从这些 Dojo 小部件提取数据并将其设置给相应的 JSF 组件。

接下来,我们必须在页面提交时调用 setDojoValue 函数,所以要按清单 6 所示更改原始的提交按钮。

清单 6. 综合了 JavaScript 函数的提交按钮
 
<h:commandButton id="button_submit" action="success" 
    value="Submit" type="submit" onclick="setDojoValue();"></h:commandButton>            

我们在 JSF commandButton 内添加 onclick 属性,所以当用户单击此按钮时,这个 JavaScript 函数就会首先被调用,之后页面会被提交。这样一来,我们就能在页面被提交之前将 Dojo 小部件内的所有数据复制到 JSF 组件,而数据更改也就能反应到后端的表单 bean。

上面所述的就是采用方式 1 所需做的全部操作。图 3 显示了这个新的创建项目页面。

图 3. 利用方式 1 创建项目
利用方式 1 创建项目的屏幕快照

正如您所见,我们使用了一个支持客户端验证的有效输入框。我们还使用了 dojo textarea,它无需滚动条也能自动扩展。

方式 2:构建定制 JSF 组件来启用 Dojo 小部件

JSF 是一种功能强大的 Web 框架。它不仅为 Web 应用程序提供了标准的用户界面组件,它还是一种非常灵活的 API,允许用户定制。在本节中,我们将介绍如何开发综合了 Dojo 样式的定制 JSF 组件。

通常,JSF 组件将包含如下部分:

  • UIComponent Class:该类获取自 UIComponentBase 或其他已有的 JSF 组件,比如 outputText。这个类代表的是整个 JSF 组件的核心逻辑。
  • Render Class:该类用来呈现组件。通常,它负责处理如何生成要呈现的 HTML 代码,所以它是本节的关键所在。我们需要更改这个类并让其生成 Dojo 样式的 HTML 代码。
  • UI Component Tag Class:它是一个 JSP 标记处理程序类,能让 UI Component 用于 JSP 页面。它还能关联一个单独的呈现类和一个 UIComponent 类。
  • Tag Library Descriptor File:这是一个标准的 JavaEE JSP 标记库描述符(tld)文件,能关联标记处理程序类与 JSP 页面内的一个可用标记。

我们将继续以创建项目的场景为例来展示如何创建一个具备 dojo 验证功能的输入文本 JSF 组件。

首先,先来看看这个定制了的 JSF 标记。清单 7 显示了如何在 Web 应用程序中使用这个定制了的 JSF 标记:

清单 7. 使用一个定制了的 JSF 标记
 
<h:form id="project">
<table>
<tbody>
     <tr>
     <td>Project Name:</td>
     <td>
     <jsfdojo:input id="projectName" type="text" 
     invalidMessage="Invalid project name." 
     dojoType="dijit.form.ValidationTextBox" 
     dojoRequired="true" 
     regExp="[\w]+" 
     value="#{projectFormBean.projectName}"/>
     </td>
     </tr>            

图 4 显示了使用方式 2 时的 Web 页面(您也可以查阅图 4 的放大 图像)。

图 4. 创建项目 – 方式 2
使用方式 2 创建项目的页面屏幕快照

要开发具有 Dojo 样式的定制 JSF 输入框,可以使用已有的 JSF 输入框作为指导,所以无需实现自己的 UIComponent 类;可重用已有的类。虽然这里无需实现呈现类和标记类,但是即便实现也不是很难。此外,还可以继承现有 JSF API,而且只需覆盖几个函数。

清单 8 显示了用于处理 JSP 页面内的标记的这个标记类的代码。它还关联了 UIComponent 和 Render 类。

清单 8. 标记类
 
public class InputTag extends InputTextTag {

    @Override
    public String getComponentType() {
        return "javax.faces.HtmlInputText";
    }

    @Override
    public String getRendererType() {
        return "jsfdojo.input.render";
    }

    private String dojoType;

    private String invalidMessage;

    private String regExp;

    private String type;
    
    private String dojoRequired;

    public String getDojoRequired() {
        return dojoRequired;
    }

    public void setDojoRequired(String dojoRequired) {
        this.dojoRequired = dojoRequired;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getRegExp() {
        return regExp;
    }

    public void setRegExp(String regExp) {
        this.regExp = regExp;
    }

    public String getDojoType() {
        return dojoType;
    }

    public void setDojoType(String dojoType) {
        this.dojoType = dojoType;
    }

    public String getInvalidMessage() {
        return invalidMessage;
    }

    public void setInvalidMessage(String invalidMessage) {
        this.invalidMessage = invalidMessage;
    }

    @Override
    protected void setProperties(UIComponent component) {
        super.setProperties(component);
  
        component.getAttributes().put("dojoType", dojoType);
        component.getAttributes().put("dojoRequired", dojoRequired);
        component.getAttributes().put("invalidMessage", invalidMessage);
        component.getAttributes().put("regExp", regExp);
        component.getAttributes().put("type", type);
    }

}            

这个标记类继承自 InputTextTag。我们需要覆盖一些函数并添加一些新的字段以便新属性可被用在此标记内。

getComponentType 函数指定 UIComponent 类,getRendererType 函数指定 Render 类。因此,如果此 JSF 组件已经与这个呈现类分离,在此标记类内,这两个函数用来将二者链接在一起。

此标记类内的这些字段代表的是用在 JSP 标记内的属性。setProperties 函数用来将这些属性存储在此组件的属性图内。此图将被用在这个呈现类内。

如上面所述,我们将实现一个单独的呈现类。此类负责生成 HTML 代码以便呈现在 Web 页面内。清单 9 显示了该呈现类的代码。

清单 9. 单独的呈现类
 
public class InputRender extends TextRenderer {

    @Override
    public void encodeEnd(FacesContext context, UIComponent component) 
     throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        writer.startElement("input", component);
        String id = (String) component.getClientId(context);
        writer.writeAttribute("id", id, "id");
        writer.writeAttribute("name", id, "id");
        writer.writeAttribute("value", getValue(component), "value");
        
        writer.writeAttribute("type", (String) 
     component.getAttributes().get("type"), null);
writer.writeAttribute("dojoType", (String) 
     component.getAttributes().get("dojoType"), null);
writer.writeAttribute("required", (String) 
     component.getAttributes().get("dojoRequired"), null);
writer.writeAttribute("invalidMessage", (String) 
     component.getAttributes().get("invalidMessage"), null);
writer.writeAttribute("regExp", (String) component.getAttributes().get("regExp"), null);
        
        writer.endElement("input");
    }

    protected Object getValue(UIComponent component) {
        Object value = null;
        if (component instanceof UIInput) {
            value = ((UIInput) component).getSubmittedValue();
        }
        if (null == value && component instanceof ValueHolder) {
            value = ((ValueHolder) component).getValue();
        }
        if (value == null) {
            value = "";
        }
        return value;
    }
}            

这个标记类继承自 TextRenderer,所以无需关心编码的问题。所需做的只是实现编码输出,这可通过 encodeEnd 函数实现。在此函数内,ResponseWrite 对象可以让这些 HTML 代码的生成变得很简单。对于新的添加属性,比如 typedojoTyperequiredinvalidMessageregExp,它们的值均检索自此组件的属性图,该图在此标记类内设置。

value 属性的值检索自 UIComponent,在 getValue 函数内实现。

实现了标记和呈现类之后,还需要定义 JSF 组件的标记库描述符文件(tld 文件)。清单 10 给出了这个 tld 文件的内容。

清单 10. 标记库描述符文件 – tld 文件
 
<taglib>
     <tlib-version>1.0</tlib-version>
     <jsp-version>1.2</jsp-version>
     <short-name>jsfdojo</short-name>
     <uri>http://jsfdojo.ibm.com</uri>

     <tag>
        <name>input</name>
        <tag-class>jsf.input.InputTag</tag-class>
        <body-content>empty</body-content>
        <description>
            
        </description>
        <attribute>
            <name>dojoType</name>
            <required>false</required>
            <rtexprvalue>false</rtexprvalue>
            <description>
                
            </description>
        </attribute>
        <attribute>
            <name>dojoRequired</name>
            <required>false</required>
            <rtexprvalue>false</rtexprvalue>
            <description>
                
            </description>
        </attribute>
        <attribute>
            <name>type</name>
            <required>false</required>
            <rtexprvalue>false</rtexprvalue>
            <description>
                
            </description>
        </attribute>
        <attribute>
            <name>invalidMessage</name>
            <required>false</required>
            <rtexprvalue>false</rtexprvalue>
            <description>
                
            </description>
        </attribute>
         <attribute>
            <name>regExp</name>
            <required>false</required>
            <rtexprvalue>false</rtexprvalue>
            <description>
                
            </description>
        </attribute>
        <attribute>
            <name>value</name>
            <required>false</required>
            <rtexprvalue>false</rtexprvalue>
            <description>
                Value reference expression pointing the attribute that
                will have the credit card number.
            </description>
        </attribute>
        ……
    </tag>
</taglib>            

在这个 tld 文件内,“tag class” 字段指定这个被实现的标记类。它还描述了将被用在此标记内的属性。在本例中,我们添加了几个新的属性:dojoTypedojoRequiredtypeinvalidMessageregExp。此外,还有很多超类属性,比如 idvalue 等。

最后,需要在 faces-config.xml 中注册这个新组件。清单 11 显示了此组件的注册部分。

清单 11. 在 faces-config.xml 中注册这个 JSF 组件
 
<render-kit>
<renderer>
        <description>
            Renderer customized input with dojo style
        </description>
        <component-family>javax.faces.Input</component-family>
        <renderer-type>jsfdojo.input.render</renderer-type>
        <renderer-class>
             jsf.input.InputRender
         </renderer-class>
      </renderer>
   </render-kit>            

component family 与 renderer type 字段与此标记类内的函数相映射。清单 12 显示了这些函数。

清单 12. 覆盖组件与呈现器字段
 
@Override
public String getComponentType() {
return "javax.faces.HtmlInputText";
}

@Override
public String getRendererType() {
return "jsfdojo.input.render";
}            

这就是要实现定制 JSF 组件所需做的所有工作。它涉及到了几个不同的类与 xml 实现,但它很直观,并不复杂,特别是在基于一个已有的组件进行实现时就更是如此。在本示例中,我们仅添加了一些新的属性和覆盖了编码函数来生成 Dojo HTML 代码。

方式 3:延缓注入(将 JSF 组件延缓解析成一个 Dojo 小部件)

这种方式完全在客户机端由 JavaScript 执行,但有别于本文中的第一种方式。延迟组件是指在整个页面导入后,再将 Dojo 小部件信息注入到 JSF 组件。随后,利用 Dojo 解析器解析这个 JSF 组件并将其转换为一个 Dojo 小部件。在注入过程中,可能需要注册一个处理程序,在后面提交 JSF 表单时,将会调用该处理程序。这样一来,准确的数据就可被传回至服务器端相应的 JSF 侦听器。图 5 显示了方式 3 的详细工作流程(您还可以 查看 图 5 的放大图)。

图 5. 延迟注入方式的工作流程
延迟注入方式的工作流程

步骤 1:将 Dojo 小部件信息注入到 JSF 组件。

您可能已经注意到了 JSF 组件是在整个页面完成加载后才被转变成 HTML 标记的。清单 13 所示的是 JSF inputText 组件的语法。在页面加载后,inputText 组件被转变成了 HTML 标记,如清单 14 中所示。

清单 13. JSF inputText 组件的语法
 
<td>
     <h:inputText id="projectName" value="#{projectFormBean.projectName}" size="5"/>
</td>            

清单 14. 针对 JSF inputText 组件所生成的 HTML 标记
 
<td>
     <input type="text" name="project:projectName" id="project:projectName" size="5"/>
</td>            

这种方法很直观。向所生成的这些 HTML 标记添加必要的小部件信息可以方便 Dojo 解析器识别并将其解析为一个 Dojo 小部件。此外,为了将常规的 HTML 标记与从 JSF 组件生成的那些 HTML 标记区分开来,此 JSF 组件的包装器节点上被标上了一个特别的属性 jsf2dojo='true'。清单 15 显示了 Create Project 示例更新后的 JSF 页面。更多细节,可以参考 NewProject_lazy_inject.jsp 文件。

清单 15. 用 jsf2dojo='true' 属性更新后的 JSF 页面
 
<tr>
     <td jsf2dojo='true'>
          <h:inputText id="projectName" value="#{projectFormBean.projectName}" 
          size="5"/>
     </td>
</tr>
<tr>
     <td jsf2dojo='true'>
     <h:inputTextarea id="projectDescription" 
     value="#{projectFormBean.projectDescription}" rows="2" cols="15"/>
     </td>
</tr>            

利用这种方法,我们可以标记页面内的部分 JSF 组件并将它们转换成 Dojo 小部件;其他 JSF 组件保持不变。请注意 jsf2dojo='true' 属性被添加到了这个包装器节点而不是此 JSF 组件自身,因为要是添加到后者的话,JSF 编译器会把它视为带编译错误的无效属性。

步骤 2:将所注入的 JSF 组件解析成 Dojo 小部件

在 Dojo 注入完成后,下一步就是将 JSF 组件解析成 Dojo 小部件。Dojo 解析器对所有小部件来说都是至关重要的。默认情况下,在页面加载后,Dojo 解析器就会扫描整个页面寻找带 Dojo 标志(例如,dojoType='xxx')的标记并将它们解析成小部件。Dojo 解析器的另一个用途是动态地解析标记,或如我们所说的进行延缓解析。这里我们就介绍一下如何操作。

所有解析逻辑均封装进 jsf2dojo.js 文件,所以第一个步骤就是将其包含进 JSF 页面,如清单 16 所示。

清单 16. 导入 jsf2dojo.js
 
				
<script type="text/javascript" src="script/jsf2dojo.js"></script>            

接下来,将所有解析步骤添加到 jsf2dojo.js。第一步是进行小部件声明,如清单 17 所示。

清单 17. 小部件类声明与页面 onload 处理程序
 
				
dojo.require("dijit.form.ValidationTextBox");
dojo.require("dijit.form.Textarea");
dojo.require("dojo.parser");

var TYPE_MAP = {
     text      : 'dijit.form.ValidationTextBox',
     textarea : 'dijit.form.Textarea'
};

dojo.addOnLoad(init);            

小部件类是为 Create Project 示例声明的。这里,使用了 Dojo ValidationTextBoxTextarea 小部件。Dojo 解析器也包括在内。TYPE_MAP 常量定义了我们在后面将要使用的类映射。dojo.addOnLoad(init) 函数注册了一个回调功能,它将在整个页面被加载以及 Dojo 完成初始化后被调用。

清单 18 显示了函数 init 的内容。

清单 18. 将 JSF 组件解析成一个 Dojo 小部件
 
				
function init(){          
     var jsfContainerList = dojo.query("[jsf2dojo='true']");     

     for(var i = 0 ; i < jsfContainerList.length; i++){
          var jsfWidget = _getJsfWidget(jsfContainerList[i]);//get JSF component node
          if(!jsfWidget) continue;     
               
          if('input' == jsfWidget.tagName.toLowerCase() 
          && 'text' == jsfWidget.type.toLowerCase()){ // input 
               jsfWidget.setAttribute('dojoType',
                    TYPE_MAP[jsfWidget.type.toLowerCase()]);
               jsfWidget.setAttribute('promptMessage',
                    "Please Enter your information");
               jsfWidget.setAttribute('required',"true");                    
               
          }else{
               jsfWidget.setAttribute('dojoType', 
                    TYPE_MAP[jsfWidget.tagName.toLowerCase()]);
          }
          dojo.parser.parse(jsfContainerList[i]);//parse wrapper node
       }
}            

init 函数包含所有解析逻辑。首先,dojo.query("[jsf2dojo='true']") 用来获取包含我们前面定义的 JSF 组件的所有包装器(<td>)。接下来,_getJsfWidget() 用来获取从 JSF 组件生成的 HTML 标记。对于 JSF inputText 组件,我们添加 dojoType=' dijit.form.ValidationTextBox' 及其他特定属性,包括一个提示消息以及此属性是否必需的信息。对于 JSF inputTextarea 组件,我们添加 dojoType=' dijit.form.Textarea' 。现在万事俱备,可以调用 dojo.parser.parse() 来生成这个 Dojo 小部件。这里,JSF 组件的包装器节点(<td>)被用于解析。

步骤 3:验证页面

可以通过访问 http://[server ip]:[server port]/JSFSampleEarWeb/faces/NewProject_lazy_inject.jsp 验证结果。在页面上应该能够看到这些 Dojo 小部件,如图 6 所示。

图 6. 创建项目 – 方式 3
利用方式 3 创建项目的页面屏幕快照

在单击提交后,项目名称与描述字段会被传送到服务器端的 JSF 侦听器并被显示在另一个名为 Project_result.jsp 的 JSF 页面。

高级主题

您可能已经注意到了前面所列的步骤处理的只是一个简单的示例,其中 JSF 组件具有与 Dojo 小部件的紧密映射。但如果映射非常复杂呢?如前面提到过的,我们将需要其他一些技巧。

首先,我们将向此页面添加更多的 JSF 组件,如清单 19 所示。

清单 19. 添加了更多组件的 JSF 页面
 
				
<tr>
     <td jsf2dojo='true'>
          <h:selectOneMenu id="projectOption" value="location">
          <f:selectItem id="select1" itemValue="Shanghai" itemLabel="Shanghai"/>
          <f:selectItem id="select2" itemValue="London" itemLabel="London"/>
          <f:selectItem id="select3" itemValue="New York" itemLabel="New York"/>
          </h:selectOneMenu>
     </td>
</tr>
<tr>
     <td jsf2dojo='true' jsfType='radio'>
          <h:selectOneRadio id="newPrjRadio" value="NYPrj">
                 <f:selectItem id="rd1" itemLabel="Yes" itemValue="y" />
          <f:selectItem id="rd2" itemLabel="No" itemValue="n" />
          </h:selectOneRadio>
     </td>
</tr>
<tr>
     <td jsf2dojo='true'>
          <h:selectBooleanCheckbox id="ck1" value="manager" />Project Manager
          <h:selectBooleanCheckbox id="ck2" value="technical leader"/>Techinal Leader
          <h:selectBooleanCheckbox id="ck3" value="member" /> Developer&Tester
     </td>
</tr>
<tr>
     <td jsf2dojo='true'>
          <h:commandButton id="btnSubmit" action="success" value="Submit"
           type="submit"></h:commandButton>
     </td>
     <td><input id='btnSubmit_mock' type='text' style='display:none'/></td>
</tr>            

这里,为 JSF selectOneRadio 组件添加了一个新属性 jsfType='radio',也为 JSF commandButton 组件添加一个隐藏的输入文本。这些属性会在后面的解析过程用到。要获取更多关于此更新后的页面的信息,可以参考 NewProject_lazy_inject_advanced.jsp 文件。

接下来,需要解析这些新的 JSF 组件。

在某些情况下,在 JSF 组件被转换为 Dojo 小部件后,HTML 源代码会变得完全不同。例如,清单 20 显示了 JSF selectOneMenu 组件的 HTML 源代码,清单 21 显示了 selectOneMenu 被转换为 Dojo ComboBox 小部件后的源代码。

清单 20. JSF selectOneMenu 组件的 HTML 源代码
 
				
<select id="project:projectOption" size="1" name="project:projectOption">
     <option value="Shanghai">Shanghai</option>
     <option value="London">London</option>
     <option value="New York">New York</option>
</select>            

清单 21. Dojo ComboBox 小部件的 HTML 源代码
 
				
<div id="widget_project:projectOption" class="dijit ..." value="Shanghai" 
     widgetid="project:projectOption" ...>
     <div style="overflow: hidden;">
               ...
          <div class="dijitReset dijitInputField">
               <input id="project:projectOption" class="dijitReset" type="text"/>
          </div>
     </div>
</div>            

为了让这些差异对服务器端的 JSF 侦听器透明,需要注册一个处理程序并在 JSF 表单被提交时调用。清单 22 显示了更新后的逻辑以便将更多 JSF 组件解析成 Dojo 小部件。

清单 22. 将更多的 JSF 组件解析成 Dojo 小部件
 
				
for(var n = 0; n < jsfWidgets.length; n++){
     var jsfWidget = jsfWidgets[n];
     
     if ('select' == jsfWidget.tagName.toLowerCase()){
          jsfWidget.setAttribute(DOJO_TYPE, 
               TYPE_MAP[jsfWidget.tagName.toLowerCase()]);
          mockFunc = _mockSelect;
          
     }else if('textarea' == jsfWidget.tagName.toLowerCase()){
          jsfWidget.setAttribute(DOJO_TYPE, 
               TYPE_MAP[jsfWidget.tagName.toLowerCase()]);
          
     }else if('input' == jsfWidget.tagName.toLowerCase()){
          var type = jsfWidget.type.toLowerCase();
          
          if('submit' == type){
               var params = {label: jsfWidget.value};
               var dojoWidget = new dijit.form.Button(params, jsfWidget);
               dojo.connect(dojoWidget, 'onClick', window, 'formSubmit');
               mockFunc = _mockSubmitInput;

          }else if('text' == type){
               jsfWidget.setAttribute(DOJO_TYPE, TYPE_MAP[type]);
               jsfWidget.setAttribute('promptMessage',
                    "Please Enter your information");
               jsfWidget.setAttribute('required',"true");                    
          
          }else if ('checkbox' == type || 'radio' == type){     
               jsfWidget.setAttribute(DOJO_TYPE, TYPE_MAP[type]);
          }
     }
     if(!dojoWidget){
          dojoWidget = dojo.parser.parse(jsfList[j])[0];//parse wrapper node
     }     
     if(dojoWidget && mockFunc){
          mockFuncMap.push({'dojoWidget': dojoWidget, 'mockFunc': mockFunc});
     }
}            

回调处理程序 _mockSelect_ mockSubmitInput 分别针对 JSF selectOneMenu 和 commandButton 进行注册。如清单 23 所示,这些处理程序将数据绑定给相应的字段以确保表单信息在 HTTPRequest 内保持不变。这样一来,服务器端的 JSF 侦听器就不会感觉到任何变化。请注意,Dojo Button 是特别为 commandButton 创建的。

清单 23. JSF selectOneMenu 的示例回调处理程序
 
				
Function _mockSelect(dojoWidget){
     var mockedSelect = dojo.byId(dojoWidget.id);
     mockedSelect.value = (dojoWidget.item) ? dojoWidget.item.value : 
          dojoWidget.store.root.value;
}            

通常,所有回调处理程序都具有两个步骤:第一步是要找到相应的字段并添加必要的值。如清单 22 中所示,通过使用 dojo.connect(dojoWidget, 'onClick', window, 'formSubmit') 函数,在 JSF 表单被提交时就会调用 formSubmit。清单 24 显示了 formSubmit 的详细内容。

清单 24. 在 JSF 表单被提交前调用 formSubmit 函数
 
				
function formSubmit(event){
        for(var i = 0 ; i < mockFuncMap.length; i++){     
          mockFuncMap[i]['mockFunc']( mockFuncMap[i]['dojoWidget'] );        
     }
     event.target.form.submit(); //event.target - submit button
}            

formSubmit 的惟一作用就是调用所有注册的回调处理程序以确保表单信息的正确性。所有事情都对服务器端的 JSF 侦听器透明。

现在,通过访问 http://[server ip]:[server port]/JSFSampleEarWeb/faces/NewProject_lazy_inject_advanced.jsp 验证结果。现在,您将会看到所有的 JSF 组件都已被转换成了 Dojo 小部件,如图 7 所示。

图 7. 创建具有更多小部件的项目– 方式 3
使用方式 3 创建具有更多小部件的项目的屏幕快照

对比

在前面的章节中,我们列出了三种综合 JSF 应用程序与 Dojo 的方式。每种方式都各有利弊。在这一部分,我们在以下几方面对这三种方式进行对比:

  • 复杂性 — 它是否易于实现?是否需要修改很多文件或修改配置文件?
  • 适用性 — 它是否适用于所有 JSF 组件?是否有何局限?
  • 重用性 — 它是否易于重用?是否有可能成为一个可被直接用于其他应用程???的独立组件?

表格 1 给了这三种方式的对比。

表格 1 三种方式的对比
 
方式 复杂性 适用性 重用性
方式 1 1 2 2
方式 2 3 3 3
方式 3 2 3 2

注意:我们用数字 1 到 3 来表示每个方面的得分。1 代表最低分,3 代表最高分。

如您在表中看到的,方式 1 是最容易实现的,但它有局限性。此方式不适用于每个 JSF 组件,比如 Checkbox。因此,方式 1 是能快速地将某些 JSF 组件(input boxtextarea 等)转变为 Dojo 小部件的一种很好的方法。

方式 2 有点复杂,因为构建您自己的 JSF 组件并不是很简单。但这种方式能适用于任何 JSF 组件,并且很容易重用。带 Dojo 的 JSF 组件可被其他应用程序直接使用。因此,方式 2 对企业级开发来说是一个很好的选择,因为很多项目可能具有相同的需求。

方式 3 处于中间水平。它没有方式 2 那么复杂,也可适用于任何 JSF 组件,但它不易被重用。它要求您具有更多的关于 Dojo 小部件框架的知识。因此,这种方式适用于单一项目开发。如果只有一个项目需要带 Dojo 的 JSF 组件,而您又不想花费太多时间去实现它,那么方式 3 就是您最佳的选择。

结束语

集成 JSF 与 Dojo 非常有用。它综合利用了 JSF 的服务器端功能与 Dojo 的功能强大的小部件,能让您轻松创建具有更好用户体验的 Web 应用程序。本文提供了三种方式来用 Dojo 修改 JSF 应用程序。每种方式都各有利弊,所以您可以根据项目的需要选择最适合的一种。

下载

描述 名字 大小 下载方法
示例代码 JSFSampleEarWeb.war 17KB

HTTP

参考资料

学习 获得产品和技术
  • Dojo 下载中心 提供了 Dojo 最新的以及之前的所有发行版。
  • Dojo Forum 可以获得与 Dojo 有关的任何问题的答案。

火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织