内容


JSF 2 简介

JSF 向导

用 JSF 2 和 CDI 实现一个向导

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: JSF 2 简介

敬请期待该系列的后续内容。

此内容是该系列的一部分:JSF 2 简介

敬请期待该系列的后续内容。

Java™ Enterprise Edition (Java EE) 6 包含了很多像 JSF 2 这样功能强大的技术。Contexts and Dependency Injection(CDI)是这些技术中的一个,它在很大程度上标准化了在其他框架酝酿多年的一些概念。

在本文中,我向您展示如何综合使用 JSF 2 和 CDI 来实现一个在线小测验向导。CDI 为我带来了依赖注入、producer 方法和一个 conversation 作用域。我使用了这三者来实现一个向导,您可以轻松地将其用于任何一个多选的在线测试。

本系列并非只关注于 CDI。我将介绍如何:

  • 使用 facelets 模板来最小化代码并最大化重用
  • Ajax 化向导以便获得更为顺畅的用户体验
  • 使用 CDI 的依赖注入来简化代码
  • 实现并使用 CDI producer 方法来在视图中无缝地使用 beans
  • 利用 CDI 的 conversation 作用域来实现多请求用户用例

本文中的全部示例源代码均可下载得到。请参见 运行示例代码 侧栏获得下载链接以及指向部署指导的一个指针。

测验向导

图 1 显示了运行中的这个测验向导:

图 1. 测验向导
测验向导
测验向导

最初,这个应用程序只包含一个可开启向导的单独链接:<h:commandLink value="#{msgs.startWizard}" action="#{wizard.start}"/>。这个链接的文本(Start the wizard)来自一个属性文件并由该链接值内的 msgs.startWizard 表达式代表。国际化是 JSF 101 circa 2004,所以我在这里就不再过多涉及这些细节了。只需注意到整个应用程序都是本地化了的就足够了,并且所有字符串均从 messages.properties 文件拉出。

Start the wizard 链接会将用户带到这个测验向导页面,在这里,用户会被提问,一次一个问题,如 图 1 底部的两个图片所示。我通过一些简单的 Ajax 和一个服务器端 bean 控制此向导按钮的启用状态,我在本文的 Ajax 部分将向您详示。

图 2 显示了最后的一个问题,后跟用户答案的一个总结。当用户处于最后一个问题时,只有 Finish 按钮是启用的;单击该按钮会将用户带到总结页面。

图 2. 总结页面
向导总结
向导总结

了解了这个测验向导如何工作后,我将向您展示它是如何实现的。

测验应用程序

这个测验应用程序的文件如图 3 所示:

图 3. 此应用程序的文件
应用程序目录

我用一个 JSF 2 模板(/templates/wizardTemplate.xhtml)实现此测验向导,向导视图(/quizWizard/wizard.xhtml)使用了这个模板。

除了上述模板和视图外,我还有针对向导的每个组成块的 facelets — 全部处于 quizWizard 目录:

  • 头部(/quizWizard/heading.xhtml)
  • 问题(/quizWizard/question.xhtml)
  • 单选按钮(quizWizard/choices.xhtml)
  • Next、Previous 以及 Finish 按钮(quizWizard/controls.xhtml)

index.xhtml facelet 用 Start the wizard 链接启动这个应用程序,而 done.xhtml facelet 则显示了问题和答案的总结。

对于客户机,就介绍这么多。在服务器上,应用程序有三个 bean,我们接下来将讨论其中的两个。

此应用程序的两个 question bean

Question bean,如清单 1 所示,实际上是一个问题、一组答案选项以及一个答案:

清单 1. Question bean
package com.clarity;

import java.io.Serializable;

public class Question implements Serializable {
  private static final long serialVersionUID = 1284490087332362658L;

  private String question, answer;
  private String[] choices;
  private boolean answered = false; // next button is enabled when answered is true
  
  public Question(String question, String[] choices) {
    this.question = question;
    this.choices = choices;
  }

  public void setAnswer(String answer) {
    this.answer = answer;
    answered = true;
  }

  public String getAnswer()    { return answer;   }
  public String getQuestion()  { return question; }
  public String[] getChoices() { return choices;  }
  public boolean isAnswered()  { return answered; }

  public void setAnswered(boolean answered) { this.answered = answered; }  
}

此应用程序在 Questions 类内还包含了一个问题数组,如清单 2 所示:

清单 2. Questions bean
package com.clarity;

import java.io.Serializable;

import com.corejsf.util.Messages;

public class Questions implements Serializable {
  private static final long serialVersionUID = -7148843668107920897L;

  private String question;
  private Question[] questions = {      
    new Question(
       Messages.getString("com.clarity.messages", "expandQuestion", null),
       new String[] { 
         Messages.getString("com.clarity.messages", "hydrogen", null),
         Messages.getString("com.clarity.messages", "helium", null),
         Messages.getString("com.clarity.messages", "water", null),
         Messages.getString("com.clarity.messages", "asphalt", null)
       }),
       
   new Question(
       Messages.getString("com.clarity.messages", "waterSGQuestion", null),
       new String[] { 
         Messages.getString("com.clarity.messages", "onedotoh", null),
         Messages.getString("com.clarity.messages", "twodotoh", null),
         Messages.getString("com.clarity.messages", "onehundred", null),
         Messages.getString("com.clarity.messages", "onethousand", null)
       }),
       
   new Question(
       Messages.getString("com.clarity.messages", "numThermoLawsQuestion", null),
       new String[] { 
         Messages.getString("com.clarity.messages", "one", null),
         Messages.getString("com.clarity.messages", "three", null),
         Messages.getString("com.clarity.messages", "five", null),
         Messages.getString("com.clarity.messages", "ten", null)
       }),
       
   new Question(
       Messages.getString("com.clarity.messages", "closestSunQuestion", null),
       new String[] { 
         Messages.getString("com.clarity.messages", "venus", null),
         Messages.getString("com.clarity.messages", "mercury", null),
         Messages.getString("com.clarity.messages", "mars", null),
         Messages.getString("com.clarity.messages", "earth", null)
       })         
  };
  
  public int size()                        { return questions.length; }
  public String getQuestion()              { return question; }
  public void setQuestion(String question) { this.question = question; }
  public Question[] getQuestions()         { return questions; }
}

清单 1清单 2 均没有什么特别之处 — 它们只是提供给我服务器上的一列问题 — 不过其中有一点值得一提,即我借助编程的方式用 helper 方法从一个资源包拉出字符串。您可以通过 下载此代码 来了解这个方法是如何工作的,而在 Core JavaServer Faces 则可以更深入地阅读到相关信息(参见 参考资料)。

以上就是对此应用程序的 bean 的全部介绍了,只有一点需要补充,即 Wizard bean,它充当了此向导的控制器 。它里面的代码是此应用程序内惟一真正有趣的 Java 代码。我在 CDI: Dependency injection and conversations 部分,还会讨论这个 Wizard bean。

您对应用程序内的文件和这些 question bean 有所了解后,我接下来将向您显示我是如何实现这个向导的视图的。

模板和视图

对于大多数的向导,您可以放心地归纳出向导的结构,如图 4 所示:

图 4. 向导的结构
测验向导按钮
测验向导按钮

清单 3 显示了封装该结构的一个模板:

清单 3. 向导模板 (templates/wizardTemplate.xhtml)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">

  <h:head>
    <title>
      <ui:insert name="windowTitle">
        #{msgs.windowTitle}
      </ui:insert>
    </title>
  </h:head>
  
  <h:body>  
    <h:outputStylesheet library="css" name="styles.css" target="head"/>       
    
    <ui:insert name="heading"/>
          
    <div class="wizardPanel">
    
      <div class="subheading">
        <ui:insert name="subheading"/>
      </div>
      
       <div class="work">
         <ui:insert name="work"/>
       </div>
       
      <div class="controls">
        <ui:insert name="controls"/>
      </div>
      
    </div>      
        
  </h:body>
</html>

这个测验向导的实现则如清单 4 所示:

清单 4. 向导 facelet (quizWizard/wizard.xhtml)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    template="/templates/wizardTemplate.xhtml">

  <ui:define name="heading">
    <ui:include src="heading.xhtml"/>
  </ui:define> 
  
  <ui:define name="subheading">
    <ui:include src="question.xhtml"/>
  </ui:define>
  
  <ui:define name="work">
    <ui:include src="choices.xhtml"/>
  </ui:define>
   
  <ui:define name="controls">
    <ui:include src="controls.xhtml"/>
  </ui:define> 

</ui:composition>

模板都相当简单。它们插入由视图定义的页面的某些部分。在本例中,清单 3 内所示的模板插入的是由 清单 4 内的视图所定义的 headingsubheadingworkcontrols 节。在模板内封装视图的常见特性会让创建新视图更为简便 — 在本例中,就是新类型的向导。

清单 5 显示了这个测验向导的 heading 节:

清单 5. heading (quizWizard/heading.xhtml)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">

      <div class="heading">
        #{msgs.quizTitle}
      </div>

</ui:composition>

清单 6 显示了 subheading 节:

清单 6. subheading (quizWizard/question.xhtml)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.or g/1999/xhtml"
     xmlns:f="http://java.sun.com/jsf/core"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">

  <h:panelGrid columns="1" id="question">
    #{wizard.cursor+1}. #{questions[wizard.cursor].question}?
  </h:panelGrid>
    
</ui:composition>

清单 5 内的 heading 显示了这次测验的标题,在本例中标题是 Science Quiz,而 清单 6 内的 subheading 则显示了问题。清单 6 内引用的 wizard.cursor 是一个指向当前问题的游标(如果您愿意,也可称之为索引)。该游标的基数为零,所以 #{wizard.cursor+1} 显示的将是题号,而 #{questions[wizard.cursor].question} 显示的是问题。

完成了这些需要提前进行的准备(比如服务器端 bean 和模板)后,我接下来将向您展示一些真正有趣的东西了:此向导的 Ajax 是如何实现的,以及此向导是如何使用 CDI 的。先从 Ajax 开始。

Ajax

此测验向导内的所有用户交互都会导致 Ajax 调用,当这些调用返回时,只呈现页面的适当部分。Ajax 调用所做的一件事情就是控制此向导按钮的启用状态。图 5 显示了在第一个问题和第二个问题期间,此向导按钮的启用状态:

图 5. 测验向导的按钮
测验向导的按钮
测验向导的按钮

这个向导的 Ajax 被整齐地封装在两个 facelet 文件内。清单 7 显示了 choices.xhtml:

清单 7. 选项
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
     xmlns:f="http://java.sun.com/jsf/core"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">
       
  <h:form id="choices">
    <h:panelGrid columns="2">  
        <h:selectOneRadio value="#{questions[wizard.cursor].answer}"
                         layout="pageDirection">
          <f:selectItems value="#{questions[wizard.cursor].choices}"/>
          <f:ajax render=":buttons"/>
        </h:selectOneRadio>
     </h:panelGrid>
  </h:form> 
    
</ui:composition>

当用户选择一个单选按钮时,JSF 就会向服务器进行一次 Ajax 调用并会在一个 backing-bean 属性内记录这次单选按钮的选择(问题的答案)。当调用返回时,JSF 就会更新向导的按钮。

清单 8 显示了 controls.xhtml:

清单 8. 控件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
     xmlns:f="http://java.sun.com/jsf/core"
     xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">

    <h:form id="buttons">
   
        <h:panelGrid columns="4" styleClass="wizardControls">
        <f:ajax render=":question :choices buttons">

            <h:commandButton id="next" 
                        styleClass="wizardButton"
                             value="#{msgs.nextButtonText}" 
                          disabled="#{not wizard.nextButtonEnabled}"/> 
                    actionListener="#{wizard.nextButtonClicked}"/>
      
            <h:commandButton id="previous"
                        styleClass="wizardButton"
                             value="#{msgs.previousButtonText}" 
                          disabled="#{not wizard.previousButtonEnabled}"
                    actionListener="#{wizard.previousButtonClicked}"/>
        </f:ajax>
                                                              
        <h:commandButton id="finish"
                    styleClass="wizardButton"
                         value="#{msgs.finishButtonText}" 
                      disabled="#{not wizard.finishButtonEnabled}"
                        action="#{wizard.end}"/>
                                                                      
        </h:panelGrid>
        
    </h:form>
</ui:composition>

当用户单击 Next 或 Previous 按钮时,JSF 就会向服务器进行一次 Ajax 调用,并且当此 Ajax 调用返回时,JSF 就会更新问题、问题的选择(单选按钮)以及按钮本身。

Finish 按钮不是一个 Ajax 按钮,因为单击它会导航到 done 页面。

请注意清单 78 中对 wizard bean 有很多引用。该 bean 实际上是这个测验向导的一个控制器。我在本文结束之前来介绍一下这个 bean。

CDI: Dependency injection and conversations

CDI 可被描述为打了激素的 JSF 托管 beans。作为 Java EE 6 的一个组件,CDI 可以说是在 Spring 内酝酿许久的一些概念的标准化,比如依赖注入和拦截器。实际上,CDI 和 Spring 3 有很多类似的特性。

CDI 让您能够通过松散耦合(loose coupling)强类型化(strong typing)为关注点解除耦合。这样一来,您就得以从日常的 Java 编程的辛劳中解放出来,比如实例化对象和控制对象的生命期。

从 JSF 的角度,CDI 的一个特别吸引人之处是 conversation 作用域。conversation 作用域是 Seam 最早提出来的,指的是一个生命期可通过编程方式控制的作用域,它让您能够从请求和会话之间的全有或没有的两难中逃离出来。

此向导对 CDI 的所有使用都位于 Wizard bean 内,如清单 9 所示:

清单 9. Wizard bean
package com.clarity;

import java.io.Serializable;

import javax.enterprise.context.Conversation;
import javax.enterprise.context.ConversationScoped;
import javax.enterprise.inject.Produces;
import javax.faces.event.ActionEvent;
import javax.inject.Inject;
import javax.inject.Named;

@Named()
@ConversationScoped()
public class Wizard implements Serializable {
  private static final long serialVersionUID = 1L;
  private Questions questions = new Questions();
  private int cursor = 0;
  
  @Inject
  private Conversation conversation;@Produces @Named
  public Question[] getQuestions() {
    return questions.getQuestions();
  }
  
  public void nextButtonClicked(ActionEvent e) {
    incrementCursor();
  }

  public void previousButtonClicked(ActionEvent e) {
    decrementCursor();
  }
    
  public void incrementCursor() { ++cursor; }
  public void decrementCursor() { --cursor; }
  public int  getCursor()       { return cursor; }
  public void resetCursor()     { cursor = 0; }

  public boolean getNextButtonEnabled() {
    return cursor != questions.size() - 1 &&
    (questions.getQuestions())[cursor].isAnswered();
  }
  
  public boolean getPreviousButtonEnabled() {
    return cursor > 0;
  }
  
  public boolean getFinishButtonEnabled() {
    return cursor == questions.size() - 1 &&
    (questions.getQuestions())[cursor].isAnswered();
  }
  
  public String start() {
    conversation.begin();
    return "quizWizard/wizard";
  }
  
  public String end() {
    conversation.end();
    return "/done";
  }
  
  private void setCurrentQuestionUnanswered() {
    Question currentQuestion = (questions.getQuestions())[cursor];
    currentQuestion.setAnswered(false);    
  }
}

此测验向导应用程序几乎所有的有趣代码都位于 清单 9 内。首先,Wizard bean 具有一些方法,能控制向导按钮启用状态,正如我在前一章节中所讨论的。它还具有另外一些方法,当用户单击 Next 或 Previous 按钮时,JSF 就会调用这些方法,分别前进到下一个问题,或移回至前一个问题。

但表面上,Wizard bean 最有趣的一点是其对 CDI 的使用。首先,本系列从始至终都使用了 @Named 注释的 CDI 实现(它实际由 JSR 330, Dependency Injection for Java 定义)来代替 @ManagedBean。两个注释都创建一个可从 JSF 表达式语言访问的设置了作用域的 bean。但 CDI 的受管 bean 的情形则更为复杂,所以如果您使用的是一个 Java EE 6 兼容的服务器,那么应该优先选用 @Named 而非 @ManagedBean

如果仔细研究 清单 6清单 7,就会发现我用 JSF 表达式语言访问了一个名为 questions 的 bean。您可能还记得我在 清单 2 中实现了一个 Questions 类。不过,在 清单 2 中并未出现过 @Named 注释。在通常情况下,注释的缺少会导致一个错误,但在本例中,questions bean 来自别处 — 它由 Wizard.getQuestions() 方法生成。该方法由一个 @Produces 注释,这意味着如果您在表达式语言中引用这个 bean,那么 JSF 就会调用该方法来获得这个 Questions bean。

之后是 Wizard bean 对 conversation 作用域的使用。应用程序欢迎页面内的 Start the wizard start Wizard bean 的 start() 方法,此方法通过调用 conversationbegin() 方法开始一次对话。该方法会将当前请求(实际上是一次持续单个请求的对话)提升成一个长时间运行的对话,此对话直到超时或有人调用 conversationend() 方法才会结束。由于我已经为 Wizard 指定了 Conversation 作用域,所以它的生命期会随着对话的结束而结束。

当然,您可以避开 conversation 作用域并在用户会话中实现您自己的虚拟对话。实际上,在 conversation 作用域出现之前,很多开发人员正是用这种做法来保持其应用程序内的多请求用例的状态。CDI 让您可以避免手动记账(bookkeeping)。

最后,注意到我使用了 CDI 注入来将一个对话注入到这个受管 bean,所以我可以用编程的方法来启动和终止一次对话。资源注入让我可以专注于进行对象本身的事情,而不是忙于创建对象并控制其生命周期的乏味工作。

结束语

在本文中,我用很少的代码阐释了很多重要概念 — Ajax 向导、模板、依赖注入、conversation 作用域。借助于 JSF 2 和 CDI,您就可以通过最少的努力和最大程度的灵活性和可重用性实现健壮的可重用 web 应用程序。

JSF 2 简介 系列在这个夏季将告一段落。我在秋天还会带来更多内容,继续帮您增强您的 JSF 技巧。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=512929
ArticleTitle=JSF 2 简介: JSF 向导
publish-date=08232010