转载于http://www.ibm.com/developerworks/cn/java/j-jsf2/
JSF 生命周期:概述 JSF 程序生命周期的 5 个阶段如下(注意每个阶段的事件处理): 恢复视图 应用请求的值;处理验证 更新模型值;处理事件 调用程序;处理事件 进行响应;处理事件 这 5 个阶段显示了 JSF 通常处理 GUI 的顺序。虽然这个清单列出了每个阶段中事件处理的可能执行顺序,但是 JSF 的生命周期很难是固定一成不变的。您可以通过忽略某个阶段或合并整个生命周期从而对执行顺序进行修改。例如,如果一个无效的请求值被拷贝到一个组件中,那么当前的视图就会重新显示,而有些阶段就可能不会执行。在这种情况中,您可以执行一个 FacesContext.responseComplete方法调用,将用户重定向到一个不同的页面上,然后使用请求分发器(从 FacesContext中的请求对象中获得)将其转发到一个适当的 Web 资源上。另外,您可以调用 FacesContext.renderResponse重新显示原来的视图。(详细信息请参看下面的示例程序。) 关于本系列文章 这个 4 部分的系列文章专门用来消除那些怀疑论者对 JavaServer Faces(JSF)技术的恐惧、怀疑或疑虑(FUD),方法是给您一个机会自己通过一个一步步的、容易学习的格式来了解这种技术。通过这 4 篇文章的教程介绍,我们将提供一系列的例子简要介绍 JSF 的基本架构、特性和功能。一旦熟悉 JSF 处理事情的方法之后,我想您将发现很难再返回 Struts Model 2 风格的开发了。毕竟,在体验过 JSF 的事件驱动的 GUI 组件模型之后,谁还会希望重新经历 XML 配置的恶梦呢? 要掌握本系列教程的内容,您应该熟悉 Java 编程、JavaBeans(即事件模型和属性)、JavaServer Pages(JSP)、JSP Standard Tag Library Expression Language 以及 Web 开发的所有基本概念。 关键是让生命周期构成您的开发项目,而不完全依赖于生命周期。在需要时,您可以修改生命周期,而不用担心破坏您的程序。在大部分情况中,您会发现 JSF 的生命周期是值得遵守的,因为它的逻辑非常好。表单必须在任何应用程序逻辑执行之前进行验证,并且在进行验证之前,必须对域中的数据进行转换。遵守生命周期的规定,可以让您更自由地考虑有关验证和转换的问题,而不是请求处理本身的阶段。有一点非常重要:其他 Web 框架也都具有类似的生命周期;它们只不过是没有很好地进行宣传。 专注 有些使用 JSF 的开发者可能从来都不会编写一个组件,也不会对框架进行任何扩展;而另外一些人则专注于这种任务的开发。尽管 JSF 的生命周期与大部分那其他项目都是相同的,但是根据在项目中的角色您可以采用不同的阶段。如果您更专注于通用的应用程序开发,就可能会关注请求处理生命周期的中间阶段: 应用请求值 更新模型值 调用程序 如果您专注于 JSF 组件的开发,就可能会关注于整个生命周期中的第一个阶段和最后一个阶段: 恢复视图 进行响应 在接下来的几节中,我们将遍历 JSF 请求处理生命周期的每个步骤,包括事件处理和验证。了解了每个步骤的基本知识之后,我们将简要介绍一个示例程序,它可以展示这些步骤如何一起使用。在开始之前,首先来看一下图 1,这是一个有关 JSF 生命周期的图。 图 1. JSF 生命周期 回页首 阶段 1:恢复视图 在 JSF 生命周期的第一个阶段 ―― 恢复视图―― 中,会有一个来自 FacesServlet控制器的请求。控制器会对请求进行考查,并提取出视图的 ID,这是由 JSP 页面的名字来确定的。 JSF 框架控制器使用这个视图 ID 来为当前的视图查找组件。如果这个视图尚未存在,那么 JSF 控制器就会创建它。如果这个视图早已存在,那么 JSF 控制器就会使用它。这个视图包含了所有的 GUI 组件。 生命周期的这个阶段表示为三个视图实例:新视图、原始视图和后视图,每个视图的处理方式都不相同。在 新视图的情况中,JSF 会构建 Faces页面的视图,并将事件处理程序和验证程序绑定到组件上。这个视图被保存在一个 FacesContext对象中。 FacesContext对象包含了 JSF 用来管理当前会话中当前请求的 GUI 组件状态所需要的所有状态信息。FacesContext将视图保存在自己的 viewRoot属性中;viewRoot包含了当前视图 ID 的所有 JSF 组件。 在 原始视图的情况中(第一次加载的是一个页面),JSF 会创建一个空视图。这个空视图会在用户事件产生时进行填充。JSF 可以直接从原始视图过渡到进行响应的阶段。 在 后视图(postback)的情况中(用户返回之前访问过的页面),包含页面的视图早已经存在了,因此只需要进行恢复就可以了。在这种情况中,JSF 就使用现有视图的状态信息来重构状态。后视图的下一个阶段是应用请求值。 回页首 阶段 2:应用请求值 应用请求值阶段的目的是让每个组件检索自己当前的状态信息。这些组件必须首先通过 FacesContext对象进行检索或创建(使用其值)。虽然组件值也可以从 cookie 或头文件中进行检索,但是它们通常是通过请求参数进行检索的。 如果一个组件的即时事件处理属性 没有设置为 true,那么就会对这些值进行转换。因此,如果 域被绑定到一个 Integer属性上,那么该值就会被转换为一个 Integer类型。如果值的转换失败了,那么就会生成一个错误消息,并在 FacesContext中进行排队,在产生响应的阶段会显示其中的消息,同时还会显示所有的验证错误。 如果一个组件的即时事件处理属性 的确被设置为 true,那么这些值就会被转换为适当的类型,并进行有效性验证。然后转换后的值会被保存到组件中。如果值转换或值的有效性验证失败了,就会生成一个错误消息,并在 FacesContext中进行排队,在产生响应的阶段会显示其中的消息,同时还会显示所有的验证错误。 即时事件处理 JSF 的即时事件处理属性用来处理那些通常不必要对整个表单进行有效性验证的事件。例如,假设一个雇员表单中有一个单选按钮来说明他是否是经理。当他选中 Manager 选项时,应用程序就会为经理生成一些内容。由于单选按钮只用来生成一个列表,而不需要用户填写整个表单,因此不需要对整个表单进行有效性验证。在这种情况中,您就可以使用即时事件处理。有关这个主题的更详细内容,请参阅 即时事件处理。 处理验证 生命周期中的第一个事件处理发生在应用请求值阶段之后。在这个阶段中,每个组件都有一些值需要根据应用程序的验证规则进行有效性验证。这些验证规则可以是预先进行定义的(JSF 中提供的),也可以由开发者进行定义。用户所输入的值会与这些验证规则进行比较。如果说输入的值无效,就会向 FacesContext中添加一个错误消息,并且该组件会被表示为无效的。如果一个组件被表示为无效的,那么 JSF 就会转到产生响应的阶段,在这个阶段中会显示当前的视图,以及验证错误消息。如果没有有效性验证错误,那么 JSF 就会转到更新模型值的阶段。 回页首 阶段 3:更新模型值 JSF 应用程序生命周期中的第三个阶段 ―― 更新模型值―― 负责更新服务器端模型的实际值,通常来讲,这都是通过更新后台 bean(称为管理 bean)的属性实现的。只有那些与组件值绑定在一起的 bean 属性才会被更新。注意这个阶段发生在有效性验证之后,因此可以确保拷贝到 bean 属性的值都是有效的(至少在表单域一级都是有效的;在业务规则一级仍可能无效)。 回页首 阶段 4:调用程序 在生命周期的第四个阶段 ―― 调用程序―― 中,JSF 控制程序会调用程序来处理 表单的提交操作。组件值已经经过了类型转换和有效性验证,并被应用到模型对象中了,因此您现在可以使用它们来执行应用程序的业务逻辑了。 在这个阶段,您还可以为一个给定的序列或很多可能的序列指定后面的逻辑视图,这可以通过为一次成功的表单提交定义一个特定的结果并返回这个结果来实现。例如:在成功输出时,将用户重定向到下一页中。要让这种导航工作能够起作用,您需要在 faces-config.xml 文件中创建一个到 成功输出的映射作为一条导航规则。一旦导航发生之后,您就转换到生命周期的最后一个阶段了。 回页首 阶段 5:进行响应 在生命周期的第五个阶段 ―― 进行响应―― 中,您可以在视图中显示当前状态中的所有组件。 图 2 是 JSF 生命周期的第五个阶段的一个对象状态图,包括时间有效性验证和处理。 图 2. JSF 生命周期的五阶段 回页首 范例 现在您已经对 JSF 生命周期的阶段有了基本的了解,下面我们将向您介绍在一个范例 Web 应用程序中,这些阶段是如何协同工作的。除了展示 JSF 生命周期的基本功能之外,这个应用程序还会利用一些通用的 JSF GUI 组件,例如 Radio List, List, Text Field, Label, Panel 等等,这样您就可以亲自体验一下在 第 1 部分中曾经简要讨论过的这些组件。 这个示例程序还会展示在 JSF 中使用其他 Java 技术的两种方法。它将组合使用 JSF 和 JavaScript 来启用即时事件处理(在那些对整个表单进行验证是多余的情况中),其布局是由 Struts Tiles 进行管理的。 虽然 Struts Tiles 并不是 JSF 的一个必要部分,但是 tiles 通常用来为一个程序中的所有 JSF 页面提供一致的外观。要学习更多有关 Struts Tiles 的内容,请参阅 参考资料。 程序设置 这个示例 Web 程序实际上是一个非常简单的创建、阅读、更新并删除(CRUD)一个在线 CD 仓库中库存的程序。它包括一个表单,让用户可以向系统中输入新 CD;有一些单选按钮,让用户选择音乐的分类。当用户选择一个分类时,就启动一些 JavaScript 脚本将这个表单立即发回服务器。程序组合采用 JSF 和 JavaScript 技术来处理一个组件,而不是整个页面,这种技术称为 即时事件处理。在这种情况中,您可以填充一个子类清单,而不用验证表单的其他内容。 这个示例程序还包括一个 CD 清单,它将展示如何使用 JSF 的 dataTable。从这个页面中,最终用户可以根据标题或者艺术家对 CD 清单进行排序。 类和方法 图 3 列出了这个示例程序的类。图中列出了 4 个类,我们只关注其中的 3 个:StoreManagerDelegate、CD和 StoreController。 图 3. 示例程序类 StoreManagerDelegate类是这个程序的业务代表。它为整个模型呈现了主界面。CD类是一个数据转换对象(DTO)。如果这是一个真实的程序,那么 StoreManagerDelegate类就会为添加、删除和编辑 CD 实现所有的业务规则,还会负责使用一个数据访问对象(DAO)将 CD存储到一个永久的存储介质中。StoreManagerDelegate和 CD 包含了一些用于这个 MVC 程序的 模型。 StoreController类是本例中的主要后台 bean。StoreController类是 GUI 世界和模型世界之间的黏合剂。它将自己的很多行为都委托给 StoreManagerDelegate进行处理。StoreController是这个 MVC 程序的 控制程序。 StoreController类展示了如何构建一个可排序的 CRUD 清单。它具有以下与 CRUD 相关的方法:editCD、addNew、addCD以及 updateCD。StoreController还负责将模型对象呈现给表单。在这种情况中,它使用 cd属性将当前的 CD对象呈现给 CD 表单,该属性的类型就是 CD。 回页首 开始编码 开始编写这个示例程序的最好方法是遍历它的使用案例: 新增 CD 编辑现有的 CD 根据标题对 CD 进行排序 根据艺术家对 CD 进行排序 第三个使用案例和第四个使用案例的代码基本上是相同的,因此我将向您展示如何根据标题进行排序,并将第四个使用案例留作练习,请您自行完成。我们很快就会对使用案例进行编码,但是首先让我们来了解一下完成后的应用程序的页面将是什么样子。 并非真正的 CRUD 注意这个程序并不是一个真正的 CRUD 清单程序。它实际上只是一个 CRU清单,因为我将 D留给您自己实现了。但是无须担心,它非常简单。删除操作的步骤与编辑操作的步骤非常类似,后者在本文中已经实现了。您可以完成 CRUD 吗? 图 4 显示了具有可排序列的 CD 清单页面。 图 4. 具有可排序列的 CD 清单页面 图 5 显示了具有分类组件的 CD 表单页面。 图 5. 尚未选择分类的 CD 表单页面 图 6 显示了具有分类和子类组件的 CD 表单页面。 图 6. 已经选择了分类和子类组件的 CD 表单页面 回页首 使用案例 1:新增 CD 在该程序的第一个使用案例中,用户将添加一个新 CD:切换到 CD 清单页面上,点击 Add CD链接(这是在 listing.jsp 文件中定义的),如清单 1 所示。 清单 1. 在 listing.jsp 中定义的 Add CD 按钮 <h:commandLink action="#{CDManagerBean.addNew}"> <f:verbatim>Add CD</f:verbatim> </h:commandLink> 这个链接被绑定到 CDManagerBean的 addNew方法上。这个 addNew方法在 JSF 生命周期的调用程序阶段(最后一个阶段)被调用的。操作被使用 JSF 绑定表达式 #{CDManagerBean.addNew}绑定到这个方法上。CDManagerBean是这个程序的存储控制器的一个别名。CDManagerBean是这个控制器的逻辑名。控制器类是一个在 faces-config.xml 文件中定义的管理 bean,如清单 2 所示。 清单 2. 在 faces-config.xml 中定义的 StoreController 类 <managed-bean> <description> The "backing file" bean that backs up the CD application </description> <managed-bean-name> CDManagerBean </managed-bean-name> <managed-bean-class> com.arcmind.jsfquickstart.controller.StoreController </managed-bean-class> <managed-bean-scope> session </managed-bean-scope> </managed-bean> 准备表单 addNew()方法通过创建一个空 CD 来准备表单,如清单 3 所示。 清单 3. addNew()创建一个空 CD 表单 [StoreController.java] /** * Prepare the cdForm to add a new CD. * This gets executed before we prompt * the user to add a new CD. * * @return success */ public String addNew() { if (subCategoryList == null) { subCategoryList = new HtmlSelectOneListbox(); } subCategoryList.setRendered(false); this.cd = new CD(); return "success"; } addNew()方法通过创建一个新的 CD 来清空 CD 表单域。这个 CD 表单的域被绑定到 cd属性的属性中。这个方法还会将正在显示的子类清单置空。 返回成功结果 接下来,addNew()方法会被调用,控制权被重定向到成功映射页面,即 cdForm.jsp 文件。cdForm.jsp 文件是在 faces-config.xml 文件中定义的,如清单 4 所示。 清单 4. cdForm.jsp 是 addNew()的成功映射 <navigation-rule> <from-view-id>/listing.jsp</from-view-id> ... <navigation-case> <from-action>#{CDManagerBean.addNew}</from-action> <from-outcome>success</from-outcome> <to-view-id>/cdForm.jsp</to-view-id> </navigation-case> </navigation-rule> 清单 4 表明如果用户从清单切换到 addNew (#{CDManagerBean.addNew})操作,并且 addNew操作成功返回,那就会切换到 cdForm.jsp 页面。 设置 cdForm 和 panelGrid cdForm.jsp是包含 CD 表单的表单。其中具有 ID、Title、Artist、Price、Category 和 Subcategory 的域。这些域被放到一个名为 panelGrid的容器中。JSF 组件,例如 AWT 组件,具有一些容器和组件。容器是一个包含其他组件的组件。这是一个 混合设计模式的例子。panelGrid有 3 列。每个域都各在一行中,还会有一个标签和消息用于显示该域的错误消息。cdForm和 panelGrid是在清单 5 中定义的。 清单 5. 定义 cdForm和 panelGrid <f:view> <h2>CD Form</h2> <h:form id="cdForm"> <h:inputHidden id="cdid" value="#{CDManagerBean.cd.id}"/> <h:panelGrid columns="3" rowClasses="row1, row2"> <h:outputLabel for="title" styleClass="label"> <h:outputText value="Title"/> </h:outputLabel> <h:inputText id="title" value="#{CDManagerBean.cd.title}" required="true"/> <h:message for="title" styleClass="errorText"/> <h:outputLabel for="artist" styleClass="label"> <h:outputText value="Artist"/> </h:outputLabel> <h:inputText id="artist" value="#{CDManagerBean.cd.artist}" required="true"/> <h:message for="artist" styleClass="errorText"/> <h:outputLabel for="price" styleClass="label"> <h:outputText value="Price"/> </h:outputLabel> <h:inputText id="price" value="#{CDManagerBean.cd.price}" required="true"/> <h:message for="price" styleClass="errorText"/> <h:outputLabel for="category" styleClass="label"> <h:outputText value="Category"/> </h:outputLabel> <h:selectOneRadio id="category" value="#{CDManagerBean.cd.category}" immediate="true" onclick="submit()" valueChangeListener="#{CDManagerBean.categorySelected}"> <f:selectItems value="#{CDManagerBean.categories}"/> </h:selectOneRadio> <h:message for="category" styleClass="errorText"/> <h:outputLabel for="subcategory" styleClass="label"> <h:outputText value="Subcategory"/> </h:outputLabel> <h:selectOneListbox id="subcategory" value="#{CDManagerBean.cd.subCategory}" binding="#{CDManagerBean.subCategoryList}"> <f:selectItems value="#{CDManagerBean.subCategories}"/> </h:selectOneListbox> <h:message for="subcategory" styleClass="errorText"/> </h:panelGrid> <br /> <h:commandButton id="submitAdd" action="#{CDManagerBean.addCD}" value="Add CD" rendered="#{not CDManagerBean.editMode}"/> <h:commandButton id="submitUpdate" action="#{CDManagerBean.updateCD}" value="Update CD" rendered="#{CDManagerBean.editMode}"/> </h:form> </f:view> 关于代码的注释 每个输入域都将该域绑定到控制器的 cd属性的一个属性上。例如,标题的输入文本域被使用下面的 JSF 绑定表达式绑定到 cd属性上:value="#{CDManagerBean.cd.title}"。 您可能会注意到在清单 5 中几乎没有什么 HTML 语句。这是由于 panelGrid会生成大部分的 HMTL 语句。注意实际的外观是由与 panelGrid相关的样式表决定的。属性 rowClasses="row1, row2"会为正在修改的行设置 CSS 类。第一行是白色的,第二行是灰色的。您还可以为列或其他内容指定 CSS 类。JSF panelGrid组件可以方便地快速设置表单的布局。如果您希望实现 panelGrid没有提供的功能,就不能使用它:不过可以使用 HTML 自己设置组件的布局。然而,如果您发现自己在很多页面上都使用了定制的 HTML,那么就可能会考虑编写自己的定制组件。这种想法可以让您尽可能 DRY地重用 HTML 语句(DRY 是 don't repeat yourself的缩写,这个术语来自于 Dave Thomas 的 Pragmatic Programmer一书)。 关于清单 5 另外需要注意的是控制器有一个 editMode属性,由 cdForm.jsp 用于有选择地显示 submitAdd按钮或 submitUpdate按钮;submitAdd按钮是在表单不处于编辑模式时显示的。submitUpdate按钮是在表单处于编辑模式时显示的。这可以简化为编辑和添加模式使用相同的 JSP。(默认情况下,表单不处于编辑模式。)这种功能是由 cdForm.jsp 中的每个按钮上的呈现表达式实现的。例如,清单 6 列出了 submitAdd button rendered="#{not CDManagerBean.editMode}"上的呈现表达式。submitAdd按钮被使用表达式 (action="#{CDManagerBean.addCD}") 绑定到 addCD方法上。 清单 6. 使用 addCD()方法添加一个 CD [StoreController.java] /** * Add a cd to the store. * * @return outcome */ public String addCD() { store.addCD(this.cd); return "success"; } 对域进行有效性验证 在 addCD方法被调用之前,JSF 必须对 GUI 中的域进行有效性验证。这实际上非常简单,因为您还没有为域关联任何有效性验证条件。在应用请求值阶段,这些值被从请求参数拷贝到组件值中(这是由组件本身进行的)。现在,价格从一个字符串转换为一个浮点类型。如果用户为价格输入的是“abc”,那么转换为浮点类型的操作就会失败,控制权将被重新定向到 cdForm.jsp 页面上,供最终用户进行修正。与价格相关的 h:message将显示一个转换错误消息。如果所有的值都可以正常进行类型转换,并且现在都可以使用了(如果需要的话),那么您就可以进行有效性验证的处理了。由于这个示例程序并没有与组件关联任何有效性验证规则(在下一篇文章中我们将介绍这种特性),因此您可以继续进入更新模型值的阶段了。 在更新模型值的阶段中,会使用保存在 GUI 组件中的经过转换和有效性验证的值来调用 CD 的赋值方法。addCD()方法是在 调用程序阶段中被调用的。addCD()方法使用一个业务代理(store对象)来执行这个操作。addCD方法在系统中使用 store对象来存储 CD。由于 addCD方法会返回成功,因此接下来会显示这个清单,这是在 faces-config.xm 中定义的。在 faces-config.xml 中定义的导航规则如清单 7 所示。 清单 7. addCD成功输出的导航规则 <navigation-rule> <from-view-id>/cdForm.jsp</from-view-id> <navigation-case> <from-action>#{CDManagerBean.addCD}</from-action> <from-outcome>success</from-outcome> <to-view-id>/listing.jsp</to-view-id> </navigation-case> ... </navigation-rule> 回页首 使用案例 2:编辑 CD 这个示例程序的第二个使用案例也会在这个清单页面(listing.jsp)中启动。除了向您介绍如何编辑 JSF 页面中的数据之外,这个使用案例还将向您介绍 JSF dataTable组件。 这个清单页面使用一个 dataTable组件来显示 CD 的清单。dataTable的值被绑定到控制程序类 StoreController的 cds属性。cds属性的定义如清单 8 所示。 清单 8. 在 StoreController.java 中定义的 cds属性 [StoreController.java] /** List of cds for CD listing. */ private DataModel cdModel = new ListDataModel(); { cdModel.setWrappedData(store.findTitleAsc()); } /** * List of CDs in the system. * * @return Returns the cds. */ public DataModel getCds() { return cdModel; } cds属性是基于从存储对象 StoreManagerDelegate返回的 java.util.List的,这个对象是该程序的业务代理。cdModel对从 DataModel中的存储对象返回的清单进行了封装。DataModel是一个用于 dataTable的模型。 dataTable的定义如清单 9 所示。 清单 9. listing.jsp 中的 dataTable定义 <f:view> <h:form> <h:dataTable id="items" value="#{CDManagerBean.cds}" var="cd" rowClasses="oddRow, evenRow" headerClass="tableHeader"> 注意该值被绑定到控制程序的 cds属性上。rowClasses和 headerClass属性用来指定 CSS 类,后者用来定义 dataTable的外观。正如前面介绍的一样,JSF 严重依赖于 CSS 来定义 GUI 的外观。如果您并不了解 CSS(即您之前都是使用字体标签和 HTML 表来设置外观的),就可能会希望在灵活运行 JSF 之前首先来学习一下有关 CSS 的知识。 column 组件 Title、Artist和 Price域都是使用 column组件显示的,如清单 10 所示(此处只显示了 Title域)。 清单 10. 在 column 组件中添加域 <h:column> <f:facet name="header"> ... <h:outputText value="Title"/> </f:facet> <h:commandLink action="#{CDManagerBean.editCD}"> <h:outputText value="#{cd.title}"/> </h:commandLink> </h:column> column组件是 dataTable的一个子组件。column组件使用一个子组件和一个 facet。facet是一个有名的子组件;它并不是一个子孙组件。column组件有一个名为 header的 facet,它定义了在 header 中显示的内容。对于本例来说,commandLink是 column组件的一个子孙组件。commandLink在一个链接中显示了 CD 的标题,该链接被绑定到操作 #{CDManagerBean.editCD}上。这个操作属性将 commandLink绑定到控制程序类的 editCD()方法上,如清单 11 所示。 清单 11. editCDcommandLink 的后台 bean 方法 [StoreController.java] /** * Edit the CD. This get executed before the edit cdForm * page gets loaded. * * @return outcome */ public String editCD() { this.cd = (CD) cdModel.getRowData(); this.cd = (CD) store.getCDById(cd.getId()); if ((cd.getCategory() != null) || !"".equals(cd.getCategory())) { this.subCategoryList.setRendered(true); this.subCategories = getSubcategoriesList(cd.getCategory()); } else { this.subCategoryList.setRendered(false); } this.editMode = true; return "success"; } editCD()方法 editCD()方法是在 JSF 生命周期的调用程序阶段调用的。editCD()方法准备控制程序以使用编辑模式来显示 cdForm.jsp 页面。这是通过查看当前选定的 CD 来实现的,CD 是通过调用 cdModel.getRowData()方法来选择的。 注意 JSF DataModel允许您从比传统的 Web 应用程序更高的层次上使用数据。您并不需要对请求参数进行检查:只需要调用 cdModel.getRowData()方法向 DataModel(cdModel)查询已经选择了哪个 CD。这个更高级别的抽象对 Web 开发进行了相当程度的简化。 一旦取得当前选择的 CD 之后,就可以使用业务代理来加载该 CD 的最新拷贝了(store.getCDById())。在加载这个 CD 之后,store.getCDById()会激活 subCategory清单(假设这个 CD 已经关联了一个子目录),然后将 editMode属性设置为 true。回想一下,editMode属性是由 cdForm用来显示 Add 或 Update 按钮。最后,store.getCDById()方法返回 success。在清单 12 中重要的导航规则可以保证返回成功之后,切换到 cdForm.jsp 页面,如下所示。 清单 12. 一条重要的导航规则 <navigation-rule> <from-view-id>/listing.jsp</from-view-id> <navigation-case> <from-action>#{CDManagerBean.editCD}</from-action> <from-outcome>success</from-outcome> <to-view-id>/cdForm.jsp</to-view-id> </navigation-case> <navigation-case> <from-action>#{CDManagerBean.addNew}</from-action> <from-outcome>success</from-outcome> <to-view-id>/cdForm.jsp</to-view-id> </navigation-case> </navigation-rule> updateCD()方法 CD 表单会加载并显示 CD 属性的属性设置。最终用户可以根据需要编辑所得到的表单,并在完成时点击 Update 按钮。Update 按钮是当用户处于 Edit 模式时所显示的惟一一个按钮,它只会在 editMode为 true时显示,如清单 13 所示。 清单 13. Update CD 按钮 [cdForm.jsp] <h:commandButton id="submitUpdate" action="#{CDManagerBean.updateCD}" value="Update CD" endered="#{CDManagerBean.editMode}"/> Update 按钮被绑定到 updateCD()方法上。在调用 update 方法之前,JSF 必须对 GUI 中的域进行有效性验证。在应用请求值阶段,这些值被从请求参数中拷贝到组件值中(这是由组件本身完成的)。现在,价格被从一个字符串转换成了一个浮点类型。由于没有为组件关联任何有效性验证规则,因此如果所有请求的值都已经存在并经过转换了,就可以转换到生命周期的下一个步骤了。 更新模型值 在更新模型值阶段中,会使用保存在 GUI 组件中经过类型转换和有效性验证的值来调用 CD 的赋值函数。updateCD()方法是在调用程序阶段被调用的。updateCD()方法如清单 14 所示。 清单 14. updateCD()方法 [StoreController.java] /** * Update the CD loaded on the form. * * @return success */ public String updateCD() { store.updateCD(this.cd); this.editMode = false; return "success"; } updateCD()方法可以代理业务代理的大部分职责。它将 editMode 设置为 false(这是默认值),并返回成功。成功输出将您重定向回清单页面中,在这个页面中您可以查看根据清单 15 中显示的导航规则新编辑的 CD。 清单 15. 成功的 UpdateCD 会将您带回 listing.jsp [faces-config.xml] <navigation-rule> <from-view-id>/cdForm.jsp</from-view-id> ... <navigation-case> <from-action>#{CDManagerBean.updateCD}</from-action> <from-outcome>success</from-outcome> <to-view-id>/listing.jsp</to-view-id> </navigation-case> </navigation-rule> 回页首 使用案例 3:对 CD 进行排序 我们要介绍的最后一个使用案例将向您展示如何对表进行排序。这个使用案例也是在 CD 清单页面上启动的。清单页允许根据标题和艺术家对 CD 按照升序或降序的顺序进行排列。在本例中,我将向您展示如何根据标题进行排序,并将根据艺术家进行排序留作练习。 标题头排序有一些到控制程序中排序方法的链接。清单 16 显示了在 listing.jsp 中是如何显示标题头的。 清单 16. 对 commandLinks 进行排序 [listing.jsp] <h:column> <f:facet name="header"> <h:panelGroup> <h:outputText value="Title"/> <f:verbatim> [</f:verbatim> <h:commandLink styleClass="smallLink" action="#{CDManagerBean.sortTitleAsc}"> <h:outputText id="ascTitle" value="asc"/> </h:commandLink> <h:outputText value=","/> <h:commandLink styleClass="smallLink" action="#{CDManagerBean.sortTitleDec}"> <h:outputText id="decTitle" value="dec"/> </h:commandLink> <f:verbatim>]</f:verbatim> </h:panelGroup> </f:facet> <h:commandLink action="#{CDManagerBean.editCD}"> <h:outputText value="#{cd.title}"/> </h:commandLink> </h:column> panelGroup 组件 注意一下清单 16,链接是在标题列的 header facet 中定义的。facet 只会关联一个惟一名字的组件;这样,要在 header facet 中放置一个多链接的组件,您需要使用 panelGroup。panelGroup(与 panelGrid类似)是一个单独的组件,其中包含了很多子组件。panelGroup包含两个链接,如清单 17 所示。 清单 17. panelGroup组件链接 [listing.jsp] <h:commandLink styleClass="smallLink" action="#{CDManagerBean.sortTitleAsc}"> <h:outputText id="ascTitle" value="asc"/> </h:commandLink> ... <h:commandLink styleClass="smallLink" action="#{CDManagerBean.sortTitleDec}"> <h:outputText id="decTitle" value="dec"/> </h:commandLink> 第一个链接被绑定到控制程序的 sortTitleAsc方法上,第二个链接被绑定到 sortTitleDec上。这两个方法如清单 18 所示。 清单 18. panelGroup链接方法 [StoreController.java ] /** * Uses the store delegate to return * a sorted list of CDs by title (ascending). * * @return asc */ public String sortTitleAsc() { this.cdModel.setWrappedData(store.findTitleAsc()); return "asc"; } /** * Uses the store delegate to return * a sorted list of CDs by title (descending). * * @return dec */ public String sortTitleDec() { this.cdModel.setWrappedData(store.findTitleDec()); return "dec"; } 这两个方法都依赖于业务代理返回一个按照正确要求排序后的 java.util.List。注意这个方法会返回逻辑输出 asc和 dec。这两个输出在 faces-config.xml 文件中都没有映射。没有映射的输出会导致重新加载当前的视图;这样,listing.jsp 将会在调用这些方法时重新进行加载,清单页面也会按照正确的顺序重新显示。 这种方法的优点是它依赖于业务代理进行排序。业务代理又可能会依赖于一个 DAO 对象,而后者又依赖于一个数据库查询或 OR 映射查询,这样可以对 CD 进行有效的查询。这种方法通常比具有一个“智能” GUI 组件的方法更好,后一种方法知道如何对随机的域对象(CD 就是一个域对象)进行排序,因为排序操作是一个经常发生的操作,严格来说,是模型的一部分(即域对象的一部分),而不是视图的一部分。 正如前面介绍的一样,对标题进行排序和对艺术家进行排序的代码几乎是相同的。作为一个练习,请自己试图为第四个使用案例编写代码,对艺术家而不是标题进行排序。 回页首 即时事件处理 我们要介绍的最后一个主题是即时事件处理。即时事件处理在您不希望(或需要)对整个页面进行有效性验证来处理用户输入的情况中非常有用。回想一下,示例程序的 cdForm.jsp 页面使用单选按钮来显示一个目录和子目录清单。当最终用户选择一个目录时,cdForm.jsp 页面就会使用 JavaScript 重新生成表单,这样就可以显示子目录清单了。 这是一个即时事件处理的例子,因为整个表单 没有在调用事件处理程序之前进行有效性验证。相反,类清单的事件处理程序会生成子目录,并强制 JSF 跳过进行响应的阶段。组件的事件处理程序通常都是在调用程序阶段执行的。即时事件组件的事件处理程序是在应用请求值阶段执行的,这发生在其余组件的类型转换和有效性验证之前。 清单 19 显示了在 cdForm.jsp 页面中再次显示的目录清单。 清单 19. cdForm.jsp 中的目录清单 [cdForm.jsp] <h:selectOneRadio id="category" value="#{CDManagerBean.cd.category}" immediate="true" onclick="submit()" valueChangeListener="#{CDManagerBean.categorySelected}"> <f:selectItems value="#{CDManagerBean.categories}"/> </h:selectOneRadio> selectOneRadio目录域被绑定到 CD的目录属性(value="#{CDManagerBean.cd.category}")上。注意这个即时事件处理被激活了(immediate="true")。这种设置意味着 Category组件的事件会在应用值阶段(而不是在调用程序阶段)进行处理(以及类型转换和有效性验证)。 JavaScript 功能是在 onclick="submit()"这一行 ―― 即当用户进行修改时,它应该立即被提交到 Web 程序中进行处理。 事件处理程序方法 在清单中显示的可用分类是由 f:selectItems标签值(value="#{CDManagerBean.categories}")确定的。这个组件的事件处理程序的变化是控制程序的 categorySelected()方法(valueChangeListener="#{CDManagerBean.categorySelected}")。事件处理程序如清单 20 所示。 清单 20. categorySelected事件处理程序 [StoreController.java] /** * Event Handler for a category getting selected. * * @param event event data */ public void categorySelected(ValueChangeEvent event) { subCategoryList.setRendered(true); String value = (String) event.getNewValue(); if (value != null) { this.subCategories = this.getSubcategoriesList(value); } FacesContext context = FacesContext.getCurrentInstance(); context.renderResponse(); } categorySelected()方法做的第一件事情是允许 subCategoryList调用自己。categorySelected()方法然后会使用所选择的分类值来查找一个 subCategories清单。subCategories属性被绑定到 subcategoryList值上。接下来,事件处理程序通过调用当前 FacesContext上的 renderResponse()方法强制 JSF 转到进行响应阶段。然后,GUI(cdForm.jsp)为当前显示的目录重新显示可用的子目录。 将组件绑定到控制程序上 subCategoryList组件是从 GUI 上绑定的。正如您可以将值绑定到组件上一样,您也可以将这些组件绑定到一个控制程序上。子目录是在 cdForm.jsp 页面中定义的,如清单 21 所示。 清单 21. 在 cdForm.jsp 页面中定义的子目录清单 [cdForm.jsp] <h:selectOneListbox id="subcategory" value="#{CDManagerBean.cd.subCategory}" binding="#{CDManagerBean.subCategoryList}"> <f:selectItems value="#{CDManagerBean.subCategories}"/> </h:selectOneListbox> binding属性允许您将 GUI 的组件绑定到后端的 bean(控制程序)上。这样,上面的组件就会被绑定到 CDManagerBean.subCategoryList上,这是在清单 22 中定义的控制程序中的一个属性。 清单 22. subCategoryList属性 [StoreController.java ] /** GUI Component that represents the Subcategory list on the CDForm. */ private UIInput subCategoryList; { subCategoryList = new HtmlSelectOneListbox(); } /** * Subcategory list component * * @param aSubCategoryList The subCategoryList to set. * * @uml.property name="subCategoryList" */ public void setSubCategoryList(UIInput aSubCategoryList) { this.subCategoryList = aSubCategoryList; } /** * Subcategory list component * * @return Returns the subCategoryList. * * @uml.property name="subCategoryList" */ public UIInput getSubCategoryList() { return subCategoryList; }?