Skip to content

2. Java Server Faces

接下来我们将介绍 Java Server Faces 框架。虽然将使用 2.0 版本,但示例主要展示 1.0 版本的特性。对于 2.0 版本,我们将仅介绍后续示例应用所需的特性。

2.1. JSF在Web应用中的定位

首先,让我们明确 JSF 在 Web 应用程序开发中的定位。通常,Web 应用程序会基于如下所示的多层架构构建:

  • [web]层是与Web应用程序用户接触的层。用户通过浏览器显示的网页与Web应用程序进行交互。JSF位于这一层,且仅位于这一层;
  • [métier]层实现应用程序的管理规则,例如工资计算或发票生成。该层通过[web]层使用来自用户的数据,并通过[DAO]层使用来自数据库管理系统(DBMS)的数据,
  • [DAO]层(数据访问对象)、[jpa]层(Java持久化API)以及JDBC驱动程序负责管理对数据库管理系统数据的访问。 [jpa]层作为ORM(对象关系映射器)使用。它在[DAO]层处理的对象与关系型数据库数据的行和列之间架起桥梁,
  • 各层的集成可通过 Spring 容器或 EJB3(企业级 Java Bean)实现。

下文为说明 JSF 而给出的示例,将仅使用一个层,即 [web] 层:

在掌握 JSF 的基础知识后,我们将构建多层 Java EE 应用程序。

2.2. MVC的开发模型

JSF 通过以下方式实现了所谓的 MVC 架构模式(模型-视图-控制器):

该架构实现了 MVC 设计模式(模型、视图、控制器)。客户端请求的处理遵循以下四个步骤:

  1. 请求 - 浏览器客户端向控制器 [Faces Servlet] 发起请求。该控制器处理所有客户端请求,是应用程序的入口点,即 MVC 中的 C
  2. 处理——控制器 C 处理该请求。为此,它借助了应用程序 [2a] 特有的事件处理程序。这些处理程序可能需要业务层 [2b] 的协助。 一旦处理了客户端的请求,该请求可能会触发各种响应。一个典型的例子是:
    • 如果请求未能正确处理,则显示错误页面;
    • 否则显示确认页面;
  3. 导航——控制器选择要发送给客户端的响应(即视图)。选择要发送给客户端的响应需要经过多个步骤:
    • 选择将生成响应的 Facelet。这被称为视图 V,即 MVC 中的 V。该选择通常取决于用户请求的操作执行结果;
    • 向该 Facelet 提供其生成响应所需的数据。实际上,响应通常包含由控制器计算得出的信息。这些信息构成了所谓的视图模型 M,即 MVC 中的 M,

因此,步骤3包括选择视图V并构建其所需的模型M

  1. 响应——控制器 C 请求所选的 Facelet 进行渲染。Facelet 使用控制器 C 准备的模型 M 来初始化响应中需发送给客户端的动态部分。 响应的具体形式可能多种多样:可以是 HTML 数据流、PDF 数据流、Excel 文件等。

在 JSF 项目中:

  • 控制器 C 是 Servlet [javax.faces.webapp.FacesServlet]。该 Servlet 位于库 [javaee.jar] 中,
  • 视图 V 由采用 Facelets 技术的页面实现,
  • M模型和事件处理器由Java类实现,这些类通常被称为“后端Bean”或简称为Bean

现在,让我们明确 MVC Web 架构与分层架构之间的关系。这是两个不同的概念,但有时会被混淆。以一个单层 JSF Web 应用程序为例:

如果我们将 [web] 层与 JSF 层合并实现,虽然形成了 MVC 架构,但这并非多层架构。 在此情况下,[web] 层将包揽所有工作:呈现、业务逻辑、数据访问。而在 JSF 架构中,这些工作将由 Bean 负责。

现在,让我们考虑一种多层Web架构:

[web]层可以在不使用框架且不遵循MVC模型的情况下实现。这样虽然确实形成了一个多层架构,但Web层并未实现MVC模型。

在 MVC 中,我们提到模型 M 即视图 V 的模型,即 c.a.d,即视图 V 所显示的所有数据。MVC 的模型 M 通常还有另一种定义:

许多作者认为,位于 [web] 层右侧的部分构成了 MVC 的模型 M。为避免歧义,我们将:

  • 当指代 [web] 层右侧的所有内容时,称之为“领域模型”;
  • 当指代视图V所显示的数据时,称为视图模型

下文中,“M模型”一词将专指视图V的模型。

2.3. 示例 mv-jsf2-01:JSF 项目的组成部分

前几个示例将仅限于使用 JSF 实现的 Web 层:

待掌握基础知识后,我们将研究采用多层架构的更复杂示例。

2.3.1. 项目生成

我们将使用 NetBeans 7 生成首个项目 JSF2。

  
  • 在 [1] 中,创建一个新项目,
  • 在 [2] 中,选择类别 [Maven] 和项目类型 [Web Application],
  • 在 [3] 中,指定新项目文件夹的父文件夹,
  • 在 [4] 中,为项目命名,
  • 在 [5] 中,选择服务器。使用 NetBeans 7 时,可选择 Apache Tomcat 或 GlassFish 服务器。两者的区别在于 GlassFish 支持 EJB(企业级 Java Bean),而 Tomcat 不支持。 我们的示例不会使用 EJB,因此这里可以选择任意服务器,
  • 在 [6] 中,我们选择 Java 6 Web 版本,
  • 在 [7] 中,是生成的项目。

让我们来查看项目的各个组成部分,并说明各自的作用。

  • 在 [1] 中:项目的各个分支:
    • [Web Pages]:将包含网页文件(.xhtml、.jsp、.html)、资源(图片、各类文档)、Web层配置以及JSF框架的配置;
    • [Source packages]:项目的 Java 类;
    • [Dependencies]:项目所需的、由Maven框架管理的.jar归档文件;
    • [Java Dependencies]:项目所需的、但未由Maven框架管理的.jar文件;
    • [Project Files]:Maven 和 NetBeans 的配置文件;
  • 在 [2] 中:分支 [Web Pages],

其中包含以下页面 [index.jsp]:


<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/HTML4/loose.dtd">

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>JSP Page</title>
    </head>
    <body>
        <h1>Hello World!</h1>
    </body>
</html>

这是一个以大号字体显示字符串“Hello World”的网页。

文件 [META-INF/context.xml] 如下:


<?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true" path="/mv-jsf2-01"/>

第 2 行表明应用程序上下文(或其名称)为 /mv-jsf2-01。这意味着该项目的网页将通过形式为 http://machine:port/mv-jsf2-01/page 的 URL 进行请求。上下文默认即为项目名称。我们无需修改此文件。

  • 在 [3] 中,分支 [Source Packages],

该分支包含项目中 Java 类的源代码。此处没有类。NetBeans 生成了一个默认包,可以将其删除:[4]。

  • 在 [5] 分支中,[Dependencies] 分支,

该分支显示了项目所需且由 Maven 管理的所有库。此处列出的所有库都将由 Maven 自动下载。这就是为什么 Maven 项目需要互联网连接。下载的库将存储在本地。如果另一个项目需要本地已存在的库,则该库将不会被下载。 我们将看到,该库列表及其所在的仓库均在 Maven 项目的配置文件中定义。

  • 在 [6] 中,项目所需的、且未由 Maven 管理的库,
  • 在 [7] 中,是 Maven 项目的配置文件:
    • [nb-configuration.xml] 是 NetBeans 的配置文件。我们在此不予讨论。
    • [pom.xml]:Maven配置文件。POM代表Project Object Model。有时我们需要直接修改此文件。

生成的文件 [pom.xml] 如下:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-jsf2-01</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>mv-jsf2-01</name>

  <properties>
    <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-web-api</artifactId>
      <version>6.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
          <compilerArguments>
            <endorseddirs>${endorsed.dir}</endorseddirs>
          </compilerArguments>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.1.1</version>
        <configuration>
          <failOnMissingWebXml>false</failOnMissingWebXml>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <version>2.1</version>
        <executions>
          <execution>
            <phase>validate</phase>
            <goals>
              <goal>copy</goal>
            </goals>
            <configuration>
              <outputDirectory>${endorsed.dir}</outputDirectory>
              <silent>true</silent>
              <artifactItems>
                <artifactItem>
                  <groupId>javax</groupId>
                  <artifactId>javaee-endorsed-api</artifactId>
                  <version>6.0</version>
                  <type>jar</type>
                </artifactItem>
              </artifactItems>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

</project>
  • 第 5-8 行定义了将由 Maven 项目创建的 Java 对象(工件)。这些信息来自创建项目时使用的向导:

一个 Maven 对象由四个属性定义:

  • [groupId]:类似于包名的信息。 因此,Spring 框架的库具有 groupId=org.springframework,QZXW2HTML 框架的库具有 JSF=groupId=javax.faces,
  • [artifactId]:Maven 对象的名称。 在 [org.springframework] 组中,包含以下 artifactId:spring-contextspring-corespring-beans、…… 在 [javax.faces] 组中,包含 artifactId 和 jsf-api,
  • [version]:Maven 构建成果的版本号。 因此,org.springframework.spring-core 构建产物具有以下版本:2.5.4、2.5.5、2.5.6、2.5.6.SECO1,……
  • [packaging]:工件的格式,通常为 war jar

因此,我们的 Maven 项目将在 [istia.st] 组(第 5 行)中生成一个 [war](第 8 行),其名称为 [mv-jsf2-01](第 6 行),版本号为 [1.0-SNAPSHOT] (第7行)。这四项信息必须唯一地定义一个Maven工件。

第 17-24 行列出了 Maven 项目的依赖项,即项目所需的库列表。 每个库由四项信息定义(groupId、artifactId、版本、打包方式)。当packaging信息缺失(如本例所示)时,将使用packaging jar文件。 此外还添加了另一个信息“scope”,用于确定在项目生命周期的哪些阶段需要该库。默认值为“compile”,表示该库在编译和运行时均需使用。值“provided”表示该库仅在编译时需要,运行时则不需要。在此示例中,运行时将由 Tomcat 7 服务器提供该库。

2.3.2. 项目运行

我们运行该项目:

在 [1] 中,Maven 项目被执行。如果 Tomcat 服务器尚未启动,此时将启动它。浏览器也会随之启动,并请求项目上下文中的 URL(即 [2])。 由于未请求任何文档,因此若存在 index.htmlindex.jspindex.xhtml 页面,则使用其中之一。在此情况下,将使用 [index.jsp] 页面。

2.3.3. Maven 项目的文件系统

  • [1]:项目的文件系统位于 [Files] 选项卡中,
  • [2]:Java源代码位于[src / main / java]文件夹中,
  • [3]:网页位于文件夹 [src / main / webapp] 中,
  • [4]:文件夹 [target] 由项目构建(build)生成,
  • [5]:在此,项目构建生成了一个名为 [mv-jsf2-01-1.0-SNAPSHOT.war] 的归档文件。正是该归档文件被 Tomcat 服务器执行。

2.3.4. 为 JSF 配置项目

我们当前的项目并非 JSF 项目。它缺少 JSF 框架的库。要将当前项目转换为 JSF 项目,请按以下步骤操作:

  • 在 [1] 中,进入项目属性,
  • 在 [2] 中,选择类别 [Frameworks],
  • 在 [3] 中,添加一个框架,
  • 在 [4] 中,选择 Java Server Faces,
  • 在 [5] 中,NetBeans 向我们推荐了该框架的 2.1 版本。我们接受该建议,
  • 在 [6] 中,项目因此增加了新的依赖项。

文件 [pom.xml] 已更新以反映此新配置:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-jsf2-01</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>mv-jsf2-01</name>

  ...
  <dependencies>
    <dependency>
      <groupId>com.sun.faces</groupId>
      <artifactId>jsf-api</artifactId>
      <version>2.1.1-b04</version>
    </dependency>
    <dependency>
      <groupId>com.sun.faces</groupId>
      <artifactId>jsf-impl</artifactId>
      <version>2.1.1-b04</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.1.2</version>
    </dependency>
    <dependency>
      <groupId>taglibs</groupId>
      <artifactId>standard</artifactId>
      <version>1.1.2</version>
    </dependency>
    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-web-api</artifactId>
      <version>6.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    ...
  </build>
  <repositories>
    <repository>
      <URL>http://download.java.net/maven/2/</URL>
      <id>jsf20</id>
      <layout>default</layout>
      <name>Repository for library Library[jsf20]</name>
    </repository>
    <repository>
      <URL>http://repo1.maven.org/maven2/</URL>
      <id>jstl11</id>
      <layout>default</layout>
      <name>Repository for library Library[jstl11]</name>
    </repository>
  </repositories>
</project>

第 14-33 行,已添加新的依赖项。Maven 会自动下载它们。它会从所谓的仓库中获取这些依赖项。默认会自动使用中央仓库(Central Repository)。可以通过 <repository> 标签添加其他仓库。此处已添加了两个仓库:

  • 第 46-51 行:用于库 JSF 2 的仓库,
  • 第 52-57 行:用于库 JSTL 1.1 的仓库。

该项目还新增了一个网页:

[index.HTML] 页面内容如下:


<?xml version='1.0' encoding='UTF-8' ?>
<!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">
  <h:head>
    <title>Facelet Title</title>
  </h:head>
  <h:body>
    Hello from Facelets
  </h:body>
</html>

这里有一个名为 XML 的文件(第 1 行)。其中包含了 HTML 的标签,但采用的是 XML 的格式。我们称之为 XHTML。 使用 JSF 2 创建网页的技术称为 Facelets。因此,XHTML 页面有时也被称为 Facelet 页面。

第 3-4 行定义了带有 XML 命名空间(xmlns=XML Name Space)的 <html> 标签。

  • 第 3 行定义了主命名空间 http://www.w3.org/1999/xhtml
  • 第4行定义了http://java.sun.com/jsf/html命名空间,用于HTML标签。这些标签将带有h:前缀,如xmlns:h所示。这些标签位于第5、7、8和10行。

遇到命名空间声明时,Web 服务器将遍历应用程序中 [META-INF] 和 Classpath 目录,查找后缀为 .tld 的文件(TagLib Definition)。 在此,它将在 [jsf-impl.jar] 和 [1,2] 归档中找到它们:

让我们查看 [3] 文件 [HTML_basic.tld]:

<?xml version="1.0" encoding="UTF-8"?>

<taglib xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd" version="2.1">

<!-- ============== 标签库描述元素 ============= -->

    <description>
        This tag library contains JavaServer Faces component tags for all
        UIComponent + HTML RenderKit Renderer combinations defined in the
        JavaServer Faces Specification.
    </description>
    <tlib-version>
        2.1
    </tlib-version>
    <short-name>
        h
    </short-name>
    <uri>
        http://java.sun.com/jsf/html
    </uri>

<!-- ============== 标签库验证器 ============= -->
...
  • 第19行,标签库的URI
  • 第 16 行是其短名称。

该文件中包含各种 <h:xx> 标签的定义。这些标签由 Java 类管理,这些类同样位于 [jsf-impl.jar] 构建产物中。

让我们回到我们的项目 JSF。该项目新增了一个分支:

分支 [Other Sources] [1] 包含必须位于项目类路径中且非 Java 代码的文件。JSF 中的消息文件即属于此类。 我们已看到,若未将框架 JSF 添加到项目中,该分支将不存在。 要创建该分支,只需在“[Files] [2]”选项卡中创建“[src / main / resources] [3]”文件夹即可。

最后,在分支 [Web Pages] 中出现了一个新文件夹:

创建了文件夹 [WEB-INF],其中包含文件 [web.xml] 。该文件用于配置 Web 应用程序:


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <URL-pattern>/faces/*</URL-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>
  • 第 7-10 行定义了一个 Servlet,即 c.a.d,这是一个能够处理客户端请求的 Java 类。JSF 应用程序的工作原理如下:

该架构实现了 MVC 设计模式(模型、视图、控制器)。我们回顾一下前文所述内容。处理客户端请求的过程分为以下四个步骤:

1 - 请求 - 客户端浏览器向控制器 [Faces Servlet] 发起请求。该控制器处理所有客户端请求,是应用程序的入口点,即 MVC 中的 C

2 - 处理 - 控制器 C 处理该请求。为此,它借助了应用程序 [2a] 特有的事件处理程序。这些处理程序可能需要业务层 [2b] 的协助。 处理完客户端请求后,可能会返回多种响应。一个典型的例子是:

  • 如果请求未能正确处理,则显示错误页面;
  • 否则显示确认页面;

3 - 导航 - 控制器选择要发送给客户端的响应(即视图)。选择要发送给客户端的响应需要经过多个步骤:

  • 选择将生成响应的 Facelet。这被称为视图 V,即 MVC 中的 V。该选择通常取决于用户请求的操作执行结果;
  • 向该 Facelet 提供其生成响应所需的数据。实际上,响应通常包含由控制器计算得出的信息。这些信息构成了所谓的视图模型 M,即 MVC 中的 M,

因此,步骤3包括选择视图V并构建其所需的模型M

4 - 响应 - 控制器 C 请求所选的 Facelet 进行渲染。Facelet 使用控制器 C 准备的模型 M 来初始化响应中需发送给客户端的动态部分。 响应的具体形式可能多种多样:可以是 HTML 数据流、PDF 数据流、Excel 文件等。

在 JSF 项目中:

  • 控制器 C 是 Servlet [javax.faces.webapp.FacesServlet],
  • 视图 V 由采用 Facelets 技术的页面实现,
  • M模型和事件管理器由Java类实现,这些类通常被称为“后端Bean”或简称为Bean。

让我们回顾一下文件 [web.xml] 的内容:


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <URL-pattern>/faces/*</URL-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>
  • 第 12-15 行:<servlet-mapping> 标签用于将一个 Servlet 与客户端浏览器请求的 URL 关联起来。 此处指定,形式为 [/faces/*] 的 URL 请求应由名为 [Faces Servlet] 的 Servlet 处理。该 Servlet 在第 7-10 行中定义。 由于文件中没有其他 <servlet-mapping> 标签,这意味着 [Faces Servlet] Servlet 仅处理形式为 [/faces/*] 的 URL 请求。 我们看到应用程序上下文名为 [/mv-jsf2-01]。因此,由 [Faces Servlet] Servlet 处理的客户端 URL 将采用 [http://machine:port/mv-jsf2-01/faces/*] 的形式。 .html 和 .jsp 页面默认由 Servlet 容器本身处理,而非由特定的 Servlet 处理。因为 Servlet 容器知道如何管理它们,
  • 第7-10行:定义Servlet [Faces Servlet]。由于所有接收到的URL请求都会被转发至该Servlet,因此它作为模型MVC的控制器C,
  • 第10行:表示该Servlet必须在Web服务器启动时立即加载到内存中。默认情况下,Servlet仅在收到针对它的第一个请求时才会被加载,
  • 第3-6行:定义了针对Servlet [Faces Servlet]的参数。 参数 javax.faces.PROJECT_STAGE 定义了正在运行的项目所处的阶段。在 Development 阶段,Servlet [Faces Servlet] 会显示有助于调试的错误信息。在 Production 阶段,这些信息将不再显示,
  • 第17-19行:会话时长(以分钟为单位)。客户端通过一系列请求/响应循环与应用程序交互。每个循环都使用专属的连接 TCP-IP,且每个新循环都会建立新的连接。 因此,如果客户端 C 发出了两个请求 D1 和 D2,服务器 S 无法识别这两个请求属于同一个客户端 C。服务器 S 并不存储客户端的记忆。 这是所使用的 HTTP 协议(HyperText 传输协议)所决定的:客户端通过一系列客户端请求/服务器响应的循环与服务器进行交互,每次都使用新的 TCP-IP 连接。这被称为无状态协议。 而在其他协议中,例如 FTP(文件传输协议),客户端 C 在与服务器 S 交互期间始终使用同一连接。因此,一个连接与特定的客户端相关联。服务器 S 始终知道自己正在与谁交互。为了能够识别某个请求属于特定的客户端,Web 服务器可以使用会话技术:
    • 当客户端首次发出请求时,服务器S会向其发送预期的响应以及一个令牌(即专属于该客户端的随机字符串);
    • 在随后的每次请求中,客户端 C 会将收到的令牌发回给服务器 S,从而使服务器 S 能够识别该客户端。

应用程序现在可以要求服务器存储与特定客户端相关的信息。这被称为客户端会话。 第18行表明会话的生命周期为30分钟。这意味着如果客户端C在30分钟内未发出新请求,其会话将被销毁,其中包含的信息也将丢失。在下一次请求时,系统将视其为新客户端,并启动新的会话;

  • 第21-23行:当用户请求上下文但未指定具体页面时(例如此处的[http://machine:port/mv-jsf2-01]),将显示的页面列表。 此时,Web 服务器(而非 Servlet)会检查应用程序是否定义了 <welcome-file-list> 标签。如果存在,则显示列表中找到的第一个页面;如果不存在,则显示第二个页面,以此类推,直到找到一个存在的页面。 在此示例中,当客户端请求 URL [http://machine:port/mv-jsf2-01] 时,实际返回的是 URL [http://machine:port/mv-jsf2-01/index.xhtml]。

2.3.5. 运行项目

运行新项目后,浏览器中显示的结果如下:

  • 在 [1] 中,请求上下文时未指定文档,
  • 在 [2] 中,如前所述,此时返回的是主页(welcome-file)[index.xhtml]。

出于好奇,我们可以查看收到的源代码 [3]:

1
2
3
4
5
6
7
<?xml version='1.0' encoding='UTF-8' ?>
<!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"><head>
    <title>Facelet Title</title></head><body>
    Hello from Facelets
  </body>
</html>

我们收到了 HTML。index.xhtml 中的所有 <h:xx> 标签均已转换为 HTML 中的对应标签。

2.3.6. 本地 Maven 仓库

我们提到过,Maven会下载项目所需的依赖项并将其存储在本地。我们可以浏览这个本地仓库:

  • 在 [1] 中,选择 [Window / Other / Maven Repository Browser] 选项,
  • 在 [2] 中,会打开一个名为 [Maven Repositories] 的标签页,
  • 在 [3] 中,包含两个分支,一个用于本地仓库,另一个用于中央仓库。后者规模庞大。要查看其内容,需更新其索引 [4]。此更新过程需耗时数十分钟。
  • 在 [5] 中,本地仓库的库,
  • 在 [6] 中,可找到一个分支 [istia.st],该分支对应于我们项目的 [groupId],
  • 在 [7] 中,可访问本地仓库的属性,
  • 在 [8] 中,可查看本地仓库的路径。了解该路径很有用,因为有时(虽然很少见)Maven 不再使用项目的最新版本。 进行修改后发现变更未被采纳。此时可以手动删除本地仓库中对应 [groupId] 的分支。这将强制 Maven 根据项目的最新版本重新创建该分支。

2.3.7. 使用 Maven 搜索工件

现在让我们学习如何使用 Maven 搜索工件。我们从文件 [pom.xml] 的当前依赖项列表开始:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>istia.st</groupId>
  <artifactId>mv-jsf2-01</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>mv-jsf2-01</name>

  ...
  <dependencies>
    <dependency>
      <groupId>com.sun.faces</groupId>
      <artifactId>jsf-api</artifactId>
      <version>2.1.1-b04</version>
    </dependency>
    <dependency>
      <groupId>com.sun.faces</groupId>
      <artifactId>jsf-impl</artifactId>
      <version>2.1.1-b04</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.1.2</version>
    </dependency>
    <dependency>
      <groupId>taglibs</groupId>
      <artifactId>standard</artifactId>
      <version>1.1.2</version>
    </dependency>
    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-web-api</artifactId>
      <version>6.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    ...
  </build>
  <repositories>
    <repository>
      <url>http://download.java.net/maven/2/</url>
      <id>jsf20</id>
      <layout>default</layout>
      <name>Repository for library Library[jsf20]</name>
    </repository>
    <repository>
      <url>http://repo1.maven.org/maven2/</url>
      <id>jstl11</id>
      <layout>default</layout>
      <name>Repository for library Library[jstl11]</name>
    </repository>
  </repositories>
</project>

第 13-40 行定义了依赖项,第 45-58 行则指定了可获取这些依赖项的仓库(除始终使用的中央仓库外)。我们将修改依赖项,以使用这些库的最新版本。

首先,我们删除当前的依赖项 [1]。随后修改文件 [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
...
    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
...
    <repositories>
        <repository>
            <url>http://download.java.net/maven/2/</url>
            <id>jsf20</id>
            <layout>default</layout>
            <name>Repository for library Library[jsf20]</name>
        </repository>
        <repository>
            <url>http://repo1.maven.org/maven2/</url>
            <id>jstl11</id>
            <layout>default</layout>
            <name>Repository for library Library[jstl11]</name>
        </repository>
    </repositories>
</project>

第 5-12 行,已删除的依赖项不再出现在 [pom.xml] 中。现在,让我们在 Maven 仓库中搜索它们。

  • 在 [1] 中,向项目添加了一个依赖项,
  • 在 [2] 中,必须指定所查找工件的相关信息(groupId、artifactId、版本、打包类型(Type)和范围)。 我们首先指定 [groupId] [3],
  • 在 [4] 处,我们输入 [espace] 以显示可能的工件列表。这里显示的是 [jsf-api] 和 [jsf-impl]。 我们选择 [jsf-api],
  • 在 [5] 中,采用相同方法,选择最新版本。打包类型为 jar

我们对所有构建产物都按此方式操作:

在 [6] 中,添加的依赖项已显示在项目中。文件 [pom.xml] 反映了这些更改:


<dependencies>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-api</artifactId>
            <version>2.1.7</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-impl</artifactId>
            <version>2.1.7</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>taglibs</groupId>
            <artifactId>standard</artifactId>
            <version>1.1.2</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

现在假设我们不知道所需工件的 [groupId]。例如,我们想使用 Hibernate 作为 ORM(对象关系映射器),而这就是我们所知道的全部信息。 此时可以访问 [http://mvnrepository.com/] 网站:

在 [1] 中,可以输入关键词。输入 hibernate 并启动搜索。

  • 在 [2] 中,选择 [groupId]、org.hibernate 以及 [artifactId]、hibernate-core,
  • 在 [3] 中,选择 4.1.2-Final 版本,
  • 在 [4] 中,我们获得了要粘贴到 [pom.xml] 文件中的 Maven 代码。我们照做。

<dependencies>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>4.1.2.Final</version>
    </dependency>
    <dependency>
      <groupId>com.sun.faces</groupId>
      <artifactId>jsf-api</artifactId>
      <version>2.1.7</version>
      <type>jar</type>
    </dependency>
    ...
  </dependencies>

我们将文件 [pom.xml] 保存。随后 Maven 开始下载新的依赖项。项目演变如下:

  • 在 [5] 中,依赖项为 [hibernate-core-4.1.2-Final]。在发现该 [artifactId] 的仓库中,它同样由一个名为 [pom.xml] 的文件描述。 该文件被读取后,Maven 发现 [artifactId] 存在依赖项,并将其一并下载。对于每个已下载的 [artifactId],Maven 都会重复这一操作。 最终,我们在 [6] 中发现了未直接请求的依赖项。这些依赖项通过与主文件 [artifactId] 不同的图标进行标识。

在本文档中,我们主要利用Maven的这一特性。这使我们无需了解所用库的所有依赖项,而是由Maven来管理。此外,通过在开发人员之间共享[pom.xml]文件,可以确保每位开发人员确实使用了相同的库。

在接下来的示例中,我们将仅提供所使用的 [pom.xml] 文件。读者只需使用该文件,即可与本文档保持一致的开发环境。 此外,Maven项目可被主流Java开发环境(Eclipse、NetBeans、IntelliJ、JDeveloper)识别。因此,读者可使用自己惯用的开发环境来测试这些示例。

2.4. 示例 mv-jsf2-02:事件管理器 – 国际化 – 页面导航

2.4.1. 应用程序

该应用程序如下:

  • 在 [1] 中,应用程序的首页,
  • 在 [2] 中,有两个用于更改应用程序页面语言的链接,
  • [3],一个跳转至其他页面的导航链接,
  • 点击 [3] 时,将显示页面 [4],
  • 链接 [5] 可返回首页。
  • 在首页 [1] 上,链接 [2] 可切换语言,
  • 在 [3] 页面中,主页为英文。

2.4.2. NetBeans 项目

将按照第 2.3.1 节所述生成一个新的 Web 项目。将其命名为 mv-jsf2-02

  • 在生成的项目 [1] 中,
  • 在 [2] 中,已删除包 [istia.st.mvjsf202] 和文件 [index.jsp],
  • 在 [3] 中,通过以下 [pom.xml] 文件添加了 Maven 依赖项:

<dependencies>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-api</artifactId>
            <version>2.1.7</version>
        </dependency>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-impl</artifactId>
            <version>2.1.7</version>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

新增的依赖项来自 JSF 框架。只需将上述内容复制到 [pom.xml] 文件中,替换掉原有的依赖项即可。

  • 在 [4, 5] 中:在 [Files] 选项卡中创建一个名为 [src / main / resources] 的文件夹,
  • 在 [Projects] 选项卡中创建 [6],这生成了分支 [Other Sources]。

现在我们有一个名为 JSF 的项目。我们将在此创建不同类型的文件:

  • XHTML格式的网页,
  • Java类,
  • 消息文件,
  • 以及项目配置文件 JSF。

下面让我们看看如何创建每种类型的文件:

  • 在 [1] 中,我们创建了一个名为 JSF 的页面
  • 在 [2] 中,我们创建了一个名为 [index.xhtml] 的页面,格式为 [Facelets] [3],
  • 在 [4] 中,已创建两个文件:[index.xhtml] 和 [WEB-INF / web.xml]。

文件 [web.xml] 用于配置应用程序 JSF。具体内容如下:


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <URL-pattern>/faces/*</URL-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>

我们在第 2.3.4 节中已对该文件进行了说明。现重申其主要特性:

  • 所有类型为 faces/* 的 URL 文件均由 [javax.faces.webapp.FacesServlet] Servlet 处理,
  • 页面 [index.xhtml] 是该应用程序的首页。

生成的 [index.xhtml] 文件如下:


<?xml version='1.0' encoding='UTF-8' ?>
<!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">
  <h:head>
    <title>Facelet Title</title>
  </h:head>
  <h:body>
    Hello from Facelets
  </h:body>
</html>

我们在第 2.3.4 节中已经遇到过这个文件。

现在创建一个 Java 类:

  • 在 [1] 中,我们在 [Source Packages] 分支下创建了一个 Java 类,
  • 在 [2] 中,为其命名并将其放入 [3] 包中,
  • 在 [4] 中,创建的类出现在项目中。

所创建类的代码是一个类骨架:


/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package istia.st;

/**
 *
 * @author Serge Tahé
 */
public class Form {
  
}

最后,创建一个消息文件:

  • 在 [1] 中,创建文件 [Properties],
  • 在 [2] 中,指定文件名,并在 [3] 中指定其文件夹,
  • 在 [4] 中,文件 [messages.properties] 已创建。

有时,需要创建文件 [WEB-INF/faces-config.xml] 来配置项目 JSF。 该文件在 JSF 1 中是必需的。在 JSF 2 中则是可选的。但如果 JSF 网站进行了国际化处理,则该文件必不可少。后续情况将属于此类。因此,我们现在将演示如何创建此配置文件。

  • 在 [1] 中,我们创建配置文件 JSF,
  • 在 [2] 中,为其指定名称,并在 [3] 中指定其文件夹,
  • 在 [4] 中,生成的文件。

生成的 [faces-config.xml] 文件内容如下:


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
    xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">


</faces-config>

根标签为 <faces-config>。该标签的正文为空。我们将需要对其进行补充。

现在我们已具备创建 JSF 项目的所有要素。在接下来的示例中,我们将展示完整的 JSF 项目,并逐一详细说明其各部分。现在我们将通过一个项目来解释以下概念:

  • 表单事件管理,
  • JSF网站页面的国际化,
  • 页面间的导航。

项目 [mv-jsf2-02] 如下所示。读者可在示例网站上找到该项目(参见第 1.2 节)。

  • 在 [1] 中,项目 JSF 的配置文件,
  • [2] 文件,即该项目的 JSF 页面,
  • [3],唯一的 Java 类,
  • [4],消息文件。

2.4.3. 页面 [index.xhtml]

文件 [index.xhtml] [1] 将页面 [2] 发送至客户端浏览器:

生成此页面的代码如下:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <f:view locale="#{changeLocale.locale}">
    <head>
      ...
    </head>
    <body>
      ....
    </body>
  </f:view>
</html>
  • 第 7-9 行:页面使用的命名空间/标签库。以 h 为前缀的标签属于 HTML,而以 f 为前缀的标签则是 JSF 特有的标签,
  • 第10行:<f:view>标签用于界定JSF引擎需处理的代码范围,即包含<f:xx>标签的代码段。local属性用于指定页面的显示语言。此处我们将使用两种语言:英语和法语。 locale 属性的值采用 EL 表达式语言(Expression Language)#{expression} 的形式。表达式的形式多种多样。我们通常将其表示为 bean['clé'] bean.champ 的形式。 在我们的示例中,bean 可以是 Java 类,也可以是消息文件。在 JSF 1 版本中,这些 bean 必须在 [faces-config.xml] 文件中声明。 在 JSF 2 中,对于 Java 类而言,这已不再是强制要求。现在可以使用注解,使一个 Java 类成为 JSF 2 所识别的 Bean。而消息文件则必须在配置文件 [faces-config.xml] 中进行声明。

2.4.4. [changeLocale] Bean

在表达式 EL #{changeLocale.locale} 中

  • changeLocale 是 Bean 的名称,此处指 Java 类 ChangeLocale,
  • locale 是类 ChangeLocale 中的一个字段。 该表达式由 [ChangeLocale].getLocale() 进行求值。通常情况下,表达式 #{bean.champ} 的求值结果为 [Bean]。getChamp(),其中 [Bean] 是 Java 类的实例,该类被命名为 bean 和 getChamp, 该bean中字段champ的关联getter。

ChangeLocale 类的定义如下:


package utils;

import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.enterprise.context.SessionScoped;

@ManagedBean
@SessionScoped
public class ChangeLocale implements Serializable{
  // 页面的语言
  private String locale="fr";
  
  public ChangeLocale() {
  }
  
  ...
  public String getLocale() {
    return locale;
  }
  
}
  • 第 11 行:局部字段,
  • 第 17 行:其 getter 方法,
  • 第 7 行:注解 ManagedBean 使 Java 类 ChangeLocale 成为 JSF 所识别的 Bean。 Bean 通过名称进行标识。该名称可通过注解的 name 属性设定:@ManagedBean(name= "xx ")。 若未指定该属性,则使用类名,并将类名的首字母转换为小写。因此,Bean ChangeLocale 的名称为 changeLocale。 需注意,注解 ManagedBean 属于包 javax.faces.bean.ManagedBean,而非包 javax.annotations.ManagedBean
  • 第 8 行:注解 SessionScoped 确定了 Bean 的作用域。此类注解有多种。我们通常会使用以下三种:
    • RequestScoped:Bean的生命周期与浏览器请求/服务器响应的循环周期一致。如果处理来自同一浏览器或另一台浏览器的新的请求时,再次需要该Bean,则会重新实例化它,
    • SessionScoped:Bean的生命周期与特定客户端的会话周期一致。该Bean最初为处理该客户端的某个请求而创建,随后将保留在该客户端的会话内存中。此类Bean通常存储特定客户端的专属数据,并在客户端会话结束时被销毁,
    • ApplicationScoped:Bean的生命周期与应用程序本身相同。具有此生命周期的Bean通常由应用程序的所有客户端共享。它通常在应用程序启动时初始化。

这些注解存在于两个包中:javax.enterprise.context.SessionScoped(JSF 2)和 javax.faces.bean.SessionScoped(JSF 1)。 在此,我们使用 JSF 2 包。这要求我们创建文件 [WEB-INF / beans.xml]:

  

当导入 [javax.enterprise.context.SessionScoped] 包时,该文件由 NetBeans 自动生成。其内容如下:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

除了根标签 <beans> 之外,该文件为空。这样就足够了,只需确保该文件存在即可。

最后需要注意的是,类 [ChangeLocale] 实现了接口 [Serializable]。对于作用域为 Session 的 Bean,这是必需的,因为 Web 服务器可能会将其序列化为文件。 我们稍后将再讨论 [ChangeLocale] Bean。

2.4.5. 消息文件

让我们回到 [index.xhtml] 文件:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <f:view locale="#{changeLocale.locale}">
    <head>
      <title><h:outputText value="#{msg['welcome.titre']}" /></title>
    </head>
    <body>
    ...
    </body>
  </f:view>
</html>
  • 第 8 行:标签 <h:outputText> 显示表达式 EL #{msg['welcome.titre']} 的值,其形式为 #{bean['champ']}。 bean 可以是 Java 类的名称,也可以是消息文件的名称。此处指的是消息文件的名称。该文件必须在配置文件 [faces-config.xml] 中进行声明。bean msg 的声明如下:

<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">


  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
  </application>
</faces-config>
  • 第 11-18 行:<application> 标签用于配置应用程序 JSF,
  • 第 12-17 行:<resource-bundle> 标签用于定义应用程序的资源,此处为消息文件,
  • 第 13-15 行:<base-name> 标签定义了消息文件的名称,
  • 第14行:该文件将命名为 messages[_CodeLangue][_CodePays].properties。<base-name>标签仅定义名称的前缀部分,其余部分为默认值。可能存在多个消息文件,每种语言对应一个:
  • 在 [1] 中,可以看到四个消息文件,它们对应于在 [faces-config.xml] 中定义的消息基名,
    • messages_fr.properties:包含法语消息(代码 fr);
    • messages_en.properties:包含英语消息(代码 en);
    • messages_es_ES.properties:包含西班牙语(代码 es)的消息,对应西班牙(代码 ES)。还有其他类型的西班牙语,例如玻利维亚的西班牙语(es_BO);
    • messages.properties:当服务器运行的机器语言没有关联的消息文件时,服务器会使用此文件。例如,如果应用程序在德国的机器上运行,且默认语言为德语(de),则会使用此文件。 由于不存在 [messages_de.properties] 文件,应用程序将使用 [messages.properties] 文件,
  • 在 [2] 中:语言代码遵循国际标准,
  • 在 [3] 中:国家代码亦同。

消息文件的名称在第14行定义。系统将在项目的Classpath文件中查找该文件。如果该文件位于某个包内, 则需在第14行定义该包,例如ressources.messages,若文件[messages.properties]位于Classpath的[ressources]文件夹中。 由于第14行的名称中不包含包名,文件[messages.properties]应放置在文件夹[src / main / resources]的根目录下:

在 [1] 中,NetBeans 项目的 [Projects] 选项卡中,文件 [messages.properties] 显示为已定义的各种消息版本列表。 各版本通过一到三个代码的序列进行标识,例如 [codeLangue_codePays_codeVariante]。 在 [1] 中,仅使用了 [codeLangue] 代码:en 代表英语,fr 代表法语。每个版本在文件系统中都有一个单独的文件。

在本例中,法语消息文件 [messages_fr.properties] 将包含以下内容:


welcome.titre=Tutoriel JSF (JavaServer Faces)
welcome.langue1=Fran\u00e7ais
welcome.langue2=Anglais
welcome.page1=Page 1
page1.titre=page1
page1.entete=Page 1
page1.welcome=Page d'accueil

而文件 [messages_en.properties] 的内容如下:


welcome.titre=JSF (JavaServer Faces) Tutorial
welcome.langue1=French
welcome.langue2=English
welcome.page1=Page 1
page1.titre=page1
page1.entete=Page 1
page1.welcome=Welcome page

文件 [messages.properties] 与文件 [messages_en.properties] 完全相同。最终,客户端浏览器将可在法语页面和英语页面之间进行选择。

让我们回到声明消息文件的 [faces-config.xml] 文件:


...

  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
  </application>
</faces-config>

第 8 行表明,消息文件中的一行将在 JSF 页面中通过标识符 msg 进行引用。该标识符在之前分析的 [index.xhtml] 文件中被使用:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <f:view locale="#{changeLocale.locale}">
    <head>
      <title><h:outputText value="#{msg['welcome.titre']}" /></title>
    </head>
    <body>
      ...
    </body>
  </f:view>
</html>

第8行的<h:outputText>标签将显示键welcome.titre对应的消息值(即包含标识符msg的消息)。 该消息将在当前活动语言的 [messages.properties] 文件中被查找并找到。例如,对于法语:


welcome.titre=Tutoriel JSF (JavaServer Faces)

一条消息的格式为键=值。在评估表达式 #{msg['welcome.titre']} 后,文件 [index.xhtml] 的第 8 行变为:


      <title><h:outputText value="Tutoriel JSF (JavaServer Faces)" /></title>

这种消息文件机制使得更改 JSF 项目中页面的语言变得非常简单。这被称为项目的国际化,或更常使用其缩写 i18n,因为“internationalisation”一词以 i 开头,以 n 结尾,且 i n 之间有 18 个字母。

2.4.6. 表单

让我们继续探索 [index.xhtml] 文件的内容:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <f:view locale="#{changeLocale.locale}">
    <head>
      <title><h:outputText value="#{msg['welcome.titre']}" /></title>
    </head>
    <body>
      <h:form id="formulaire">
        <h:panelGrid columns="2">
          <h:commandLink value="#{msg['welcome.langue1']}" action="#{changeLocale.setFrenchLocale}"/>
          <h:commandLink value="#{msg['welcome.langue2']}" action="#{changeLocale.setEnglishLocale}"/>
        </h:panelGrid>
        <h1><h:outputText value="#{msg['welcome.titre']}" /></h1>
        <h:commandLink value="#{msg['welcome.page1']}" action="page1"/>
      </h:form>
    </body>
  </f:view>
</html>
  • 第11-18行:<h:form>标签引入了一个表单。表单通常由以下内容组成:
    • 输入字段标签(文本框、单选按钮、复选框、下拉列表等);
    • 表单提交标签(按钮、链接)。用户通过按钮或链接将输入内容发送至服务器进行处理,

任何 JSF 标签均可通过 id 属性进行标识。通常情况下,该属性并非必需,本文中使用的绝大多数 JSF 标签均未使用该属性。不过,在某些情况下,该属性仍具有实用价值。 第 17 行,表单通过 id 属性进行标识。在此示例中,表单的 id 不会被使用,因此本可以省略。

  • 第18-21行:此处的<h:panelGrid>标签定义了一个两列的表格。它生成了<table>标签,
  • 表单包含三个触发处理的链接,分别位于第19、20和23行。<h:commandLink>标签至少具有两个属性:
    • value:链接的文本;
    • action:要么是一个字符串 C,要么是一个方法的引用,该方法执行后返回字符串 C。该字符串 C 可以是:
      • 项目中名为 JSF 的页面名称,
      • 或是文件 [faces-config.xml] 的导航规则中定义的名称,且与项目中的页面 JSF 相关联;

无论哪种情况,一旦由 action 属性定义的操作执行完毕,页面 JSF 都会被显示出来。

让我们通过第 13 行链接的示例来探讨表单处理的机制:


         <h:commandLink value="#{msg['welcome.langue1']}" action="#{changeLocale.setFrenchLocale}"/>}"/>

首先,系统会调用消息文件,将表达式 #{msg['welcome.langue1']} 替换为其值。评估后,该标签变为:


<h:commandLink value="Français" action="#{changeLocale.setFrenchLocale}"/>}"/>

该标签 JSF 的翻译 HTML 将如下所示:


<a href="#" onclick="mojarra.jsfcljs(document.getElementById('formulaire'),{'formulaire:j_idt8':'formulaire:j_idt8'},'');return false">Français</a>

这将呈现如下视觉效果:

请注意 HTML 标签的 onclick 属性 <a>。当用户点击 [Français] 链接时,将执行一段 JavaScript 代码。该代码嵌入在浏览器接收到的页面中,并由浏览器执行。 JavaScript 代码在 JSF 和 AJAX(异步 JavaScript 和 XML)技术中被广泛使用其主要目的是提升 Web 应用程序的用户体验和响应速度。 它通常由软件工具自动生成,因此无需刻意理解。但有时开发人员可能需要在 JSF 页面中添加 JavaScript 代码,此时就需要掌握 JavaScript 知识。

在此无需理解为标签<h:commandLink>生成的JavaScript代码。但有两点值得注意:

  • JavaScript代码使用了我们为<h:form>标签指定的表单标识符,
  • JSF 会为所有未定义 id 属性的标签自动生成标识符。此处可见一个示例:j_idt8。 为标签赋予清晰的标识符,有助于在必要时更好地理解生成的 JavaScript 代码。尤其当开发人员需要自行添加操作页面组件的 JavaScript 代码时,就必须了解这些组件的 id 标识符。

当用户点击上文页面中的链接 [Français] 时,会发生什么?让我们来分析一下应用程序 JSF 的架构:

控制器 [Faces Servlet] 将收到客户端浏览器发来的请求,其格式如下所示:

1
2
3
4
5
6
POST /mv-jsf2-02/faces/index.xhtml HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-URLencoded
Content-Length: 126

formulaire=formulaire&javax.faces.ViewState=-9139703055324497810%3A8197824608762605653&formulaire%3Aj_idt8=formulaire%3Aj_idt8 
  • 第 1-2 行:浏览器请求 URL [http://localhost:8080/mv-jsf2-02/faces/index.xhtml]。 情况总是如此:最初通过 URL URLFormulaire 获取的 JSF 表单中的输入内容,会被发送至该 URL。 浏览器有两种方式发送输入的值:GET POST。使用 GET 方法时,浏览器会将输入的值发送至被请求的 URL。 在上文中,浏览器本可以发送以下第一行:

GET /mv-jsf2-02/faces/index.xhtml?formulaire=formulaire&javax.faces.ViewState=-9139703055324497810%3A8197824608762605653&formulaire%3Aj_idt8=formulaire%3Aj_idt8 HTTP/1.1

在此使用的 POST 方法中,浏览器通过第 6 行将输入的值发送给服务器。

  • 第 3 行:指定表单值的编码格式,
  • 第 4 行:指定第 6 行的字节大小,
  • 第 5 行:空行,表示 HTTP 头部的结束以及 126 字节表单值的开始,
  • 第6行:表单值以 element1=值1&element2=值2& ... 的形式呈现,其编码格式由第3行定义。在此编码格式中,某些字符会被替换为相应的十六进制值。最后一个元素便是如此:

formulaire=formulaire&javax.faces.ViewState=...&formulaire%3Aj_idt8=formulaire%3Aj_idt8

其中 %3A 代表字符 :。因此,发送至服务器的字符串为:formulaire:j_idt8=formulaire:j_idt8。 大家可能还记得,在分析为


          <h:commandLink value="#{msg['welcome.langue1']}" action="#{changeLocale.setFrenchLocale}"/>

该标识符是由 JSF 自动生成的。这里的关键在于,当该标识符出现在客户端浏览器发送的值字符串中时,JSF 便能得知链接 [Français] 已被点击。 随后,它将利用上述 action 属性,来决定如何处理收到的字符串。 action="#{changeLocale.setFrenchLocale}" 属性告知 JSF,客户端的请求应由名为 changeLocale 的对象的 [setFrenchLocale] 方法进行处理。 需要指出的是,该 Bean 是通过 Java 类 [ChangeLocale] 中的注解定义的:


@ManagedBean
@SessionScoped
public class ChangeLocale implements Serializable{

Bean 的名称由 @ManagedBean 注解的 name 属性定义。如果缺少该属性,则使用类名作为 Bean 名称,并将首字母转换为小写。

让我们回到浏览器的请求:

以及生成了链接 [Français] 的 <h:commandLink> 标签,我们点击了该链接:


          <h:commandLink value="#{msg['welcome.langue1']}" action="#{changeLocale.setFrenchLocale}"/>

控制器将把浏览器的请求转发给由 <h:commandLink> 标签的 action 属性定义的事件处理程序。由 <h:commandLink> 命令的 action 属性引用的事件处理程序 M 必须具有以下签名:

public String M();
  • 它不接收任何参数。我们将看到,尽管如此,它仍可访问客户端的请求,
  • 它必须返回类型为 String 的结果 C。该字符串 C 可以是:
    • 项目中某个页面 JSF 的名称;
    • 或是文件 [faces-config.xml] 的导航规则中定义的名称,且与项目中的某个页面 JSF 相关联;
    • 或者是一个空指针,表示客户端浏览器无需切换页面,

在上述 JSF 架构中, 控制器 [Faces Servlet] 将使用事件处理程序返回的字符串 C,并可能结合其配置文件 [faces-config.xml],来确定应向客户端发送哪个页面 JSF, 作为响应发送给客户端 [4]。


          <h:commandLink value="#{msg['welcome.langue1']}" action="#{changeLocale.setFrenchLocale}"/>

中,点击链接事件处理程序[Français]是方法[changeLocale.setFrenchLocale],其中changeLocale是已研究过的类[utils.ChangeLocale]的实例


package utils;

import java.io.Serializable;
import javax.enterprise.context.SessionScoped;
import javax.faces.bean.ManagedBean;

@ManagedBean
@SessionScoped
public class ChangeLocale implements Serializable{
  // 页面的语言环境
  private String locale="fr";
  
  public ChangeLocale() {
  }
  
  public String setFrenchLocale(){
    locale="fr";
    return null;
  }
  
  public String setEnglishLocale(){
    locale="en";
    return null;
  }

  public String getLocale() {
    return locale;
  }
}

方法 setFrenchLocale 确实具有事件处理程序的签名。请记住,事件处理程序必须处理客户端的请求。既然它没有接收任何参数,它如何访问该请求?有多种实现方式:

  • 包含页面 JSF P 的事件处理程序的 Bean B,通常也是包含该页面模型 M 的 Bean。 这意味着 Bean B 包含一些字段,这些字段将由页面 P 中输入的值进行初始化。此操作将由控制器 [Faces Servlet] 在调用 Bean B 的事件处理程序之前完成。因此,该事件处理程序可以通过其所属的 Bean B 的字段访问客户在表单中输入的值,并进行处理。
  • 类型为 [FacesContext] 的静态方法 [FacesContext.getCurrentInstance()] 可访问当前请求 JSF 的执行上下文,该上下文是一个类型为 [FacesContext] 的对象。 通过此方式获得的请求执行上下文,可通过以下方法访问客户端浏览器发送到服务器的参数:
Map FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap()

如果客户端浏览器提交的参数(POST)如下:

formulaire=formulaire&javax.faces.ViewState=...&formulaire%3Aj_id_id21=formulaire%3Aj_id_id21

则方法 getRequestParameterMap() 将返回以下字典:

表单
表单
javax.faces.ViewState
...
表单:j_id_id21
表单:j_id_id21

在标签中


          <h:commandLink value="#{msg['welcome.langue1']}" action="#{changeLocale.setFrenchLocale}"/>

对事件管理器 locale.setFrenchLocale 有什么要求?我们希望它能确定应用程序使用的语言。 在 Java 术语中,这被称为应用程序的“本地化”。JSF 和 [index.xhtml] 页面中的 <f:view> 标签使用了此本地化功能:


  <f:view locale="#{changeLocale.locale}">
    ...
</f:view>

若要将页面切换为法语,只需将 locale 属性的值设为 fr;若要切换为英语,则需将其值设为 enlocale 属性的值由表达式 [ChangeLocale].getLocale() 获取。 该表达式返回类 [ChangeLocale] 中字段 locale 的值。据此可推导出方法 [ChangeLocale].setFrenchLocale() 的代码,该方法负责将页面切换为法语:


  public String setFrenchLocale(){
    locale="fr";
    return null;
}

我们曾说明,事件处理程序必须返回一个 C 字符串,该字符串将由 [Faces Servlet] 用于查找 JSF 页面,并将其作为响应发送给客户端浏览器。 如果要返回的页面与当前处理的页面相同,事件处理程序只需返回 null 值即可。这就是第 3 行所做的事情:我们希望返回相同的页面 [index.xhtml],但使用不同的语言。

让我们回到请求处理架构:

事件处理程序 changeLocale.setFrenchLocale 已执行,并向控制器 [Faces Servlet] 返回了值 null。因此,该控制器将重新显示页面 [index.xhtml]。 让我们重新查看该页面:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <f:view locale="#{changeLocale.locale}">
    <head>
      <title><h:outputText value="#{msg['welcome.titre']}" /></title>
    </head>
    <body>
      <h:form id="formulaire">
        <h:panelGrid columns="2">
          <h:commandLink value="#{msg['welcome.langue1']}" action="#{changeLocale.setFrenchLocale}"/>
          <h:commandLink value="#{msg['welcome.langue2']}" action="#{changeLocale.setEnglishLocale}"/>
        </h:panelGrid>
        <h1><h:outputText value="#{msg['welcome.titre']}" /></h1>
        <h:commandLink value="#{msg['welcome.page1']}" action="page1"/>
      </h:form>
    </body>
  </f:view>
</html>

每当评估 #{msg['...']} 类型的值时,都会使用 [messages.properties] 消息文件之一。实际使用的文件是与页面“本地化”对应的那个(第 6 行)。 由于事件处理程序 changeLocale.setFrenchLocale 将该位置定义为 fr,因此将使用文件 [messages_fr.properties]。 点击链接 [Anglais](第 14 行)将把本地化设置更改为 en(参见方法 changeLocale.setEnglishLocale)。 此时将使用文件 [messages_en.properties],页面将显示为英文:

每次显示页面 [index.xhtml] 时,都会执行标签 <f:view>


  <f:view locale="#{changeLocale.locale}">

因此方法 [ChangeLocale].getLocale() 会被重新执行。由于我们为该 Bean 设置了 Session 作用域:


@ManagedBean
@SessionScoped
public class ChangeLocale implements Serializable{

因此,在某个请求中进行的本地化设置将保留给后续请求使用。

我们还需要研究页面 [index.xhtml] 中的最后一个元素:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <f:view locale="#{changeLocale.locale}">
    <head>
      <title><h:outputText value="#{msg['welcome.titre']}" /></title>
    </head>
    <body>
      <h:form id="formulaire">
        <h:panelGrid columns="2">
          <h:commandLink value="#{msg['welcome.langue1']}" action="#{changeLocale.setFrenchLocale}"/>
          <h:commandLink value="#{msg['welcome.langue2']}" action="#{changeLocale.setEnglishLocale}"/>
        </h:panelGrid>
        <h1><h:outputText value="#{msg['welcome.titre']}" /></h1>
        <h:commandLink value="#{msg['welcome.page1']}" action="page1"/>
      </h:form>
    </body>
  </f:view>
</html>

第17行的<h:commandLink>标签,其action属性等于一个字符串。在此情况下,不会调用任何事件处理程序来处理该页面。 系统会立即跳转至页面 [page1.xhtml]。让我们来分析该用例中应用程序的运行机制:

用户点击链接 [Page 1]。表单数据被提交至控制器 [Faces Servlet]。该控制器通过接收到的请求识别出链接 [Page 1] 已被点击。它检查相应的标签:


        <h:commandLink value="#{msg['welcome.page1']}" action="page1"/>

该链接未关联任何事件处理程序。控制器 [Faces Servlet] 随即跳转至上述步骤 [3],并显示页面 [page1.xhtml]:

2.4.7. 页面 JSF [page1.xhtml]

页面 [page1.xhtml] 向客户端浏览器发送以下数据流:

 

生成此页面的代码如下:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <f:view locale="#{changeLocale.locale}">
    <head>
      <title><h:outputText value="#{msg['page1.titre']}"/></title>
    </head>
    <body>
      <h1><h:outputText value="#{msg['page1.entete']}"/></h1>
      <h:form>
        <h:commandLink value="#{msg['page1.welcome']}" action="index"/>
      </h:form>
    </body>
  </f:view>
</html>

该页面中没有任何未曾解释过的内容。读者可自行将代码 JSF 与发送至客户端浏览器的页面进行对应。返回主页的链接:


        <h:commandLink value="#{msg['page1.welcome']}" action="index"/>

将显示页面 [index.xhtml]。

2.4.8. 项目执行

我们的项目现已完成。我们可以进行构建(Clean and Build):

  • 项目构建会在 [Files] 选项卡中创建 [target] 文件夹。该文件夹内包含项目的 [mv-jsf2-02-1.0-SNAPSHOT.war] 压缩包。正是该压缩包会被部署到服务器上,
  • 在 [WEB-INF / classes] 和 [2] 中,包含项目 [Source Packages] 文件夹中的已编译类,以及原先位于 [Other Sources] 分支中的文件(此处为消息文件),
  • 在 [WEB-INF / lib] [3] 中,包含项目的库文件,
  • 在 [WEB-INF] 和 [4] 的根目录下,存放着项目的配置文件,
  • 在存档 [5] 的根目录下,包含项目分支 [Web Pages] 中的页面 JSF,
  • 项目构建完成后,即可运行 [6]。它将根据其运行配置 [7] 进行执行,
  • 如果 Tomcat 服务器尚未启动,则会启动 [8],
  • [mv-jsf2-02-1.0-SNAPSHOT.war] 归档文件将被加载到服务器上。这被称为在应用服务器上部署项目,
  • 在 [9] 处,要求在运行时启动浏览器。浏览器将请求应用程序上下文 [10]、c.a.d。 URL [http://localhost:8080/mv-jsf2-02]。根据文件 [web.xml] 的规则(参见第 44 页),将向客户端浏览器提供文件 [faces/index.xhtml]。 由于 URL 的格式为 [/faces/*],因此它将由控制器 [Faces Servlet] 处理(参见第 44 页的 [web.xml])。 该控制器将处理该页面,并发送以下 HTML 数据流:
 
  • 随后,控制器 [Faces Servlet] 将处理该页面产生的事件。

2.4.9. 配置文件 [faces-config.xml]

我们使用了以下 [faces-config.xml] 文件:


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">


  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
  </application>
</faces-config>

这是国际化 JSF 2 应用程序的最小配置文件。在此我们利用了 JSF 2 相较于 JSF 1 的新特性:

  • 使用 @ManagedBean@RequestScoped@SessionScoped@ApplicationScoped 注解声明 Bean 及其作用域,
  • 使用页面名称(不带 xhtml 后缀)作为导航键在页面间进行导航。

可能希望不使用这些功能,而像在 JSF 中那样,在 [faces-config.xml] 中声明项目 JSF 的这些元素。1. 在这种情况下,文件 [faces-config.xml] 可能如下所示:


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
<!-- 应用程序 -->
  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
  </application>
  
  <!-- 托管 Bean -->
  <managed-bean>
    <managed-bean-name>changeLocale</managed-bean-name>
    <managed-bean-class>utils.ChangeLocale</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
  </managed-bean>

   <!-- 导航 -->
  <navigation-rule>
    <description/>
    <from-view-id>/index.xhtml</from-view-id>
    <navigation-case>
      <from-outcome>p1</from-outcome>
      <to-view-id>/page1.xhtml</to-view-id>
    </navigation-case>
  </navigation-rule>

  <navigation-rule>
    <description/>
    <from-view-id>/page1.xhtml</from-view-id>
    <navigation-case>
      <from-outcome>welcome</from-outcome>
      <to-view-id>/index.xhtml</to-view-id>
    </navigation-case>
  </navigation-rule>


</faces-config>
  • 第 20-24 行:changeLocale Bean 的声明
    • 第 21 行:Bean 的名称;
    • 第 22 行:与 Bean 关联的类的全名;
    • 第 23 行:Bean 的作用域。可能的值包括 requestsession、application,
  • 第 27-34 行:声明导航规则:
    • 第 28 行:可在此处描述该规则。此处未进行描述;
    • 第29行:导航的起始页面(起点);
    • 第30-33行:一个导航案例。可能有多个;
    • 第31行:导航键;
    • 第 32 行:导航目标页面。

导航规则可以以更直观的方式显示。编辑文件 [faces-config.xml] 时,可以使用标签页 [PageFlow]:

 

假设我们使用前面的 [faces-config.xml] 文件。我们的应用程序会有什么变化?

  • 在类 [ChangeLocale] 中,注解 @ManagedBean @SessionScoped 将消失,因为该 Bean 现在已在 [faces-config] 中声明,
  • 通过链接从 [index.xhtml] 导航至 [page1.xhtml] 的路径将变为:

        <h:commandLink value="#{msg['welcome.page1']}" action="p1"/>

action 属性中,赋予 [faces-config] 中定义的导航键 p1,

  • 通过链接从 [page1.xhtml] 导航至 [index.xhtml] 时,链接将变为:

        <h:commandLink value="#{msg['page1.welcome']}" action="welcome"/>

将 [faces-config] 中定义的导航键 welcome 赋值给 action 属性,

  • 方法 setFrenchLocale setEnglishLocale 需返回导航键,无需修改,因为它们原本返回 null 表示留在当前页面。

2.4.10. 结论

让我们回顾一下我们编写的 NetBeans 项目:

该项目采用以下架构:

在每个 JSF 项目中,我们会发现以下内容:

  • JSF 和 [A] 页面,这些页面由控制器 [Faces Servlet] 和 [3] 发送 [4] 至客户端浏览器,
  • 消息文件 [C],用于更改页面 JSF 的语言,
  • 处理客户端浏览器事件的 Java 类 [B] [2a, 2b],以及/或作为页面模板的类 JSF [3]。 通常情况下,[métier] 和 [DAO] 层会分别进行开发和测试。 此时,[web] 层将与一个虚拟的 [métier] 层一起进行测试。如果 [métier] 和 [DAO] 层可用,通常会使用它们的 .jar 存档进行工作。
  • [D] 配置文件用于将这些不同元素相互关联。[web.xml] 文件已在第 44 页进行过说明,通常很少需要修改。[faces-config] 文件也是如此,我们始终使用其简化版本。

2.5. 示例 mv-jsf2-03:数据输入表单 - JSF 组件

从现在起,我们将不再展示项目的构建过程。我们将展示现成的项目并解释其工作原理。读者可从本文档的网站上获取所有示例(参见第 1.2 节)。

2.5.1. 应用程序

该应用程序仅有一个视图:

该应用程序展示了可在数据输入表单中使用的主要组件 JSF:

  • [1] 列显示所使用的 JSF / HTML 标签名称,
  • [2] 列展示了每个标签的输入示例,
  • [3] 列显示作为页面模板的 Bean 的值,
  • 在 [2] 列中输入的内容通过 [4] 按钮进行验证。此验证仅会更新页面的模型 Bean。 随后将返回同一页面。因此,验证后,[3] 列将显示模型 Bean 的新值,从而允许用户检查其输入对页面模型的影响。

2.5.2. NetBeans 项目

该应用程序的 NetBeans 项目如下:

  • 在 [1] 中,项目配置文件 JSF,
  • 在 [2] 中,项目的唯一页面:index.xhtml,
  • [3],用于配置页面外观的样式表 [styles.css],
  • [4],项目的 Java 类,
  • [5],应用程序的双语(法语和英语)消息文件。

2.5.3. 文件 [pom.xml]

此处仅列出依赖项:


    <dependencies>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-api</artifactId>
            <version>2.1.7</version>
        </dependency>
        <dependency>
            <groupId>com.sun.faces</groupId>
            <artifactId>jsf-impl</artifactId>
            <version>2.1.7</version>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
</dependencies>

这些是项目 JSF 所需的依赖项。在接下来的示例中,只有当该文件发生变更时才会展示。

2.5.4. 文件 [web.xml]

文件 [web.xml] 已配置为将页面 [index.xhtml] 设为项目的主页:


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  <context-param>
    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
    <param-value>client</param-value>
  </context-param>  
  <context-param>
    <param-name>javax.faces.PROJECT_STAGE</param-name>
    <param-value>Development</param-value>
  </context-param>
  <context-param>
    <param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
    <param-value>true</param-value>
  </context-param> 
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/faces/*</url-pattern>
  </servlet-mapping>
  <session-config>
    <session-timeout>
      30
    </session-timeout>
  </session-config>
  <welcome-file-list>
    <welcome-file>faces/index.xhtml</welcome-file>
  </welcome-file-list>
</web-app>
  • 第 30 行:[index.xhtml] 页面是主页,
  • 第 11-14 行:[Faces Servlet] Servlet 的配置参数。该参数要求将类似以下格式的 Facelet 中的注释:

        <!-- 语言 -->

中的注释被忽略。如果没有此参数,注释会引发难以理解的问题,

  • 第3-6行:[Faces Servlet] Servlet 的一个参数,稍后将进行说明。

2.5.5. 文件 [faces-config.xml]

应用程序中的 [faces-config.xml] 文件如下:


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
  </application>
</faces-config>
  • 第 11-16 行:配置应用程序的消息文件。

2.5.6. 消息文件 [messages.properties]

消息文件(参见项目截图中的 [5])如下:

[messages_fr.properties]


form.langue1=Fran\u00e7ais
form.langue2=Anglais
form.titre=Java Server Faces - les tags
form.headerCol1=Type
form.headerCol2=Champs de saisie
form.headerCol3=Valeurs du modèle de la page
form.loginPrompt=login : 
form.passwdPrompt=mot de passe : 
form.descPrompt=description : 
form.selectOneListBox1Prompt=choix unique : 
form.selectOneListBox2Prompt=choix unique : 
form.selectManyListBoxPrompt=choix multiple : 
form.selectOneMenuPrompt=choix unique : 
form.selectManyMenuPrompt=choix multiple : 
form.selectBooleanCheckboxPrompt=marié(e) : 
form.selectManyCheckboxPrompt=couleurs préférées : 
form.selectOneRadioPrompt=moyen de transport préféré : 
form.submitText=Valider
form.buttonRazText=Raz

这些消息显示在页面的以下位置:

消息的英文版本如下:

[messages_en.properties]


form.langue1=French
form.langue2=English
form.titre=Java Server Faces - the tags
form.headerCol1=Input Type
form.headerCol2=Input Fields
form.headerCol3=Page Model Values
form.loginPrompt=login : 
form.passwdPrompt=password : 
form.descPrompt=description : 
form.selectOneListBox1Prompt=unique choice : 
form.selectOneListBox2Prompt=unique choice : 
form.selectManyListBoxPrompt=multiple choice : 
form.selectOneMenuPrompt=unique choice : 
form.selectManyMenuPrompt=multiple choice : 
form.selectBooleanCheckboxPrompt=married : 
form.selectManyCheckboxPrompt=preferred colors : 
form.selectOneRadioPrompt=preferred transport means : 
form.submitText=Submit
form.buttonRazText=Reset

2.5.7. 页面 [index.xhtml] 的模板 [Form.java]

在上述项目中,类 [Form.java] 将作为页面 JSF 和 [index.xhtml] 的模板或后端 Bean。让我们通过页面 [index.xhtml] 中的一个示例来说明模板的概念:


<!-- 第 1 行 -->
          <h:outputText value="inputText"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.loginPrompt']}"/>
            <h:inputText id="inputText" value="#{form.inputText}"/>
          </h:panelGroup>
          <h:outputText value="#{form.inputText}"/>

在首次调用页面 [index.xhtml] 时,上述代码会生成输入表的第 2 行:

第 2 行显示字段 [1],第 3-6 行显示字段 [2],第 7 行显示字段 [3]。

第 5 行和第 7 行使用表达式 EL,该表达式调用在类 [Form.java] 中定义的表单 Bean,具体如下:


package forms;

import javax.enterprise.context.RequestScoped;
import javax.faces.bean.ManagedBean;


@ManagedBean
@RequestScoped
public class Form {
  • 第 7 行定义了一个无名 Bean。因此,该 Bean 的名称即为以小写字母开头的类名:form
  • 该 Bean 的作用域为 request。这意味着在客户端请求/服务器响应的循环中,当请求需要时会实例化该 Bean,并在向客户端返回响应后被销毁。

在页面 [index.xhtml] 的以下代码中:


<!-- 第 1 行 -->
          <h:outputText value="inputText"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.loginPrompt']}"/>
            <h:inputText id="inputText" value="#{form.inputText}"/>
          </h:panelGroup>
<h:outputText value="#{form.inputText}"/>

第5行和第7行使用了表单Bean的值inputText。要理解页面P与其模板M之间的关联,需要回到 (客户端请求/服务器响应)循环,这是Web应用程序的典型特征:

需要区分两种情况:一种是页面 P 作为响应发送给浏览器(步骤 4),例如在初始请求页面时;另一种是用户在页面 P 上触发了一个事件,该事件由控制器 [Faces Servlet] 处理(步骤 1)。

从浏览器的角度来看,可以区分这两种情况:

  1. 在初始请求页面时,浏览器对页面的URL执行GET操作;
  2. 当提交页面中输入的值时,浏览器会对页面的 URL 执行 POST 操作。

在这两种情况下,请求的都是同一个 URL。根据浏览器请求的性质(GET 或 POST),对该请求的处理方式将有所不同。

[cas 1 – demande initiale de la page P]

浏览器使用 GET 请求该页面的 URL。 控制器 [Faces Servlet] 将直接进入响应渲染阶段 [4],页面 [index.xhtml] 将被发送给客户端。 控制器 JSF 将要求页面中的每个标签显示出来。以 [index.xhtml] 代码的第 5 行为例:


            <h:inputText id="inputText" value="#{form.inputText}"/>

标签 JSF <h:inputText value="值"/> 生成了标签 HTML <input type="text" value="值"/>。 负责处理该标签的类会遇到表达式 #{form.inputText} 并需对其进行求值:

  • 如果表单 Bean 尚未存在,则通过实例化类 forms.Form 来创建它
  • 通过调用方法 form.getInputText() 来求解表达式 #{form.inputText}
  • 文本 <input id="formulaire:inputText" type="text" name="formulaire:inputText" value="文本" /> 被插入到 HTML 数据流中,该数据流将被发送给客户端(假设方法 form.getInputText() 返回了字符串 "文本")。 此外,JSF 还将为流中放置的组件 HTML 指定一个名称(name)。 该名称由已解析的 JSF 组件的 id 标识符及其父组件的 id 标识符(此处为 <h:form id="formulaire"/> 标签)组合而成。

需要注意的是,如果在页面 P 中使用表达式 #{M.champ}(其中 M 是页面 P 的模型 Bean),则该 Bean 必须提供公共方法 getChamp()。该方法返回的类型必须能够转换为 String 类型。一种常见且可行的模型 M 如下:

1
2
3
4
private T champ;
public T getChamp(){
    return champ;
} 

其中 T 是一种可转换为 String 类型的类型,可能还包含一个 toString 方法。

同样在显示页面 P 的情况下,处理以下行:


<h:outputText value="#{form.inputText}"/>

的处理方式将与此类似,并将生成以下 HTML 数据流:

texte

在服务器内部,页面 P 被表示为一个组件树,该树反映了发送给客户端的页面标签树。我们将此树称为视图或页面状态 )。该状态会被存储。根据应用程序中 [web.xml] 文件中的配置,存储方式有两种:


<web-app ...>
...
  <context-param>
    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
    <param-value>client</param-value>
  </context-param>
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
...
</web-app>

第 7-11 行定义了控制器 [Faces Servlet]。该控制器可通过不同的 <context-param> 标签进行配置,其中第 3-6 行中的标签指定页面状态应保存在客户端(浏览器)上。 另一可能的值(第5行)是“server”,表示将状态保存到服务器上。这是默认值。

当页面状态保存在客户端时,控制器 JSF 会在发送的每个 HTML 页面中添加一个隐藏字段,其值为页面的当前状态。该隐藏字段的格式如下:

<input type="hidden" name="javax.faces.ViewState" id="javax.faces.ViewState" value="H4sIAAAAAAAAANV...Bnoz8dqAAA=" />

其值以编码形式表示发送给客户端的页面状态。需要重点理解的是,该隐藏字段属于页面表单的一部分,因此在表单提交时,它将作为浏览器发送的数据的一部分。 基于该隐藏字段,控制器 JSF 能够还原发送给客户端时的视图。

当页面状态保存在服务器上时,发送给客户端的页面状态也会保存在客户端的会话中。当客户端浏览器提交表单中输入的值时,它也会一并发送其会话令牌。 基于该令牌,控制器 JSF 将检索发送给客户端的页面状态并将其恢复。

页面 JSF 的状态编码可能需要数百字节。由于该状态会为应用程序的每位用户单独维护,因此当用户数量庞大时,可能会遇到内存问题。 因此,我们在此选择将页面状态保存到客户端(参见 [web.xml] 第 2.5.4 节,第 66 页)。

[cas 2 – traitement de la page P]

当前处于上文所述的 [1] 步骤,此时控制器 [Faces Servlet] 将收到来自客户端浏览器的请求 POST,而该浏览器此前已从该控制器接收了页面 [index.xhtml]。 当前正在处理页面事件。在事件能够被处理为 [2a] 之前,将经历多个步骤。控制器 JSF 处理请求 POST 的流程如下:

Image

  • 在 [A] 中,借助隐藏字段 javax.faces.ViewState,最初发送给客户端浏览器的视图被重建。此时,页面组件恢复了其在发送页面中的原始值。 我们的组件 inputText 恢复了其“文本”值,
  • 在 [B] 中,客户端浏览器提交的值被用于更新视图组件。 因此,如果在名为 inputText 的输入字段 HTML 中,用户输入了“jean”,则值“jean”将替换值“文本”。 此时视图将显示用户修改后的页面,而非最初发送给浏览器的页面,
  • 在 [C] 中,将对提交的值进行验证。假设前一个组件 inputText 是年龄输入字段。输入的值必须是整数。 浏览器提交的值始终为 String 类型。但在与页面 P 关联的 M 模型中,其最终类型可能完全不同。此时会将 String 类型转换T 类型。此转换可能失败。 在此情况下,请求/响应循环结束,用 [B] 构建的页面将连同错误消息(如果页面 P 的作者已预先设置)一并返回给客户端浏览器。 需要注意的是,用户看到的页面与他输入的内容完全一致,开发人员无需额外操作。而在其他技术中,例如 JSP,开发人员必须根据用户输入的值自行重建 P 页面。 组件的值还可能需要经过验证过程。仍以年龄输入字段组件 inputText 为例,输入的值不仅必须是整数,还必须属于 [1,N] 指定的数值范围。 即使输入值通过了转换阶段,也可能无法通过验证阶段。在这种情况下,请求/响应循环同样结束,由 [B] 构建的页面 P 将被返回至客户端浏览器,
  • 若页面 P 的所有组件均通过转换和验证阶段,其值将被赋给页面 P 的模型 M。若由以下标签生成的输入字段值:

        <h:inputText value="#{form.inputText}"/>

生成的输入字段值为“jean”,则该值将通过执行代码 form.setInputText("jean") 赋值给页面的表单模型。需注意,在页面 P 的模型 M 中,用于存储 P 页面输入字段值的 M 模型私有字段必须具有 set 方法

  • 一旦页面 P 的模型 M 通过提交的值更新完毕,即可处理触发页面 P 的 POST 事件。这就是 [E] 步骤。 需要注意的是,如果该事件的处理程序属于 Bean M,它就可以访问存储在该 Bean 字段中的表单 P 的值。
  • 步骤 [E] 将向控制器 JSF 返回一个导航键。在我们的示例中,这始终是待显示的页面 XHTML 的名称,不包含后缀 .xhtml。 这就是步骤 [F]。另一种做法是返回一个导航键,该键将在文件 [faces-config.xml] 中被查找。我们已经描述了这种情况。

综上所述,我们得出以下结论:

  • 页面 P 使用方法 [M].getC() 显示其模板 M 中的字段 C,
  • 页面 P 中模型 M 的字段 C 通过方法 [M].setC(saisie) 初始化为页面 P 中输入的值。在此步骤中,可能会涉及转换和验证过程,这些过程可能会失败。 在这种情况下,导致页面 P 触发 POST 的事件将不予处理,页面将按用户输入的状态原样返回给客户端。

页面 [index.xhtml] 的模板 [Form.java] 将如下所示:


package forms;

import javax.enterprise.context.RequestScoped;
import javax.faces.bean.ManagedBean;


@ManagedBean
@RequestScoped
public class Form {
  
  /** 创建表单的新实例 */
  public Form() {
  }
  
  // 表单字段
  private String inputText="texte";
  private String inputSecret="secret";
  private String inputTextArea="ligne1\nligne2\n";
  private String selectOneListBox1="2";
  private String selectOneListBox2="3";
  private String[] selectManyListBox=new String[]{"1","3"};
  private String selectOneMenu="1";
  private String[] selectManyMenu=new String[]{"1","2"};
  private String inputHidden="initial";
  private boolean selectBooleanCheckbox=true;
  private String[] selectManyCheckbox=new String[]{"1","3"};
  private String selectOneRadio="2";
  
  // 事件
  public String submit(){
    return null;
  }
  
  // 获取器和设置器
  ...
}

第 16-27 行的字段用于表单的以下位置:

2.5.8. 页面 [index.xhtml]

生成上述视图的页面 [index.xhtml] 如下所示:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">

  <f:view locale="#{changeLocale.locale}">
    <h:head>
      <title>JSF</title>
      <h:outputStylesheet library="css" name="styles.css"/>
    </h:head>
    <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
      <h:form id="formulaire">
        <!-- 语言 -->
        <h:panelGrid columns="2">
          <h:commandLink value="#{msg['form.langue1']}" action="#{changeLocale.setFrenchLocale}"/>
          <h:commandLink value="#{msg['form.langue2']}" action="#{changeLocale.setEnglishLocale}"/>
        </h:panelGrid>
        <h1><h:outputText value="#{msg['form.titre']}"/></h1>
        <h:panelGrid columnClasses="col1,col2,col3" columns="3" border="1">
          <!-- 表头 -->
          <h:outputText value="#{msg['form.headerCol1']}" styleClass="entete"/>
          <h:outputText value="#{msg['form.headerCol2']}" styleClass="entete"/>
          <h:outputText value="#{msg['form.headerCol3']}" styleClass="entete"/>
          <!-- 第 1 行 -->
          ...
          <!-- 第 2 行 -->
          ...
          <!-- 第 3 行 -->
          ...
          <!-- 第 4 行 -->
          ...
          <!-- 第 5 行 -->
          ...
          <!-- 第 6 行 -->
          ...
          <!-- 第 7 行 -->
          ...
          <!-- 第 8 行 -->
          ...
          <!-- 第 9 行 -->
          ...
          <!-- 第 10 行 -->
          ...
          <!-- 第 11 行 -->
          ...
          <!-- 第 12 行 -->
          ...
        </h:panelGrid>
        <p>
          <h:commandButton type="submit" id="submit" value="#{msg['form.submitText']}"/>
        </p>
      </h:form>
    </h:body>
  </f:view>
</html>

我们将依次研究该页面的主要组成部分。请注意 JSF 表单的一般结构:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">

  <f:view ...>
    <h:head>
      ...
    </h:head>
    <h:body ...>
      <h:form id="formulaire">
        ...
        <h:commandButton type="submit" id="submit" value="#{msg['form.submitText']}"/>
        ...
      </h:form>
    </h:body>
  </f:view>
</html>

表单的组件必须位于 <h:form> 标签内(第 12-16 行)。如果要实现应用程序的国际化,则必须使用 <f:view> 标签(第 7-18 行)。 此外,表单必须具备提交途径(POST),通常为链接或按钮(如第14行所示)。表单也可通过多种事件触发提交(例如列表中选项的变更、活动字段的切换、输入字段中输入字符等)。

2.5.9. 表单样式

为了提高表单表格列的可读性,该表单附带了一个样式表:


  <f:view locale="#{changeLocale.locale}">
    <h:head>
      <title>JSF</title>
      <h:outputStylesheet library="css" name="styles.css"/>
</h:head>
  • 第 4 行:页面的样式表在 HTML <head> 标签内通过以下标签定义:

<h:outputStylesheet library="css" name="styles.css"/>

将从 [resources] 文件夹中查找样式表:

在标签中:


<h:outputStylesheet library="css" name="styles.css"/>
  • library 是包含样式表的文件夹名称,
  • name 是样式表的名称。

下面来看一下该样式表的使用示例:


        <h:panelGrid columnClasses="col1,col2,col3" columns="3" border="1">

标签 <h:panelGrid columns="3"/> 定义了一个三列表格。columnClasses 属性用于为这些列设置样式。 columnClasses 属性的值 col1、col2、col3 分别指代表格中第 1、2 和 3 列的样式。这些样式将在页面的样式表中查找:


.info{
   font-family: Arial,Helvetica,sans-serif;
   font-size: 14px;
   font-weight: bold
}

.col1{
   background-color: #ccccff
}

.col2{
   background-color: #ffcccc
}

.col3{
   background-color: #ffcc66
}

.entete{
   font-family: 'Times New Roman',Times,serif;
   font-size: 14px;
   font-weight: bold
}
  • 第 7-9 行:名为 col1 的样式
  • 第 11-13 行:名为 col2 的样式
  • 第15-17行:名为col3的样式

这三种样式定义了各列的背景颜色。

  • 第19-23行:entete样式用于定义表格第一行文本的样式:

          <!-- 标题 -->
          <h:outputText value="#{msg['form.headerCol1']}" styleClass="entete"/>
          <h:outputText value="#{msg['form.headerCol2']}" styleClass="entete"/>
          <h:outputText value="#{msg['form.headerCol3']}" styleClass="entete"/>
  • 第 1-5 行:info 样式用于定义表格第一列文本的样式:

          <!-- 第 1 行 -->
          <h:outputText value="inputText"  styleClass="info"/>

我们不会过多强调样式表的使用,因为仅此一项就值得写一本书,而且其编写工作通常由专业人员负责。尽管如此,我们还是希望使用一个简约的样式表,以提醒大家其使用是必不可少的。

现在让我们看看页面背景图片是如何定义的:


<h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">

背景图片通过<h:body>标签的style属性进行设置。该属性用于设置样式元素。背景图片位于[resources/images/standard.jpg]文件夹中:

该图片通过 URL 和 [/mv-jsf2-03/resources/images/standard.jpg] 生成。因此可以这样写:


<h:body style="background-image: url('mv-jsf2-03/resources/images/standard.jpg');">

/mv-jsf2-03 是应用程序的上下文。该上下文由 Web 服务器管理员设定,因此可能会发生变化。可以通过表达式 EL ${request.contextPath} 获取该上下文。因此,我们更倾向于使用以下 style 属性:


style="background-image: url('${request.contextPath}/resources/images/standard.jpg');"

该属性在任何上下文中均有效。

2.5.10. 表单的两个客户端请求/服务器响应循环

让我们回顾第 2.5.7 节中已阐述的一般情况,并将其应用于所研究的表单。该表单将在经典的 JSF 环境中进行测试:

在此,既不涉及事件处理程序,也不包含 [métier] 层。 因此,[2x]步骤将不存在。需区分两种情况:一种是表单F最初由浏览器请求;另一种是用户在表单F中触发事件后,该事件由控制器[Faces Servlet]处理。这两种情况涉及两个不同的客户端请求/服务器响应循环。

  • 第一个对应于页面的初始请求,由浏览器对表单的 URL 执行的 GET 操作触发;
  • 第二个循环对应页面中输入值的提交,由浏览器对同一URL操作发起的POST操作触发。

根据浏览器发出的请求类型(GET 或 POST),控制器 [Faces Servlet] 对该请求的处理方式会有所不同。

[cas 1 – demande initiale du formulaire F]

浏览器使用 GET 请求该页面的 URL。 控制器 [Faces Servlet] 将直接进入响应渲染阶段 [4]。表单 [index.xhtml] 将由其模板 [Form.java] 初始化,并发送给客户端,客户端将收到以下视图:

Image

此时客户端与服务器之间的 HTTP 交互如下:

客户端请求 HTTP

1
2
3
4
5
6
7
8
GET /mv-jsf2-03/ HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,fr-fr;q=0.8,en;q=0.6,en-us;q=0.4,es;q=0.2
Accept-Encoding: gzip, deflate
DNT: 1
Connection: keep-alive

第 1 行显示了浏览器的 GET。

服务器的响应 HTTP

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Powered-By: JSF/2.0
Set-Cookie: JSESSIONID=F6E66136BF00EEE026ADAB1BBEBFD587; Path=/mv-jsf2-03/; HTTPOnly
Content-Type: text/html;charset=UTF-8
Content-Length: 7371
Date: Tue, 15 May 2012 09:04:57 GMT

此处未显示,第 7 行之后紧跟一个空行,然后是表单的代码 HTML。浏览器正是通过解析并显示该代码来呈现内容。

[cas 2 – traitement des valeurs saisies dans le formulaire F]

用户填写表单并通过按钮 [Valider] 提交。随后,浏览器使用 POST 请求表单的 URL。 控制器 [Faces Servlet] 处理此请求,更新表单 [index.xhtml] 的模型 [Form.java],并返回由该新模型更新的表单 [index.xhtml]。 让我们通过一个示例来分析这个循环:

Image

在上图中,用户已完成数据输入并提交。随后收到如下视图:

Image

此时客户端与服务器之间的 HTTP 交互如下:

客户端请求 HTTP

POST /mv-jsf2-03/faces/index.xhtml HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,fr-fr;q=0.8,en;q=0.6,en-us;q=0.4,es;q=0.2
Accept-Encoding: gzip, deflate
DNT: 1
Connection: keep-alive
Referer: http://localhost:8080/mv-jsf2-03/faces/index.xhtml
Cookie: JSESSIONID=374CC5F1D2ACAC182A5747A443651E36
Content-Type: application/x-www-form-URLencoded
Content-Length: 1543

formulaire=formulaire&formulaire%3AinputText=nouveau+texte&formulaire%3AinputSecret=mdp&formulaire%3AinputTextArea=Tutoriel+JSF%0D%0A&formulaire%3AselectOneListBox1=3&formulaire%3AselectOneListBox2=5&formulaire%3AselectManyListBox=3&formulaire%3AselectManyListBox=4&formulaire%3AselectManyListBox=5&formulaire%3AselectOneMenu=4&formulaire%3AselectManyMenu=5&formulaire%3AinputHidden=initial&formulaire%3AselectManyCheckbox=2&formulaire%3AselectManyCheckbox=3&formulaire%3AselectManyCheckbox=4&formulaire%3AselectOneRadio=4&formulaire%3Asubmit=Valider&javax.faces.ViewState=H4sIAAAAAAAAAJVUT0g...P4BKm1E4F0FAAA  

第 1 行是浏览器生成的 POST。第 14 行是用户输入的值。例如,我们可以从中看到输入字段中的文本:

formulaire%3AinputText=nouveau+texte

在第14行,隐藏字段javax.faces.ViewState已被提交。该字段以编码形式表示表单的状态,即最初在GET初始化时发送给浏览器的状态。

服务器的响应 HTTP

1
2
3
4
5
6
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Powered-By: JSF/2.0
Content-Type: text/html;charset=UTF-8
Content-Length: 7299
Date: Tue, 15 May 2012 09:37:17 GMT

此处未显示,第 6 行之后紧跟一行空行,以及由 POST 生成的全新模板更新后的表单代码 HTML。

现在我们来分析该表单的各个组成部分。

2.5.11. <h:inputText> 标签

<h:inputText> 标签会生成一个 HTML <input type="text" ...> 标签。

请看以下代码:


          <!-- 第 1 行 -->
          <h:outputText value="inputText"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.loginPrompt']}"/>
            <h:inputText id="inputText" value="#{form.inputText}"/>
          </h:panelGroup>
<h:outputText value="#{form.inputText}"/>

及其模板 [Form.java]:


  private String inputText="texte";

  public String getInputText() {
    return inputText;
  }
  
  public void setInputText(String inputText) {
    this.inputText = inputText;
}

当首次请求页面 [index.html] 时,生成的页面如下:

  • 代码 XHTML 的第 2 行生成 [1],
  • 标签 <h:panelGroup>(第3-6行)用于将多个元素合并到由页面完整代码第20行中的标签 <h:panelGrid> 生成的表格的同一单元格中(参见第2.5.8节)。 文本 [2] 由第 4 行生成。输入字段 [3] 由第 [5] 行生成。 在此,[Form.java] 中的方法 getInputText(Java 代码第 3-5 行)被用于生成输入字段的文本,
  • 代码 XHTML 的第 7 行生成 [4]。 随后再次调用 [Form.java] 中的 getInputText 方法来生成 [4] 文本。

由页面 XHTML 生成的流程 HTML 如下:


<tr>
<td class="col1"><span class="info">inputText</span></td>
<td class="col2">login : <input id="formulaire:inputText" type="text" name="formulaire:inputText" value="texte" /></td>
<td class="col3">texte</td>
</tr>

HTML 中的 <tr> 和 <td> 标签是由用于生成表单表格的 <h:panelGrid> 标签生成的。

现在,请在下方 [1] 输入框中输入一个值,并点击 [Valider] [2] 按钮提交表单。我们将获得响应页面 [3, 4]:

字段 [1] 的值以如下方式提交:

formulaire%3AinputText=nouveau+texte

在 [2] 中,通过以下按钮提交表单:


          <h:commandButton id="submit" type="submit" value="#{msg['form.submitText']}"/>

标签 <h:commandButton> 没有 action 属性。在这种情况下,不会调用任何事件处理程序,也不会触发任何导航规则。处理完成后,将返回同一页面。让我们回顾一下其处理流程:

Image

  • 在 [A] 中,页面 P 将按其发送时的状态恢复。这意味着 ID 为 inputText 的组件将恢复其初始值“文本”,
  • 在 [B] 阶段,浏览器提交的值(用户输入的内容)被赋值给页面 P 的组件。在此,ID 为 inputText 的组件接收值“新文本”,
  • 在 [C] 中,进行转换和验证。此处没有相关操作。在 M 模型中,与 ID 为 inputText 的组件关联的字段如下:

private String inputText="texte";

由于输入的值类型为 String,因此无需进行转换。此外,尚未创建任何验证规则。我们将在后续构建这些规则。

  • 在 [D] 中,输入的值被分配给模型。[Form.java] 的字段 inputText 接收值“新文本”,
  • 在 [E] 中,未执行任何操作,因为未将任何事件处理程序关联到按钮 [Valider]。
  • 在 [F] 中,页面 P 再次被发送给客户端,因为按钮 [Valider] 没有 action 属性。随后执行 [index.xhtml] 中的以下代码行:

          <!-- 第 1 行 -->
          <h:outputText value="inputText"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.loginPrompt']}"/>
            <h:inputText id="inputText" value="#{form.inputText}"/>
          </h:panelGroup>
<h:outputText value="#{form.inputText}"/>

第 5 行和第 7 行使用了模板中字段 inputText 的值,该值现在为“新文本”。因此显示结果如下:

Image

2.5.12. 标签 <h:inputSecret>

标签 <h:inputSecret> 会生成一个 HTML <input type="password" ...>。这是一个与 <h:JSF> 标签类似的输入字段,不同之处在于用户输入的每个字符在视觉上都会被 * 字符替换。

请看以下代码:


          <!-- 第 2 行 -->
          <h:outputText value="inputSecret"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.passwdPrompt']}"/>
            <h:inputSecret id="inputSecret" value="#{form.inputSecret}"/>
          </h:panelGroup>
<h:outputText value="#{form.inputSecret}"/>

及其在 [Form.java] 中的模板:


private String inputSecret="secret";

当首次请求页面 [index.xhtml] 时,生成的页面如下:

  • 代码 XHTML 的第 2 行生成 [1]
  • 文本 [2] 由第 4 行生成。输入字段 [3] 由 [5] 行生成。 通常,应使用 [Form.java] 的方法 getInputSecret 来生成输入字段的文本。 当该字段类型为“密码”时,则属于例外情况。标签 <h:inputSecret> 仅用于读取输入内容,而非显示。
  • 代码 XHTML 的第 7 行生成了 [4]。 此处使用了 [Form.java] 的 getInputSecret 方法来生成文本 [4](参见 Java 代码第 1 行)。

由页面 XHTML 生成的流程 HTML 如下:


<tr>
<td class="col1"><span class="info">inputSecret</span></td>
<td class="col2">mot de passe : <input id="formulaire:inputSecret" type="password" name="formulaire:inputSecret" value="" /></td>
<td class="col3">secret</td>
</tr>
  • 第3行:由标签 JSF <h:inputSecret> 生成的标签 HTML <input type="password" .../>

现在,在下方 [1] 输入框中输入一个值,并点击 [Valider] [2] 按钮提交表单。我们得到的响应页面是 [3]:

字段 [1] 的值以如下方式提交:

formulaire%3AinputSecret=mdp

通过 [2] 验证表单后,系统通过 [1] 的输入更新了 [Form.java] 表单。 随后,[Form.java] 表单中的字段 inputSecret 被赋值为 mdp。由于表单 [index.xhtml] 未定义任何导航规则或事件处理程序,因此在更新其模板后,该表单会被重新显示。 此时,页面又回到了对 [index.xhtml] 页面初始请求时的显示状态,此时模板中的 inputSecret 字段的值仅从 [3] 发生了变化。

2.5.13. 标签 <h:inputTextArea>

标签 <h:inputTextArea> 会生成一个 HTML <textarea ...>文本</textarea> 标签。 这是一个与标签 JSF <h:inputText> 类似的输入框,不同之处在于这里可以输入多行文本。

请看以下代码:


          <!-- 第 3 行 -->
          <h:outputText value="inputTextArea" styleClass="info"/>          
          <h:panelGroup>
            <h:outputText value="#{msg['form.descPrompt']}"/>
            <h:inputTextarea id="inputTextArea" value="#{form.inputTextArea}" rows="4"/>
          </h:panelGroup>         
<h:outputText value="#{form.inputTextArea}"/>

及其在 [Form.java] 中的模板:


private String inputTextArea="ligne1\nligne2\n";

当首次请求页面 [index.xhtml] 时,生成的页面如下:

  • 代码 XHTML 的第 2 行生成 [1],
  • 文本 [2] 由第 4 行生成。输入字段 [3] 由代码行 [5] 生成。 其内容是通过调用模型中的 getInputTextArea 方法生成的,该方法返回了上述 Java 代码第 1 行中定义的值,
  • 代码 XHTML 的第 7 行生成 [4]。此处再次使用了 [Form.java] 中的方法 getInputTextArea。 字符串“第1行\n第2行”中包含换行符\n。这些换行符仍然存在。但在插入到 HTML 流中后,浏览器会将其显示为空格。 而显示 [3] 的 HTML 标签 <textarea> 则正确解释了换行符。

由页面 XHTML 生成的数据流 HTML 如下:


<tr>
<td class="col1"><span class="info">inputTextArea</span></td>
<td class="col2">description : <textarea id="formulaire:inputTextArea" name="formulaire:inputTextArea" rows="4">ligne1
ligne2
</textarea></td>
<td class="col3">ligne1
ligne2
</td>
</tr>
  • 第 3-5 行:由标签 HTML <textarea>...</textarea> 由标签 JSF <h:inputTextArea> 生成

现在,在下方 [1] 输入框中输入一个值,并点击 [Valider] [2] 按钮提交表单。我们得到的响应页面是 [3]:

提交的 [1] 字段值如下:

formulaire%3AinputTextArea=Tutoriel+JSF%0D%0Apartie+1%0D%0A

通过 [2] 验证表单后,系统通过 [1] 条目更新了 [Form.java] 模板。 随后,[Form.java] 表单中的字段 textArea 被赋予了值“教程 JSF\n第1部分”。 重新显示 [index.xhtml] 可见,模板中的字段 textArea 已成功更新为 [3]。

2.5.14. 标签 <h:selectOneListBox>

标签 <h:selectOneListBox> 会生成一个 HTML 标签 <select>...</select>。从视觉上看,它会生成一个下拉列表或带有滚动条的列表。

请看以下代码:


<!-- 第 4 行 -->
          <h:outputText value="selectOneListBox (size=1)" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectOneListBox1Prompt']}"/>
            <h:selectOneListbox id="selectOneListBox1" value="#{form.selectOneListBox1}" size="1">
              <f:selectItem itemValue="1" itemLabel="un"/>
              <f:selectItem itemValue="2" itemLabel="deux"/>
              <f:selectItem itemValue="3" itemLabel="trois"/>
            </h:selectOneListbox>
          </h:panelGroup>
          <h:outputText value="#{form.selectOneListBox1}"/>

及其在 [Form.java] 中的模板:


private String selectOneListBox1="2";

当首次请求页面 [index.xhtml] 时,生成的页面如下:

  • 代码 XHTML 的第 2 行生成 [1]
  • 文本 [2] 由第 4 行生成。下拉列表 [3] 由 [5-9] 行生成。 正是 size="1" 属性的值导致列表仅显示一个项目。 如果缺少该属性,size 属性的默认值为 1。列表中的项目由第 6-8 行中的 <f:selectItem> 标签生成。这些标签的语法如下:

<f:selectItem itemValue="valeur" itemLabel="texte"/>

itemLabel 属性的值即为列表中显示的内容。 itemValue 属性的值即为该元素的。若该元素在下拉列表中被选中,则该值将被发送至控制器 [Faces Servlet]。

在 [3] 中显示的元素是通过调用方法 getSelectOneListBox1()(第 5 行)确定的。 得到的结果“2”(Java代码第1行)导致下拉列表第7行的元素被显示,这是因为其itemValue属性值为“2”,

  • 代码 XHTML 的第 11 行生成 [4]。此处再次使用了 [Form.java] 中的方法 getSelectOneListBox1

由页面 XHTML 生成的流程 HTML 如下:


<tr>
<td class="col1"><span class="info">selectOneListBox (size=1)</span></td>
<td class="col2">choix unique : <select id="formulaire:selectOneListBox1" name="formulaire:selectOneListBox1" size="1">
    <option value="1">un</option>
    <option value="2" selected="selected">deux</option>
    <option value="3">trois</option>
</select></td>
<td class="col3">2</td>
</tr>
  • 第 3 行和第 7 行:由标签 JSF <h:selectOneListBox> 生成的标签 HTML <select ...>...</select>,
  • 第 4-6 行:由 JSF <f:selectItem> 标签生成的 HTML <option ...> ... </option> 标签,
  • 第 5 行:列表中选中了 value="2" 的元素,这体现在 selected="selected" 属性的存在上。

现在,在下方从列表中选择一个新值,并点击按钮提交表单。结果显示如下页面:

提交的 [1] 字段值为:

formulaire%3AselectOneListBox1=3

通过 [2] 验证表单后,通过输入 [1] 更新了模板 [Form.java]。元素 HTML


    <option value="3">trois</option>

已被选中。浏览器将字符串“3”作为生成下拉列表的组件 JSF 的值发送:


            <h:selectOneListbox id="selectOneListBox1" value="#{form.selectOneListBox1}" size="1">

控制器 JSF 将使用方法 setSelectOneListBox1("3") 来更新下拉列表的模型。因此,更新后,模型字段 [Form.java]


        private String selectOneListBox1;

字段将包含值“3”。

当页面 [index.xhtml] 在处理完成后重新显示时,该值将导致显示上方的 [3,4]:

  • 该值决定了下拉列表中应显示的项 [3],
  • 字段 selectOneListBox1 的值显示在 [4] 中。

考虑 <h:selectOneListBox> 标签的一个变体:


<!-- 第 5 行 -->
          <h:outputText value="selectOneListBox (size=3)" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectOneListBox2Prompt']}"/>
            <h:selectOneListbox id="selectOneListBox2" value="#{form.selectOneListBox2}" size="3">
              <f:selectItem itemValue="1" itemLabel="un"/>
              <f:selectItem itemValue="2" itemLabel="deux"/>
              <f:selectItem itemValue="3" itemLabel="trois"/>
              <f:selectItem itemValue="4" itemLabel="quatre"/>
              <f:selectItem itemValue="5" itemLabel="cinq"/>
            </h:selectOneListbox>
          </h:panelGroup>
          <h:outputText value="#{form.selectOneListBox2}"/>

第 5 行中 <h:selectOneListBox> 标签在 [Form.java] 中的模板如下:


  private String selectOneListBox2="3";

首次请求页面 [index.xhtml] 时,生成的页面如下:

  • 代码 XHTML 的第 2 行生成 [1],
  • 文本 [2] 由第 4 行生成。带滚动条的列表 [3] 由 [5-11] 行生成。 正是 size="3" 属性的值导致生成的是带滚动条的列表,而非下拉列表。列表中的元素由第 6-8 行的 <f:selectItem> 标签生成,

[3]中选中的项是由调用getSelectOneListBox2()方法(第5行)确定的。 得到的结果“3”(Java代码第1行)导致列表第8行的项目被显示,这是因为其itemValue属性值为“3”,

  • 代码 XHTML 的第 13 行生成 [4]。此处再次使用了 [Form.java] 中的 getSelectOneListBox2 方法。

由页面 XHTML 生成的流程 HTML 如下:


<tr>
<td class="col1"><span class="info">selectOneListBox (size=3)</span></td>
<td class="col2">choix unique : <select id="formulaire:selectOneListBox2" name="formulaire:selectOneListBox2" size="3">
    <option value="1">un</option>
    <option value="2">deux</option>
    <option value="3" selected="selected">trois</option>
    <option value="4">quatre</option>
    <option value="5">cinq</option>
</select></td>
<td class="col3">3</td>
</tr>
  • 第 6 行:列表中选中了 value="3" 的元素,这体现在 selected="selected" 属性的存在上。

现在,在下方,让我们在列表中选择一个新值,并通过按钮提交表单。结果返回页面如下:

字段 [1] 的提交值如下:

formulaire%3AselectOneListBox2=5

通过 [2] 验证表单后,通过 [1] 的输入更新了 [Form.java] 表单。元素 HTML


    <option value="5">cinq</option>

已被选中。浏览器将字符串“5”作为生成下拉列表的组件 JSF 的值发送:


            <h:selectOneListbox id="selectOneListBox2" value="#{form.selectOneListBox2}" size="3">

控制器 JSF 将使用方法 setSelectOneListBox2("5") 来更新列表的模板。因此,更新后,字段


        private String selectOneListBox2;

字段将包含值“5”。

当页面 [index.xhtml] 在处理完成后重新显示时,该值将导致显示上方的 [3,4]:

  • 它决定了列表中应被选中的元素 [3],
  • 字段 selectOneListBox2 的值显示为 [4]。

2.5.15. 标签 <h:selectManyListBox>

<h:selectmanyListBox> 标签会生成一个 HTML <select multiple="multiple">...</select> 标签,允许用户在列表中选择多个项目。

请看以下代码:


<!-- 第 6 行 -->
          <h:outputText value="selectManyListBox (size=3)"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectManyListBoxPrompt']}"/>
            <h:selectManyListbox id="selectManyListBox" value="#{form.selectManyListBox}" size="3">
              <f:selectItem itemValue="1" itemLabel="un"/>
              <f:selectItem itemValue="2" itemLabel="deux"/>
              <f:selectItem itemValue="3" itemLabel="trois"/>
              <f:selectItem itemValue="4" itemLabel="quatre"/>
              <f:selectItem itemValue="5" itemLabel="cinq"/>
            </h:selectManyListbox>
            <p><input type="button" value="#{msg['form.buttonRazText']}" onclick="this.form['formulaire:selectManyListBox'].selectedIndex=-1;" /></p>
          </h:panelGroup>
          <h:outputText value="#{form.selectManyListBoxValue}"/>

及其在 [Form.java] 中的模板:


private String[] selectManyListBox=new String[]{"1","3"};

当首次请求页面 [index.xhtml] 时,生成的页面如下:

  • 代码 XHTML 的第 2 行生成 [1]
  • 文本 [2] 由第 4 行生成。列表 [3] 由 [5-11] 行生成。size="3" 属性使得该列表在特定时刻显示其中三个元素。 列表中选中的元素是通过调用 Java 模型中的 getSelectManyListBox() 方法(第 5 行)确定的。得到的结果 {"1","3"}(Java 代码第 1 行)是一个 String 类型的元素数组。 这些元素中的每一个都用于选择列表中的某个元素。在此,第6行和第10行中itemValue属性属于数组{"1","3"}的元素将被选中。[3]即展示了这一点。
  • 代码 XHTML 的第 14 行生成 [4]。此处调用的并非列表 Java 模型中的 getSelectManyListBox 方法,而是以下 getSelectManyListBoxValue 方法:

private String[] selectManyListBox=new String[]{"1","3"};
  ...
  // getter 和 setter
  
  public String getSelectManyListBoxValue(){
    return getValue(selectManyListBox);
  }
  
  private String getValue(String[] chaines){
    String value="[";
    for(String chaine : chaines){
      value+=" "+chaine;
    }
    return value+"]";
  }

如果调用的是 getSelectManyListBox 方法,则会得到一个 String 数组。为了将该元素纳入 HTML 数据流,控制器本应调用其 toString 方法。 但该方法针对数组时,仅返回其“哈希码”,而非我们期望的元素列表。因此,我们使用上述的 getSelectManyListBoxValue 方法来获取一个字符串,该字符串代表数组的内容,

  • 代码 XHTML 的第 12 行生成了按钮 [5]。 当点击该按钮时,onclick 属性的 JavaScript 代码将被执行。该代码将被嵌入到由代码 JSF 生成的页面 HTML 中。为了理解这一点,我们需要了解该页面的确切性质。

由页面 XHTML 生成的 HTML 流程如下:


<tr>
<td class="col1"><span class="info">selectManyListBox (size=3)</span></td>
<td class="col2">choix multiple : <select id="formulaire:selectManyListBox" name="formulaire:selectManyListBox" multiple="multiple" size="3">
    <option value="1" selected="selected">un</option>
    <option value="2">deux</option>
    <option value="3" selected="selected">trois</option>
    <option value="4">quatre</option>
    <option value="5">cinq</option>
</select>
            <p><input type="button" value="Raz" onclick="this.form['formulaire:selectManyListBox'].selectedIndex=-1;" /></p>
          </td>
<td class="col3">[ 1 3]</td>
</tr>
  • 第 3 行和第 9 行:由标签 JSF <h:selectManyListBox> 生成的标签 HTML <select multiple="multiple"...>...</select>。 正是 multiple 属性的存在表明这是一个多选列表,
  • 由于列表模板是字符串数组 {"1","3"},导致第4行(value="1")和第6行(value="3")的列表项具有selected="selected"属性,
  • 第10行:点击按钮[Raz]时,onclick属性的JavaScript代码将被执行。该页面在浏览器中通过一个常被称为DOM(文档对象模型)的对象树来表示。 树中的每个对象均可通过其 name 属性被 JavaScript 代码访问。上述代码 HTML 第 3 行中的列表名为 form:selectManyListBox。 表单本身可以通过多种方式进行引用。在此,它通过 this.form 这种写法进行引用,其中 this 指代按钮 [Raz],而 this.form 指代包含该按钮的表单。 列表表单:selectManyListBox位于该表单中。因此,表示法 this.form['formulaire:selectManyListBox'] 指代该列表在表单组件树中的位置。 表示列表的对象具有一个 selectedIndex 属性,其值为列表中选中项的编号。该编号从 0 开始,表示列表中的第一个元素。 值 -1 表示列表中未选中任何项目。若 JavaScript 代码将值 -1 赋给属性 selectedIndex,则会取消选中列表中的所有项目(如果存在的话)。

现在,请在下方列表中选择 [1] 的新值(若要选择列表中的多个元素,请按住 Ctrl 键并点击),然后点击 [Valider] [2] 按钮提交表单。 我们得到的响应页面如下:

提交的 [1] 字段值如下:

formulaire%3AselectManyListBox=3&formulaire%3AselectManyListBox=4&formulaire%3AselectManyListBox=5

通过 [2] 验证表单后,通过输入 [1] 更新了模板 [Form.java]。元素 HTML


    <option value="3">trois</option>
    <option value="4">quatre</option>
    <option value="5">cinq</option>

已被选中。浏览器将字符串“3”、“4”、“5”作为组件 JSF 的值发送,该组件生成了下拉列表:


            <h:selectManyListbox id="selectManyListBox" value="#{form.selectManyListBox}" size="3">

将使用模型中的 setSelectManyListBox 方法,根据浏览器发送的值更新此模型:


  private String[] selectManyListBox;
....
  public void setSelectManyListBox(String[] selectManyListBox) {
    this.selectManyListBox = selectManyListBox;
}

第 3 行可见,该方法的参数是一个 String 数组。此处将使用数组 {"3","4","5"}。更新后,字段


        private String[] selectManyListBox;

字段现在包含数组 {"3","4","5"}。

当页面[index.xhtml]在处理完成后重新显示时,该值将导致显示上方的[3,4]:

  • 它决定了列表中应选中的项目 [3],
  • 字段 selectManyListBox 的值显示在 [4] 中。

2.5.16. <h:selectOneMenu> 标签

标签 <h:selectOneMenu> 与标签 <h:selectOneListBox size="1"> 相同。在示例中,执行的代码 JSF 如下:


<!-- 第 7 行 -->
          <h:outputText value="selectOneMenu" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectOneMenuPrompt']}"/>
            <h:selectOneMenu id="selectOneMenu" value="#{form.selectOneMenu}">
              <f:selectItem itemValue="1" itemLabel="un"/>
              <f:selectItem itemValue="2" itemLabel="deux"/>
              <f:selectItem itemValue="3" itemLabel="trois"/>
              <f:selectItem itemValue="4" itemLabel="quatre"/>
              <f:selectItem itemValue="5" itemLabel="cinq"/>
            </h:selectOneMenu>
          </h:panelGroup>
          <h:outputText value="#{form.selectOneMenu}"/>

[Form.java] 中 <h:selectOneMenu> 标签的模板如下:


  private String selectOneMenu="1";

在首次请求页面 [index.xhtml] 时,上述代码生成以下视图:

一个执行示例如下:

字段 [1] 的提交值如下:

formulaire%3AselectOneMenu=4

2.5.17. 标签 <h:selectManyMenu>

标签 <h:selectManyMenu> 与标签 <h:selectManyListBox size="1"> 完全相同。示例中执行的代码 JSF 如下:


<!-- 第 8 行 -->
          <h:outputText value="selectManyMenu" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectManyMenuPrompt']}" styleClass="prompt" />
            <h:selectManyMenu id="selectManyMenu" value="#{form.selectManyMenu}" >
              <f:selectItem itemValue="1" itemLabel="un"/>
              <f:selectItem itemValue="2" itemLabel="deux"/>
              <f:selectItem itemValue="3" itemLabel="trois"/>
              <f:selectItem itemValue="4" itemLabel="quatre"/>
              <f:selectItem itemValue="5" itemLabel="cinq"/>
            </h:selectManyMenu>
            <p><input type="button" value="#{msg['form.buttonRazText']}" onclick="this.form['formulaire:selectManyMenu'].selectedIndex=-1;" /></p>
          </h:panelGroup>
          <h:outputText value="#{form.selectManyMenuValue}" styleClass="prompt"/>

[Form.java] 中 <h:selectManyMenu> 标签的模板如下:


    private String[] selectManyMenu=new String[]{"1","2"};

在首次请求页面 [index.xhtml] 时,上述代码生成以下页面:

列表 [1] 包含文本“一”、……、“五”,其中“一”和“二”已被选中。生成的代码 HTML 如下:


<tr>
<td class="col1"><span class="info">selectManyMenu</span></td>
<td class="col2"><span class="prompt">choix multiple : </span><select id="formulaire:selectManyMenu" name="formulaire:selectManyMenu" multiple="multiple" size="1">
    <option value="1" selected="selected">un</option>
    <option value="2" selected="selected">deux</option>
    <option value="3">trois</option>
    <option value="4">quatre</option>
    <option value="5">cinq</option>
</select>
            
            
            <p><input type="button" value="Raz" onclick="this.form['formulaire:selectManyMenu'].selectedIndex=-1;" /></p>
          </td>
<td class="col3"><span class="prompt">[ 1 2]</span></td>
</tr>

从上文第 4 行和第 5 行可以看出,元素“un”和“deux”已被选中(存在 selected 属性)。

由于无法在菜单中显示所选元素,因此很难提供一个运行示例的屏幕截图。建议读者自行测试(要在列表中选择多个元素,请按住 Ctrl 键并点击)。

2.5.18. <h:inputHidden> 标签

<h:inputHidden> 标签没有视觉呈现效果。它仅用于在页面的 HTML <input type="hidden" value="..."/> 标签流中插入一个 inputHidden 标签。 当这些标签包含在 <h:form> 标签内时,其值将成为表单提交至服务器时发送的数据的一部分。由于它们是表单字段且用户无法看到,因此被称为隐藏字段。这些字段的作用是在同一客户端的不同请求/响应循环之间保留内存:

  • 客户端请求表单 F。服务器将其发送给客户端,并将信息 I 放入隐藏字段 C 中,形式为 <h:inputHidden id="C" value="I"/>,
  • 当客户端填写完表单F并将其提交至服务器时,字段C中的值I会被发回给服务器。服务器随即可以检索到其先前存储在页面中的信息I。这样便在两个请求/响应周期之间建立了一条记忆通道,
  • JSF 本身就采用了这种技术。它存储在表单 F 中的信息 I 是该表单所有组件的值。为此,它使用了以下隐藏字段:

<input type="hidden" name="javax.faces.ViewState" id="javax.faces.ViewState" value="H4sIAAAAAAAAANV...8PswawAA" />

隐藏字段名为 javax.faces.ViewState,其值是一个字符串,以编码形式表示发送给客户端的页面中所有组件的值。当客户端在表单中输入数据后提交页面时,隐藏字段 javax.faces.ViewState 会随输入的值一同返回。 正是这一机制使得控制器 JSF 能够还原页面最初发送时的状态。该机制已在第 72 页进行过说明。

示例中的代码 JSF 如下:


<!-- 第 9 行 -->
          <h:outputText value="inputHidden"  styleClass="info"/>
          <h:inputHidden id="inputHidden" value="#{form.inputHidden}"/>
          <h:outputText value="#{form.inputHidden}"/>

[Form.java] 中 <h:inputHidden> 标签的模板如下:


  private String inputHidden="initial";

这导致在首次请求页面 [index.xhtml] 时显示如下:

  • 第 2 行生成 [1],第 4 行生成 [2]。第 3 行未生成任何视觉元素。

生成的代码 HTML 如下:


<tr>
<td class="col1"><span class="info">inputHidden</span></td>
<td class="col2"><input id="formulaire:inputHidden" type="hidden" name="formulaire:inputHidden" value="initial" /></td>
<td class="col3">initial</td>
</tr>

在表单的 POST 阶段,第 3 行中名为 formulaire:inputHidden 的字段的“初始”值将与表单中的其他值一起提交。该字段


  private String inputHidden;

将更新为该值,即其最初的值。该值将被包含在返回给客户端的新页面中。因此,最终显示效果始终如上图所示。

隐藏字段的提交值为:

formulaire%3AinputHidden=initial

2.5.19. 标签 <h:selectBooleanCheckBox>

标签 <h:selectBooleanCheckBox> 会生成一个 HTML <input type="checkbox" ...> 标签。

考虑以下 JSF 代码:


<!-- 第 10 行 -->
  <h:outputText value="selectBooleanCheckbox" styleClass="info"/>
  <h:panelGroup>
    <h:outputText value="#{msg['form.selectBooleanCheckboxPrompt']}" styleClass="prompt" />
    <h:selectBooleanCheckbox id="selectBooleanCheckbox" value="#{form.selectBooleanCheckbox}"/>
  </h:panelGroup>
  <h:outputText value="#{form.selectBooleanCheckbox}"/>

上文第 5 行中 <h:selectBooleanCheckbox> 标签在 [Form.java] 中的模板如下:


  private boolean selectBooleanCheckbox=true;

首次请求页面 [index.xhtml] 时,获得的页面如下:

  • 代码 XHTML 的第 2 行生成 [1],
  • 文本 [2] 由第 4 行生成。复选框 [3] 由 [5] 行生成。 在此,[Form.java] 的方法 getSelectBooleanCheckbox 被用于决定是否勾选该复选框。由于该方法将布尔值设为 true(参见 Java 代码),因此复选框被勾选,
  • 代码 XHTML 的第 7 行生成 [4]。 随后再次调用 [Form.java] 中的 getSelectBooleanCheckbox 方法来生成文本 [4]。

由前一个代码 JSF 生成的流程 HTML 如下:


<tr>
<td class="col1"><span class="info">selectBooleanCheckbox</span></td>
<td class="col2"><span class="prompt">mari&eacute;(e) : </span>
<input id="formulaire:selectBooleanCheckbox" type="checkbox" name="formulaire:selectBooleanCheckbox" checked="checked" /></td>
<td class="col3">true</td>
</tr>

在 [4] 中,可以看到生成的 HTML <input type="checkbox"> 标签。 关联模板的值 true 导致该标签被添加了 checked="checked" 属性。因此该复选框处于选中状态。

现在,在下方取消勾选复选框 [1],提交表单 [2],并查看生成的结果 [3, 4]:

由于该复选框未被选中,因此字段 [1] 没有提交值。

通过 [2] 验证表单后,系统通过 [1] 的输入更新了 [Form.java] 模板。 随后,[Form.java] 中的字段 selectBooleanCheckbox 被赋值为 false。 重新显示 [index.xhtml] 可见,该模型的 selectBooleanCheckbox 字段确实已更新为 [3] 和 [4]。 值得注意的是,正是得益于隐藏字段 javax.faces.ViewState,JSF 才能够判断出用户已取消了最初勾选的复选框。事实上,未勾选复选框的值并不包含在浏览器提交的数据中。 借助存储在隐藏字段 javax.faces.ViewState 中的组件树,JSF 得以确定表单中曾存在一个名为“selectBooleanCheckbox”的复选框,且其值未包含在客户端浏览器提交的数据中。 由此可推断,该复选框在提交的表单中处于未选中状态,这使得它能够将布尔值 false 赋值给关联的 Java 模型:


  private boolean selectBooleanCheckbox;

2.5.20. 标签 <h:selectManyCheckBox>

标签 <h:selectManyCheckBox> 会生成一组复选框,因此会生成多个 HTML <input type="checkbox" ...>。该标签与 <h:selectManyListBox> 标签功能相同,区别仅在于待选元素以相邻复选框的形式呈现,而非列表形式。关于 <h:selectManyListBox> 标签的说明在此同样适用。

请看以下 JSF 代码:


          <!-- 第 11 行 -->
          <h:outputText value="selectManyCheckbox" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectManyCheckboxPrompt']}" styleClass="prompt" />
            <h:selectManyCheckbox id="selectManyCheckbox" value="#{form.selectManyCheckbox}">
              <f:selectItem itemValue="1" itemLabel="rouge"/>
              <f:selectItem itemValue="2" itemLabel="bleu"/>
              <f:selectItem itemValue="3" itemLabel="blanc"/>
              <f:selectItem itemValue="4" itemLabel="noir"/>
            </h:selectManyCheckbox>
          </h:panelGroup>
<h:outputText value="#{form.selectManyCheckboxValue}"/>

上文第 5 行中 [Form.java] 中的 <h:selectManyCheckbox> 标签模板如下:


private String[] selectManyCheckbox=new String[]{"1","3"};

当首次请求页面 [index.xhtml] 时,获得的页面如下:

  • 代码 XHTML 的第 2 行生成 [1],
  • 文本 [2] 由第 4 行生成。复选框 [3] 由第 5-10 行生成。对于每个复选框:
  • itemLabel 属性定义了复选框旁显示的文本;
  • itemvalue 属性定义了当复选框被选中时将提交至服务器的值;

这四个复选框的模板是以下 Java 字段:


private String[] selectManyCheckbox=new String[]{"1","3"};

该数组定义:

  • 页面显示时,需要勾选的复选框。这是通过其值 c.a.d 实现的。其字段为 itemValue。 如上所述,数组 {"1","3"} 中对应的复选框将被勾选。这正是上图所示的效果;
  • 当页面提交时,模板 selectManyCheckbox 会接收用户勾选的复选框值数组。接下来我们将看到,
  • 代码 XHTML 的第 12 行生成了 [4]。而生成的 [4] 则是由后续的 getSelectManyCheckboxValue 方法生成的:

  public String getSelectManyCheckboxValue(){
    return getValue(getSelectManyCheckbox());
  }
  
  private String getValue(String[] chaines){
    String value="[";
    for(String chaine : chaines){
      value+=" "+chaine;
    }
    return value+"]";
}

由前一个代码 JSF 生成的流程 HTML 如下:


    <tr>
<td>
<input name="formulaire:selectManyCheckbox" id="formulaire:selectManyCheckbox:0" value="1" type="checkbox" checked="checked" /><label for="formulaire:selectManyCheckbox:0"> rouge</label></td>
<td>
<input name="formulaire:selectManyCheckbox" id="formulaire:selectManyCheckbox:1" value="2" type="checkbox" /><label for="formulaire:selectManyCheckbox:1"> bleu</label></td>
<td>
<input name="formulaire:selectManyCheckbox" id="formulaire:selectManyCheckbox:2" value="3" type="checkbox" checked="checked" /><label for="formulaire:selectManyCheckbox:2"> blanc</label></td>
<td>
<input name="formulaire:selectManyCheckbox" id="formulaire:selectManyCheckbox:3" value="4" type="checkbox" /><label for="formulaire:selectManyCheckbox:3"> noir</label></td>
    </tr>
</table></td>
<td class="col3">[ 1 3]</td>
</tr>

生成了四个 HTML <input type="checkbox" ...> 标签。第 3 行和第 7 行的标签具有 checked="checked" 属性,因此显示为已勾选状态。 需要注意的是,它们都具有相同的 name="formulaire:selectManyCheckbox" 属性,也就是说这四个 HTML 字段名称相同。如果用户勾选了第 5 行和第 9 行的复选框,浏览器将以以下形式发送这四个复选框的值:

formulaire:selectManyCheckbox=2&formulaire:selectManyCheckbox=4

,而这四个复选框的模板为


private String[] selectManyCheckbox=new String[]{"1","3"};

将接收数组 {"2","4"}。

下面我们来验证一下。在 [1] 中进行修改,在 [2] 中提交表单。在 [3] 中得到的结果:

[1]字段的提交值如下:

formulaire%3AselectManyCheckbox=2&formulaire%3AselectManyCheckbox=4

2.5.21. 标签 <h:selectOneRadio>

<h:selectOneRadio> 标签会生成一组互斥的单选按钮。

请看以下代码 JSF:


<!-- 第 12 行 -->
          <h:outputText value="selectOneRadio" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectOneRadioPrompt']}" />
            <h:selectOneRadio id="selectOneRadio" value="#{form.selectOneRadio}">
              <f:selectItem itemValue="1" itemLabel="voiture"/>
              <f:selectItem itemValue="2" itemLabel="vélo"/>
              <f:selectItem itemValue="3" itemLabel="scooter"/>
              <f:selectItem itemValue="4" itemLabel="marche"/>
            </h:selectOneRadio>
          </h:panelGroup>
          <h:outputText value="#{form.selectOneRadio}"/>

上文第 5 行中的 <h:selectOneRadio> 标签在 [Form.java] 中的模板如下:


  private String selectOneRadio="2";

首次请求页面 [index.xhtml] 时,显示的视图如下:

  • 代码 XHTML 的第 2 行生成 [1],
  • 文本 [2] 由第 4 行生成。单选按钮 [3] 由第 5-10 行生成。对于每个按钮:
  • itemLabel 属性定义了单选按钮旁显示的文本;
  • itemvalue 属性定义了当单选按钮被选中时将提交至服务器的值;

这四个单选按钮的模板是以下 Java 字段:


  private String selectOneRadio="2";

该模板定义:

  • 页面显示时,应被选中的唯一单选按钮。这是通过其值 c.a.d 实现的。其字段为 itemValue。 如上所示,值设为“2”的单选按钮将被选中。这正是上图所示的内容;
  • 当页面提交时,模板 selectOneRadio 会接收被选中单选按钮的值。接下来我们将看到,
  • 代码 XHTML 的第 12 行会生成 [4]。

由前面的代码 JSF 生成的 HTML 流程如下:


<tr>
<td class="col1"><span class="info">selectOneRadio</span></td>
<td class="col2">moyen de transport pr&eacute;f&eacute;r&eacute; : <table id="formulaire:selectOneRadio">
    <tr>
<td>
<input type="radio" name="formulaire:selectOneRadio" id="formulaire:selectOneRadio:0" value="1" /><label for="formulaire:selectOneRadio:0"> voiture</label></td>
<td>
<input type="radio" checked="checked" name="formulaire:selectOneRadio" id="formulaire:selectOneRadio:1" value="2" /><label for="formulaire:selectOneRadio:1"> v&eacute;lo</label></td>
<td>
<input type="radio" name="formulaire:selectOneRadio" id="formulaire:selectOneRadio:2" value="3" /><label for="formulaire:selectOneRadio:2"> scooter</label></td>
<td>
<input type="radio" name="formulaire:selectOneRadio" id="formulaire:selectOneRadio:3" value="4" /><label for="formulaire:selectOneRadio:3"> marche</label></td>
</tr>

生成了四个 HTML <input type="radio" ...> 标签。第 8 行标签的 checked="checked" 属性使得对应的单选按钮显示为已选中状态。 需要注意的是,这些标签都具有相同的 name="formulaire:selectOneRadio" 属性,也就是说这四个 HTML 字段名称相同。这是实现互斥单选按钮组的必要条件:当其中一个被选中时,其余的均未被选中。

如下所示,在 [1] 中勾选其中一个单选按钮,在 [2] 中提交表单,在 [3] 中显示结果:

字段 [1] 的提交值如下:

formulaire%3AselectOneRadio=4

2.6. 示例 mv-jsf2-04:动态列表

2.6.1. 应用程序

该应用程序与之前相同:

唯一的改动在于 [1] 和 [2] 区域中列表元素的生成方式。 在此,它们由 Java 代码动态生成,而在之前的版本中,它们是“硬编码”在 JSF 页面代码中的。

2.6.2. NetBeans 项目

该应用程序的 NetBeans 项目如下:

项目 [mv-jsf2-04] 与项目 [mv-jsf2-03] 完全相同,仅存在以下差异:

  • 在 [1] 中,JSF 页面的列表项将不再以“硬编码”形式写入代码中,
  • 在 [2] 中,页面模板 JSF [1] 将被修改,
  • 更新为 [3],其中一条消息将被修改。

2.6.3. 页面 [index.xhtml] 及其模板 [Form.java]

页面 JSF [index.xhtml] 变为如下:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">

  <f:view locale="#{changeLocale.locale}">
    <h:head>
      <title>JSF</title>
      <h:outputStylesheet library="css" name="styles.css"/>
    </h:head>
    <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
      <h:form id="formulaire">
        <!-- 语言 -->
        <h:panelGrid columns="2">
          <h:commandLink value="#{msg['form.langue1']}" action="#{changeLocale.setFrenchLocale}"/>
          <h:commandLink value="#{msg['form.langue2']}" action="#{changeLocale.setEnglishLocale}"/>
        </h:panelGrid>
        <h1><h:outputText value="#{msg['form.titre']}"/></h1>
        <h:panelGrid columnClasses="col1,col2,col3" columns="3" border="1">
...
          <!-- 第 4 行 -->
          <h:outputText value="selectOneListBox (size=1)" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectOneListBox1Prompt']}"/>
            <h:selectOneListbox id="selectOneListBox1" value="#{form.selectOneListBox1}" size="1">
              <f:selectItems value="#{form.selectOneListbox1Items}"/>
            </h:selectOneListbox>
          </h:panelGroup>
          <h:outputText value="#{form.selectOneListBox1}"/>
          <!-- 第 5 行 -->
          <h:outputText value="selectOneListBox (size=3)" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectOneListBox2Prompt']}"/>
            <h:selectOneListbox id="selectOneListBox2" value="#{form.selectOneListBox2}" size="3">
              <f:selectItems value="#{form.selectOneListbox2Items}"/>
            </h:selectOneListbox>
          </h:panelGroup>
          <h:outputText value="#{form.selectOneListBox2}"/>
          <!-- 第 6 行 -->
          <h:outputText value="selectManyListBox (size=3)"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectManyListBoxPrompt']}"/>
            <h:selectManyListbox id="selectManyListBox" value="#{form.selectManyListBox}" size="3">
              <f:selectItems value="#{form.selectManyListBoxItems}"/>
            </h:selectManyListbox>
            <p><input type="button" value="#{msg['form.buttonRazText']}" onclick="this.form['formulaire:selectManyListBox'].selectedIndex=-1;" /></p>
          </h:panelGroup>
          <h:outputText value="#{form.selectManyListBoxValue}"/>
          <!-- 第 7 行 -->
          <h:outputText value="selectOneMenu" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectOneMenuPrompt']}"/>
            <h:selectOneMenu id="selectOneMenu" value="#{form.selectOneMenu}">
              <f:selectItems value="#{form.selectOneMenuItems}"/>
            </h:selectOneMenu>
          </h:panelGroup>
          <h:outputText value="#{form.selectOneMenu}"/>
          <!-- 第8行 -->
          <h:outputText value="selectManyMenu" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectManyMenuPrompt']}" styleClass="prompt" />
            <h:selectManyMenu id="selectManyMenu" value="#{form.selectManyMenu}" >
              <f:selectItems value="#{form.selectManyMenuItems}"/>
            </h:selectManyMenu>
            <p><input type="button" value="#{msg['form.buttonRazText']}" onclick="this.form['formulaire:selectManyMenu'].selectedIndex=-1;" /></p>
          </h:panelGroup>
          <h:outputText value="#{form.selectManyMenuValue}" styleClass="prompt"/>
...
          <!-- 第11行 -->
          <h:outputText value="selectManyCheckbox" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectManyCheckboxPrompt']}" styleClass="prompt" />
            <h:selectManyCheckbox id="selectManyCheckbox" value="#{form.selectManyCheckbox}">
              <f:selectItems value="#{form.selectManyCheckboxItems}"/>
            </h:selectManyCheckbox>
          </h:panelGroup>
          <h:outputText value="#{form.selectManyCheckboxValue}"/>
          <!-- 第12行 -->
          <h:outputText value="selectOneRadio" styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.selectOneRadioPrompt']}" />
            <h:selectOneRadio id="selectOneRadio" value="#{form.selectOneRadio}">
              <f:selectItems value="#{form.selectOneRadioItems}"/>
            </h:selectOneRadio>
          </h:panelGroup>
          <h:outputText value="#{form.selectOneRadio}"/>
        </h:panelGrid>
        <p>
          <h:commandButton type="submit" id="submit" value="#{msg['form.submitText']}"/>
        </p>
      </h:form>
    </h:body>
  </f:view>
</html>

所做的修改体现在第26至28行。此前,该处代码为:


<h:selectOneListbox id="selectOneListBox1" value="#{form.selectOneListBox1}" size="1">
              <f:selectItem itemValue="1" itemLabel="un"/>
              <f:selectItem itemValue="2" itemLabel="deux"/>
              <f:selectItem itemValue="3" itemLabel="trois"/>
</h:selectOneListbox>

现在变为:


<h:selectOneListbox id="selectOneListBox1" value="#{form.selectOneListBox1}" size="1">
              <f:selectItems value="#{form.selectOneListbox1Items}"/>
</h:selectOneListbox>

第2至4行的三个<f:selectItem>标签已被第b行中的单个<f:selectItems>标签所取代。 该标签有一个 value 属性,其值为 javax.faces.model.SelectItem 类型的元素集合。在上文中,value 属性的值将通过调用以下方法 [form].getSelectOneListbox1Items 来获取:


  public SelectItem[] getSelectOneListbox1Items() {
    return getItems("A",3);
  }

  private SelectItem[] getItems(String label, int qte) {
    SelectItem[] items=new SelectItem[qte];
    for(int i=0;i<qte;i++){
      items[i]=new SelectItem(i,label+i);
    }
    return items;
}
  • 第 1 行,方法 getSelectOneListbox1Items 返回一个由第 5 行中的私有方法 getItems 构建的 javax.faces.model.SelectItem 类型元素数组。 需要注意的是,方法 getSelectOneListbox1Items 并非私有字段 selectOneListBox1Items 的 getter,
  • javax.faces.model.SelectItem 拥有多种构造函数。

Image

我们使用方法 getItems 的第 8 行,即构造函数 SelectItem(Object value, String label),该构造函数对应标签 JSF


    <f:selectItem itemValue="value" labelValue="label"/>
  • 第5-10行:方法getItems(String label, int qte)构建了一个包含qte个SelectItem类型元素的数组,其中第i个元素由构造函数SelectItem(i, label+i)生成。

代码 JSF


<h:selectOneListbox id="selectOneListBox1" value="#{form.selectOneListBox1}" size="1">
              <f:selectItems value="#{form.selectOneListbox1Items}"/>
</h:selectOneListbox>

在功能上等同于以下代码 JSF:


<h:selectOneListbox id="selectOneListBox1" value="#{form.selectOneListBox1}" size="1">
              <f:selectItem itemValue="0" itemLabel="A0"/>
              <f:selectItem itemValue="1" itemLabel="A1"/>
              <f:selectItem itemValue="2" itemLabel="A2"/>
</h:selectOneListbox>

JSF 页面上的所有其他列表也进行了同样的处理。因此,在 [Form.java] 模板中,可以找到以下新方法:


  public SelectItem[] getSelectOneListbox1Items() {
    return getItems("A",3);
  }
  
  public SelectItem[] getSelectOneListbox2Items() {
    return getItems("B",4);
  }
  
  public SelectItem[] getSelectManyListBoxItems() {
    return getItems("C",5);
  }
  
  public SelectItem[] getSelectOneMenuItems() {
    return getItems("D",3);
  }
  
  public SelectItem[] getSelectManyMenuItems() {
   return getItems("E",4);
   }
  
  public SelectItem[] getSelectManyCheckboxItems() {
   return getItems("F",3);
   }
  
  public SelectItem[] getSelectOneRadioItems() {
   return getItems("G",4);
   }
  
  private SelectItem[] getItems(String label, int qte) {
    SelectItem[] items=new SelectItem[qte];
    for(int i=0;i<qte;i++){
      items[i]=new SelectItem(i,label+i);
    }
    return items;
}

2.6.4. 消息文件

仅修改了一条消息:

[messages_fr.properties]


form.titre=Java Server Faces - remplissage dynamique des listes

[messages_en.properties]


form.titre=Java Server Faces - dynamic filling of lists of elements

2.6.5. 测试

欢迎读者测试此新版本。

通常情况下,表单中的动态元素是业务处理的结果,或来自数据库:

让我们通过浏览器对页面 JSF [index.xhtml] 的初始请求进行分析:

  • 页面 JSF 被请求为 [1],
  • 控制器 [Faces Servlet] 请求将其显示为 [3]。 处理该页面的引擎 JSF 调用该页面的模型 [Form.java],例如调用方法 getSelectOneListBox1Items。 该方法完全可能根据数据库中存储的信息,返回一个 SelectItem 类型的元素数组。为此,它将调用 [métier] [2b] 层。

2.7. 示例 mv-jsf2-05:导航 – 会话 – 异常处理

2.7.1. 应用程序

该应用程序与之前相同,只是表单现在以多页向导的形式呈现:

  • 在 [1] 中,表单的第 1 页——也可通过 [2] 中的链接 1 访问
  • 在 [2] 中,一组包含 5 个链接。
  • 在 [3] 中,通过 [2] 的第 2 个链接访问到的表单第 2 页
  • 在 [4] 中,通过 [2] 的链接 3 获得的表单第 3 页
  • 在 [5] 中,通过 [2] 中的“抛出异常”链接获得的页面
  • 在 [6] 中,通过 [2] 的链接 4 获取的页面。该页面汇总了第 1 至 3 页中的输入内容。

2.7.2. NetBeans 项目

该应用程序的 NetBeans 项目如下:

项目 [mv-jsf2-05] 引入了两项新功能:

  1. 在 [1] 中,页面 JSF [index.xhtml] 被拆分为三个页面 [form1.xhtml, form2.xhtml, form3.xhtml],相关输入内容已分配至这些页面。 页面 [form4.xhtml] 是上一版本中页面 [index.xhtml] 的副本。 在 [2] 中,类 [Form.java] 保持不变。它将作为前四个页面 JSF 的模板,
  2. 在 [3] 中,新增了一个名为 [exception.xhtml] 的页面:当应用程序中发生异常时,将使用该页面。

2.7.3. 页面 [form.xhtml] 及其模板 [Form.java]

2.7.3.1. XHTML 页面的代码

JSF [form1.xhtml] 页面内容如下:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <f:view locale="#{changeLocale.locale}">
    <h:head>
      <title>JSF</title>
      <h:outputStylesheet library="css" name="styles.css"/>
    </h:head>
    <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
    <h:form id="formulaire">
        <!-- 链接 -->
        <h:panelGrid columns="2">
          <h:commandLink value="#{msg['form.langue1']}" action="#{changeLocale.setFrenchLocale}"/>
          <h:commandLink value="#{msg['form.langue2']}" action="#{changeLocale.setEnglishLocale}"/>
        </h:panelGrid>
        <h1><h:outputText value="#{msg['form1.titre']}"/></h1>
        <h:panelGrid columnClasses="col1,col2" columns="2" border="1">
          <h:outputText value="#{msg['form.headerCol1']}" styleClass="entete"/>
          <h:outputText value="#{msg['form.headerCol2']}" styleClass="entete"/>
          <!-- 第 1 行 -->
          <h:outputText value="inputText"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.loginPrompt']}"/>
            <h:inputText id="inputText" value="#{form.inputText}"/>
          </h:panelGroup>
          <!-- 第 2 行 -->
          <h:outputText value="inputSecret"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.passwdPrompt']}"/>
            <h:inputSecret id="inputSecret" value="#{form.inputSecret}"/>
          </h:panelGroup>
          <!-- 第3行 -->
          <h:outputText value="inputTextArea" styleClass="info"/>          
          <h:panelGroup>
            <h:outputText value="#{msg['form.descPrompt']}"/>
            <h:inputTextarea id="inputTextArea" value="#{form.inputTextArea}" rows="4"/>
          </h:panelGroup>         
        </h:panelGrid>
        <!-- 链接 -->
        <h:panelGrid columns="6">
          <h:commandLink value="1" action="form1"/>
          <h:commandLink value="2" action="#{form.doAction2}"/>
          <h:commandLink value="3" action="form3"/>
          <h:commandLink value="4" action="#{form.doAction4}"/>
          <h:commandLink value="#{msg['form.pagealeatoireLink']}" action="#{form.doAlea}"/>
          <h:commandLink value="#{msg['form.exceptionLink']}" action="#{form.throwException}"/>
        </h:panelGrid>
      </h:form>
      </h:body>
  </f:view>
</html>

并对应以下显示:

请注意以下几点:

  • 第 16 行,该表格原先有三列,现仅剩两列。显示模型值的第 3 列已被删除。这些值将由 [form4.xhtml] 显示,
  • 第40-46行:一个包含六个链接的表格。第44行和第46行的链接采用静态导航:其action属性为硬编码。其余链接采用动态导航:其action属性指向表单Bean中负责返回导航键的方法。[Form.java]中引用的方法如下:

// 事件
  public String doAction2(){
    return "form2";
  }
  
  public String doAction4(){
    return "form4";
  }
  
  public String doAlea(){
    // 1 到 3 之间的随机数
    int i=1+(int)(3*Math.random());
    // 返回导航键
    return "form"+i;
  }
  
  public String throwException() throws java.lang.Exception{
    throw new Exception("Exception test");
}

目前我们暂不讨论第17行中的throwException方法,稍后会再回到这一点。doAction2和doAction4方法仅返回导航键,不进行任何处理。 因此,我们完全也可以这样编写:


<h:commandLink value="1" action="form1"/>
          <h:commandLink value="2" action="form2"/>
          <h:commandLink value="3" action="form3"/>
          <h:commandLink value="4" action="form4"/>
          <h:commandLink value="#{msg['form.pagealeatoireLink']}" action="#{form.doAlea}"/>
          <h:commandLink value="#{msg['form.exceptionLink']}" action="#{form.throwException}"/>

而方法 doAlea 则生成一个随机导航键,其取值范围为集合 {"form1","form2","form3"}。

[form2.xhtml, form3.xhtml, form3.xhtml] 页面的代码与 [form1.xhtml] 页面类似。

2.7.3.2. [Form.java] 模板在 [form*.xhtml] 页面中的生命周期

考虑以下操作序列:

  • 在 [1] 中,填写第 1 页并跳转至第 3 页,
  • 在 [2] 中,填充第 3 页并返回第 1 页,
  • 在 [3] 中,第 1 页显示为输入时的状态。随后返回第 3 页,
  • 在 [4] 中,第 3 页将以输入时的原样显示。

仅靠隐藏字段 [javax.faces.ViewState] 的机制无法解释这一现象。

在从 [1] 切换到 [2] 时,会发生多个步骤:

  • 模板 [Form.java] 通过 POST 更新为 [form1.jsp]。值得注意的是,字段 inputText 接收了值“un autre texte”,
  • 导航键“form3”将触发 [form3.xhtml] 的显示。 嵌入在 [form3.xhtml] 中的 ViewState 仅包含 [form3.xhtml] 的组件状态,而不包含 [form1.xhtml] 的组件状态。

从 [2] 切换到 [3] 时:

  • [Form.java] 模型将根据 [form3.xhtml] 中的 POST 进行更新。 如果 [Form.java] 模板的生命周期已过期,则会先创建一个全新的 [Form.java] 对象,然后将其更新为 [form3.xhtml] 生成的 POST。 在此情况下,该模型的 inputText 字段将恢复其默认值:

  private String inputText="texte";

并保持该值不变:因为在 [form3.xhtml] 生成的 POST 中, 没有任何内容会更新 inputText 字段,该字段属于 [form1.xhtml] 的模板,而非 [form3.xhtml] 的模板,

  • 导航键“form1”会显示[form1.xhtml]。 页面显示其模板。在此情况下,与模板 inputText 关联的输入字段 login 将显示 texte,而非在 [1] 中输入的“其他文本”值。 若要使字段 inputText 保留在 [1] 中输入的值,则模板 [Form.java] 的生命周期必须设置为会话(session),而非请求(request)。在此情况下,
    • 当 [form1.xhtml] 执行完 POST 时,该模板将被放入客户端的会话中。字段 inputText 将取值为“其他文本”,
    • 执行 [form3.xhtml] 的 POST 时,系统将从该会话中检索该模板,并由 [form3.xhtml] 的 POST 进行更新。 字段 inputText 不会被此 POST 更新,而是将保留 (该值是在 POST 处理 [form1.xhtml] 和 [1] 时获得的)。

因此,[Form.java] Bean 的声明如下:


package forms;

import javax.enterprise.context.SessionScoped;
import javax.faces.bean.ManagedBean;
import javax.faces.model.SelectItem;

@ManagedBean
@SessionScoped
public class Form {

第 8 行将该 Bean 的作用域设置为会话

2.7.4. 异常处理

让我们回顾一下 JSF 应用程序的总体架构:

当事件处理程序或模型捕获来自业务层的异常(例如与数据库意外断开连接)时,会发生什么?

  • [2a] 事件处理程序可以拦截来自 [métier] 层的所有异常,并向 [Faces Servlet] 控制器返回一个导航键,用于跳转至该异常对应的特定错误页面;
  • 对于模板而言,此方案不可行,因为当调用 [3,4] 时,系统正处于渲染特定页面 XHTML 的阶段,而非选择页面的阶段。 那么,在页面渲染过程中,该如何切换页面呢? 一个简单但并非总是适用的解决方案是:不处理该异常,使其上报至执行应用程序的Servlet容器。该容器可配置为在异常上报至Servlet容器时显示特定页面。该方案始终可行,我们现在来探讨一下。

2.7.4.1. Web应用程序的异常处理配置

Web 应用程序的异常处理配置在文件 [web.xml] 中进行:


<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  <context-param>
    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
    <param-value>client</param-value>
  </context-param>  
  <context-param>
    <param-name>javax.faces.PROJECT_STAGE</param-name>
    <param-value>Development</param-value>
  </context-param>
  <context-param>
    <param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
    <param-value>true</param-value>
  </context-param> 
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/faces/*</url-pattern>
  </servlet-mapping>
  <session-config>
    <session-timeout>
      30
    </session-timeout>
  </session-config>
  <welcome-file-list>
    <welcome-file>faces/form1.xhtml</welcome-file>
  </welcome-file-list>
  <error-page>
    <error-code>500</error-code>
    <location>/faces/exception.xhtml</location>
  </error-page>
  <error-page>
    <exception-type>java.lang.Exception</exception-type>
    <location>/faces/exception.xhtml</location>
  </error-page>
</web-app>

第 32-39 行定义了两个错误页面。<error-page> 标签的数量可根据需要任意增加。<location> 标签指定了发生错误时要显示的页面。与页面关联的错误类型可通过以下两种方式定义:

  • 通过 <exception-type> 标签,该标签定义了所处理异常的 Java 类型。 因此,第36-39行的<error-page>标签表示:若Servlet容器在应用程序运行过程中捕获到类型为[java.lang.Exception]或其派生类型的异常(第37行),则必须显示页面[/faces/exception.xhtml] (第38行)。此处采用最通用的异常类型[java.lang.Exception],可确保通过
  • 通过定义错误代码 HTTP 的 <error-code> 标签(第 33 行)来处理。 例如,如果浏览器请求 URL 或 [http://machine:port/contexte/P],而页面 P 在应用程序上下文中不存在,则应用程序不会参与响应处理。此时由 Servlet 容器生成响应,并返回默认的错误页面。 其响应流 HTTP 的第一行包含 404 错误代码,表明所请求的页面 P 不存在。 可能需要生成符合应用程序视觉规范的响应,或提供解决问题的链接。此时,应使用 <error-page> 标签并包含 <error-code>404</error-code> 属性。

上文中的错误代码 HTTP 对应 500 错误,这是应用程序“崩溃”时返回的代码。如果异常上报至 Servlet 容器,则会返回此代码。 因此,第28至35行中的两个<error-page>标签很可能属于冗余。我们同时保留这两个标签,是为了演示处理错误的两种方式。

2.7.4.2. 异常模拟

通过链接 [Lancer une exception] 人工触发了一个异常:

点击链接 [Lancer une exception] [1] 将显示页面 [2]。

在页面 [formx.xhtml] 的代码中,链接 [Lancer une exception] 是按以下方式生成的:


<!-- 链接 -->
        <h:panelGrid columns="6">
          <h:commandLink value="1" action="form1"/>
...
          <h:commandLink value="#{msg['form.exceptionLink']}" action="#{form.throwException}"/>
        </h:panelGrid>

第 5 行显示,点击该链接时将执行方法 [form].throwException。该方法内容如下:


  public String throwException() throws java.lang.Exception{
    throw new Exception("Exception test");
}

该方法中抛出一个类型为 [java.lang.Exception] 的异常。该异常将向上传播至 Servlet 容器,随后容器将显示页面 [/faces/exception.xhtml]。

2.7.4.3. 与异常相关的信息

当异常上报至 Servlet 容器时,该容器会通过向错误页面传递异常信息来显示相应的错误页面。这些信息会被作为当前正在处理的请求的新属性进行设置。 浏览器的请求及其将收到的响应被封装在类型为 [HttpServletRequest request] 和 [HttpServletResponse response] 的 Java 对象中。在处理浏览器请求的各个阶段,这些对象均可供使用。

收到来自浏览器的请求 HTTP 后,Servlet 容器将其封装到 Java 对象 [HttpServletRequest request] 中,并创建对象 [HttpServletResponse response],该对象将用于生成响应。 在此对象中,特别包含用于响应流 HTTP 的通道 TCP-IP。所有参与处理对象 request 的层 t1、t2、...、tn 均可访问这两个对象。 每个处理层均可访问初始请求 request 的元素,并通过丰富 response 对象来准备响应。 例如,localisation层可以通过response.setLocale(Locale l)方法来确定响应的localisation

不同的 ti 层可以通过 request 对象传递信息。该对象拥有一个属性字典,创建时为空,可由后续处理层进行丰富。 各处理层可在 request 对象的属性中放入下一处理层所需的信息。管理 request 对象属性的方法有两种:

  • void setAttribute(String s, Object o),用于向属性中添加由字符串 s 标识的对象 o;
  • Object getAttribute(String s),用于获取由字符串 s 标识的属性 o。

当异常上报至 Servlet 容器时,该容器会将以下属性放入正在处理的请求中:

javax.servlet.error.status_code
将返回给客户端的错误代码 HTTP
javax.servlet.error.exception
异常的 Java 类型及错误信息。
javax.servlet.error.request_uri
发生异常时请求的 URL
javax.servlet.error.servlet_name
发生异常时正在处理该请求的Servlet

我们将利用这些请求属性在页面 [exception.xhtml] 中进行显示。

2.7.4.4. 错误页面 [exception.xhtml]

其内容如下:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <f:view locale="#{changeLocale.locale}">
    <h:head>
      <title>JSF</title>
      <h:outputStylesheet library="css" name="styles.css"/>
    </h:head>
    <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
      <h:form id="formulaire">
        <h3><h:outputText value="#{msg['exception.header']}"/></h3>
        <h:panelGrid columnClasses="col1,col2" columns="2" border="1">
          <h:outputText value="#{msg['exception.httpCode']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.status_code']}"/>
          <h:outputText value="#{msg['exception.message']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.exception']}"/>
          <h:outputText value="#{msg['exception.requestUri']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.request_uri']}"/>
          <h:outputText value="#{msg['exception.servletName']}"/>
          <h:outputText value="#{requestScope['javax.servlet.error.servlet_name']}"/>
        </h:panelGrid>
        <!-- 链接 -->
        <h:panelGrid columns="6">
          <h:commandLink value="1" action="form1"/>
          <h:commandLink value="2" action="#{form.doAction2}"/>
          <h:commandLink value="3" action="form3"/>
          <h:commandLink value="4" action="#{form.doAction4}"/>
          <h:commandLink value="#{msg['form.pagealeatoireLink']}" action="#{form.doAlea}"/>
        </h:panelGrid>
      </h:form>
    </h:body>
  </f:view>
</html>

2.7.4.4.1. 异常页面的表达式

在客户端请求的处理链中,页面 XHTML 通常是处理链的最后一个环节:

该链中的所有元素都是 Java 类,包括页面 XHTML。该页面实际上会被 Servlet 容器转换为 Servlet,c.a.d 则被转换为普通的 Java 类。 更准确地说,页面 XHTML 被转换为在以下方法中执行的 Java 代码:


public void _jspService(HttpServletRequest request, HttpServletResponse response)
throws java.io.IOException, ServletException {

JspFactory _jspxFactory = null;
PageContext pageContext = null;
HTTPSession session = null;
ServletContext application = null;
ServletConfig config = null;
JspWriter out = null;
Object page = this;
JspWriter _jspx_out = null;
PageContext _jspx_page_context = null;
... 
...code de la page XHTML

从第 14 行开始,可以看到页面 XHTML 的 Java 代码。该代码将拥有若干由方法 _jspService(即上文第 1 行)初始化的对象:

  • 第 1 行:HttpServletRequest request:正在处理的请求,
  • 第 1 行:HttpServletResponse response:即将发送给客户端的响应,
  • 第 7 行:ServletContext application:一个代表 Web 应用程序本身的对象。 与对象 request 一样,对象 application 可能具有属性。这些属性由所有客户端的所有请求共享。它们通常是只读属性,
  • 第 6 行:HTTPSession 会话:代表客户端的会话。 与对象 request application 一样,对象 session 可能具有属性。这些属性由同一客户端的所有请求共享,
  • 第 9 行:JspWriter out:一个写入客户端浏览器的数据流。该对象用于调试 XHTML 页面。通过 out.println(文本) 写入的所有内容都将显示在客户端浏览器中。

在页面 JSF 中,当写入 #{表达式}表达式可以是上述 requestsession application 对象中某个属性的。系统会依次在这三个对象中查找对应的属性。因此,#{键} 的求值过程如下:

  1. request.getAttribute(键)
  2. session.getAttribute(键)
  3. application.getAttribute(键)

一旦找到非 null 的值,#{键} 的求值即停止。若需更精确地指定属性搜索的上下文,可使用:

  • #{requestScope['clé']} 用于在 request 对象中查找该属性
  • #{sessionScope['clé']} 用于在 session 对象中查找该属性
  • #{applicationScope['clé']} 用于在 application 对象中查找该属性

这在第 116 页的 [exception.xhtml] 页面中已实现。使用的属性如下:

javax.servlet.error.status_code
请求
参见第 2.7.4.3 节。
javax.servlet.error.exception
同上
同上
javax.servlet.error.request_uri
同上
同上
javax.servlet.error.servlet_name
同上
同上

页面 JSF [exception.xhtml] 所需的各种消息已添加到现有的消息文件中:

[messages_fr.properties]


exception.header=L'exception suivante s'est produite
exception.httpCode=Code HTTP de l'erreur
exception.message=Message de l'exception
exception.requestUri=URL demandée lors de l'erreur
exception.servletName=Nom de la servlet demandée lorsque l'erreur s'est produite

[messages_en.properties]


exception.header=The following error occurred
exception.httpCode=HTTP error code
exception.message=Exception message
exception.requestUri=URL requested when error occurred
exception.servletName=Servlet requested when error occurred

2.8. 示例 mv-jsf2-06:输入数据的验证与转换

2.8.1. 应用程序

该应用程序提供了一个数据输入表单。表单提交后,系统将返回相同的表单作为响应,若输入内容存在错误,则会附带相应的错误提示信息。

2.8.2. NetBeans 项目

该应用程序的 NetBeans 项目如下:

[mv-jsf2-06] 项目再次基于单页 [index.html] [1] 及其模板 [Form.java] [2]。 该项目继续使用来自 [messages.properties] 的消息,但仅提供法语版本 [3]。不提供语言切换选项。

2.8.3. 应用程序环境

此处列出配置应用程序的文件内容,不作特别说明。这些文件有助于更好地理解下文。

[faces-config.xml]


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
</faces-config>

第 17 行是新增的。将在后续内容中进行说明。

消息文件 [messages_fr.properties]


form.titre=Jsf - validations et conversions
saisie1.prompt=1-Nombre entier de type int
saisie2.prompt=2-Nombre entier de type int
saisie3.prompt=3-Nombre entier de type int
data.required=Vous devez entrer une donn\u00e9e
integer.required=Vous devez entrer un nombre entier
saisie4.prompt=4-Nombre entier de type int dans l'intervalle [1,10]
saisie4.error=4-Vous devez entrer un nombre entier dans l'intervalle [1,10]
saisie5.prompt=5-Nombre r\u00e9el de type double
double.required=Vous devez entrer un nombre
saisie6.prompt=6-Nombre r\u00e9el>=0  de type double
saisie6.error=6-Vous devez entrer un nombre >=0
saisie7.prompt=7-Bool\u00e9en
saisie7.error=7-Vous devez entrer un bool\u00e9en
saisie8.prompt=8-Date au format jj/mm/aaaa
saisie8.error=8-Vous devez entrer une date valide au format jj/mm/aaaa
date.required=Vous devez entrer une date
saisie9.prompt=9-Cha\u00eene de 4 caract\u00e8res
saisie9.error=9-Vous devez entrer une cha\u00eene de 4 caract\u00e8res exactement
saisie9B.prompt=9B-Heure au format hh:mm
saisie9B.error=La cha\u00eene saisie ne respecte pas le format hh:mm
submit=Valider
cancel=Annuler
saisie.type=Type de la saisie
saisie.champ=Champ de saisie
saisie.erreur=Erreur de saisie
bean.valeur=Valeurs du mod\u00e8le du formulaire
saisie10.prompt=10-Nombre entier de type int <1 ou >7
saisie10.incorrecte=10-Saisie n\u00b0 10 incorrecte
saisie10.incorrecte_detail=10-Vous devez entrer un nombre entier <1 ou >7
saisies11et12.incorrectes=La propri\u00e9t\u00e9 saisie11+saisie12=10 n'est pas v\u00e9rifi\u00e9e
saisies11et12.incorrectes_detail=La propri\u00e9t\u00e9 saisie11+saisie12=10 n'est pas v\u00e9rifi\u00e9e
saisie11.prompt=11-Nombre entier de type int
saisie12.prompt=12-Nombre entier de type int
error.sign="!"
error.sign_detail="!"

样式表 [styles.css] 如下:


.info{
   font-family: Arial,Helvetica,sans-serif;
   font-size: 14px;
   font-weight: bold
}

.col1{
   background-color: #ccccff
}

.col2{
   background-color: #ffcccc
}

.col3{
   background-color: #ffcc66
}

.col4{
   background-color: #ccffcc
}

.error{
   color: #ff0000
}

.saisie{
   background-color: #ffcccc;
   border-color: #000000;
   border-width: 5px;
   color: #cc0033;
   font-family: cursive;
   font-size: 16px
}

.entete{
   font-family: 'Times New Roman',Times,serif;
   font-size: 14px;
   font-weight: bold
}

2.8.4. 页面 [index.xhtml] 及其模板 [Form.java]

页面 [index.xhtml] 如下所示:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <h:head>
    <title>JSF</title>
    <h:outputStylesheet library="css" name="styles.css"/>
  </h:head>
  <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
    <h2><h:outputText value="#{msg['form.titre']}"/></h2>
    <h:form id="formulaire">
      <h:messages globalOnly="true" />
      <h:panelGrid columns="4" columnClasses="col1,col2,col3,col4" border="1">
        <!-- 第 1 行 -->
        <h:outputText value="#{msg['saisie.type']}" styleClass="entete"/>
        <h:outputText value="#{msg['saisie.champ']}" styleClass="entete"/>
        <h:outputText value="#{msg['saisie.erreur']}" styleClass="entete"/>
        <h:outputText value="#{msg['bean.valeur']}" styleClass="entete"/>
        <!-- 第 2 行 -->
        <h:outputText value="#{msg['saisie1.prompt']}"/>
        <h:inputText id="saisie1" value="#{form.saisie1}" styleClass="saisie"/>
        <h:message for="saisie1" styleClass="error"/>
        <h:outputText value="#{form.saisie1}"/>
        <!-- 第3行 -->
        <h:outputText value="#{msg['saisie2.prompt']}" />
        <h:inputText id="saisie2" value="#{form.saisie2}"  styleClass="saisie"/>
        <h:message for="saisie2" showSummary="true" showDetail="false" styleClass="error"/>
        <h:outputText value="#{form.saisie2}"/>
        <!-- 第4行 -->
        <h:outputText value="#{msg['saisie3.prompt']}" />
        <h:inputText id="saisie3" value="#{form.saisie3}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['integer.required']}"/>
        <h:message for="saisie3" styleClass="error"/>
        <h:outputText value="#{form.saisie3}"/>
        <!-- 第 5 行 -->
        <h:outputText value="#{msg['saisie4.prompt']}" />
        <h:inputText id="saisie4" value="#{form.saisie4}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['integer.required']}" validatorMessage="#{msg['saisie4.error']}">
          <f:validateLongRange minimum="1" maximum="10" />
        </h:inputText>
        <h:message for="saisie4" styleClass="error"/>
        <h:outputText value="#{form.saisie4}"/>
        <!-- 第 6 行 -->
        ...
        <!-- 第 7 行 -->
        ...
        <!-- 第 8 行 -->
        ...
        <!-- 第 9 行 -->
        ...
        <!-- 第 10 行 -->
        ...
        <!-- 第 11 行 -->
        ...
        <!-- 第 12 行 -->
        ...
        <!-- 第 13 行 -->
        ...
      </h:panelGrid>
      <!-- 控制按钮 -->
      <h:panelGrid columns="2">
        <h:commandButton value="#{msg['submit']}" action="#{form.submit}"/>
        <h:commandButton value="#{msg['cancel']}" immediate="true" action="#{form.cancel}"/>
      </h:panelGrid>
    </h:form>
  </h:body>
</html>

主要的新功能在于引入了以下标签:

  • 用于显示错误消息的 <h:messages>(第 14 行)、<h:message>(第 24、29、34 行),
  • 这些对输入设置了有效性约束的 <f:validateLongRange>(第39行)、<f:validateDoubleRange>、<f:validateLength>、<f:validateRegex>
  • 这些定义了输入与其模板之间的转换器,例如 <f:convertDateTime>。

该页面的模板是以下 [Form.java] 类:


package forms;

import com.corejsf.util.Messages;
import java.util.Date;
import javax.enterprise.context.RequestScoped;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.ValidatorException;

@ManagedBean
@RequestScoped
public class Form {

public Form() {
}
// 输入
private Integer saisie1 = 0;
private Integer saisie2 = 0;
private Integer saisie3 = 0;
private Integer saisie4 = 0;
private Double saisie5 = 0.0;
private Double saisie6 = 0.0;
private Boolean saisie7 = true;
private Date saisie8 = new Date();
private String saisie9 = "";
private Integer saisie10 = 0;
private Integer saisie11 = 0;
private Integer saisie12 = 0;
private String errorSaisie11 = "";
private String errorSaisie12 = "";

// 操作
public String submit() {
...
}

public String cancel() {
...
}

// 验证器
public void validateSaisie10(FacesContext context, UIComponent component, Object value) {
...
}
// 获取器和设置器
...
}

此处的创新之处在于,模板字段不再仅限于 String 类型,而是包含多种类型。

2.8.5. 表单中的各项输入

现在我们将依次研究表单中的不同输入项。

2.8.5.1. 输入项 1 至 4:整数输入

页面 [index.xhtml] 以以下形式呈现输入项 1:


<!-- 第 2 行 -->
        <h:outputText value="#{msg['saisie1.prompt']}"/>
        <h:inputText id="saisie1" value="#{form.saisie1}" styleClass="saisie"/>
        <h:message for="saisie1" styleClass="error"/>
        <h:outputText value="#{form.saisie1}"/>

在 [Form.java] 中,模板 form.saisie1 的定义如下:


private Integer saisie1 = 0;

在浏览器的 GET 上,与模板 [Form.java] 关联的页面 [index.xhtml] 显示效果如下:

  • 第 2 行生成 [1],
  • 第 3 行生成 [2],
  • 第 4 行生成 [3],
  • 第 5 行生成 [4]。

假设输入并确认以下内容:

那么,在应用程序返回的表单中将得到以下结果:

  • 在 [1] 中,输入错误,
  • 在 [2] 中,显示了相应的错误提示,
  • 在 [3] 中,可以看到模型中 Integer 字段 saisie1 的值没有变化。

让我们解释一下发生了什么。为此,让我们回到JSF页面的处理循环:

我们针对以下组件分析该处理循环:


<h:inputText id="saisie1" value="#{form.saisie1}" styleClass="saisie"/>

及其模型:


private Integer saisie1 = 0;
  • 在 [A] 中,浏览器在 GET 时发送的页面 [index.xhtml] 被恢复。 在 [A] 时,页面状态与用户接收到的完全一致。组件 id="saisie1" 恢复其初始值“0”,
  • 在 [B] 阶段,页面组件接收浏览器提交的值。在 [B] 阶段,页面呈现为用户输入并提交后的状态。 组件 id="saisie1" 接收的值是提交的值“x”,
  • 在 [C] 中,如果页面包含显式验证器和转换器,则会执行这些操作。 如果与组件关联的字段类型不是 String,则隐式转换器也会被执行。本例中即属此情况,字段 form.saisie1 的类型为 Integer。 JSF 将尝试将组件 id="saisie1" 的值“x”转换为类型 Integer。这将引发错误,导致处理循环 [A-F] 终止。 该错误将与组件 id="saisie1" 相关联。通过 [D2],系统随后直接进入响应渲染阶段。同一页面 [index.xhtml] 被返回,
  • 只有当页面中的所有组件都通过了转换/验证阶段后,[D]阶段才会执行。在此阶段,id="saisie1"组件的值将被赋给其模板form.saisie1

如果 [C] 阶段失败,页面将重新显示,并再次执行以下代码:


<!-- 第 2 行 -->
        <h:outputText value="#{msg['saisie1.prompt']}"/>
        <h:inputText id="saisie1" value="#{form.saisie1}" styleClass="saisie"/>
        <h:message for="saisie1" styleClass="error"/>
<h:outputText value="#{form.saisie1}"/>

在 [2] 中显示的消息来自 [index.xhtml] 的第 4 行。 标签 <h:message for="idComposant"/> 会在出现错误时显示与 for 属性指定的组件相关的错误消息。 [2]中显示的消息是标准消息,位于[jsf-api.jar]归档中的[javax/faces/Messages.properties]文件中:

在 [2] 中,可以看到该消息文件存在多种变体。让我们查看 [Messages_fr.properties] 的内容:

...
# ==============================================================================
# 组件错误
# ==============================================================================
javax.faces.component.UIInput.CONVERSION={0} : une erreur de conversion est survenue.
javax.faces.component.UIInput.REQUIRED={0} : erreur de validation. Vous devez indiquer une valeur.
javax.faces.component.UIInput.UPDATE={0} : une erreur est survenue lors du traitement des informations que vous avez soumises. 
javax.faces.component.UISelectOne.INVALID={0} : erreur de validation. La valeur est incorrecte.
javax.faces.component.UISelectMany.INVALID={0} : erreur de validation. La valeur est incorrecte.

# ==============================================================================
# 转换器错误
# ==============================================================================
...
javax.faces.converter.FloatConverter.FLOAT={2} : «{0 doit être un nombre composé dun ou de plusieurs chiffres.
javax.faces.converter.FloatConverter.FLOAT_detail={2} : «{0 doit être un nombre compris entre 1.4E-45 et 3.4028235E38. Exemple : {1}
javax.faces.converter.IntegerConverter.INTEGER={2} : «{0 doit être un nombre composé dun ou de plusieurs chiffres.
javax.faces.converter.IntegerConverter.INTEGER_detail={2} : «{0 doit être un nombre compris entre -2147483648 et 2147483647. Exemple : {1}
...


# ==============================================================================
# 验证器错误
# ==============================================================================
javax.faces.validator.DoubleRangeValidator.MAXIMUM={1} : erreur de validation. La valeur est supérieure à la valeur maximale autorisée, "{0}".
javax.faces.validator.DoubleRangeValidator.MINIMUM={1} : erreur de validation. La valeur est inférieure à la valeur minimale autorisée, "{0}".
javax.faces.validator.DoubleRangeValidator.NOT_IN_RANGE={2} : erreur de validation. Lattribut spécifié nest pas compris entre les valeurs attendues {0} et {1}.
javax.faces.validator.DoubleRangeValidator.TYPE={0} : erreur de validation. La valeur nest pas du type correct.
...

该文件中的消息按类别划分:

  • 组件错误,第 3 行,
  • 组件与其模型之间的转换错误,第 12 行
  • 页面中存在验证器时的验证错误,第 23 行。

组件 id="saisie1" 发生的错误属于将类型 String 转换为类型 Integer 时的转换错误。相关的错误消息位于消息文件的第 18 行。

javax.faces.converter.IntegerConverter.INTEGER_detail={2} : «{0}» doit être un nombre compris entre -2147483648 et 2147483647. Exemple : {1}

显示的错误消息如下:

从该消息中可以看出:

  • 参数 {2} 已被发生转换错误的组件标识符所替换,
  • 参数 {0} 已被替换为在 [1] 中为该组件输入的值,
  • 参数 {1} 已被数字 9346 替换。

大多数与组件相关的消息都有两个版本:摘要版(summary)和详细版(detail)。第16-18行就是这种情况:

javax.faces.converter.IntegerConverter.INTEGER={2} : «{0}» doit être un nombre composé d’un ou de plusieurs chiffres.
javax.faces.converter.IntegerConverter.INTEGER_detail={2} : «{0}» doit être un nombre compris entre -2147483648 et 2147483647. Exemple : {1}

键值为 _detail 的消息(第 2 行)即所谓的详细消息。另一条则是所谓的摘要消息。 <h:message>标签默认显示详细消息。可以通过showSummary和showDetail属性更改此行为。对于id为saisie2的组件,就是这样设置的


        <!-- 第 3 行 -->
        <h:outputText value="#{msg['saisie2.prompt']}" />
        <h:inputText id="saisie2" value="#{form.saisie2}"  styleClass="saisie"/>
        <h:message for="saisie2" showSummary="true" showDetail="false" styleClass="error"/>
<h:outputText value="#{form.saisie2}"/>

第 2 行,组件 saisie2 与后续字段 form.saisie2 相关联:


  private Integer saisie2 = 0;

最终结果如下:

  • 其中 [1] 表示详细消息,[2] 表示摘要消息。

<h:messages> 标签以列表形式显示所有组件的摘要错误消息以及与组件无关的错误消息。同样,属性可以更改此默认行为:

  • showDetail:true / false,用于控制是否显示详细消息;
  • showSummary:true / false,用于控制是否显示摘要消息,
  • globalOnly:true / false,用于控制是否仅显示与组件无关的错误消息。此类消息可能由开发人员创建。

与转换相关的错误消息可以通过多种方式进行修改。首先,可以指示应用程序使用另一个消息文件。此修改在 [faces-config.xml] 中进行:


<faces-config ...">
  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
...
</faces-config>

第 3-8 行定义了一个消息文件,但 <h:message><h:messages> 标签并非使用该文件。必须使用第 9 行的 <message-bundle> 标签来定义它。 第 9 行指示 <h:message(s)> 标签在处理 [javax.faces.Messages.properties] 文件之前,应先遍历 [messages.properties] 文件。因此,若在 [messages_fr.properties] 文件中添加以下行:


# 转换
javax.faces.converter.IntegerConverter.INTEGER=erreur
javax.faces.converter.IntegerConverter.INTEGER_detail=erreur d\u00e9taill\u00e9e

则针对组件 saisie1saisie2 返回的错误信息将变为:

Image

修改转换错误消息的另一种方法是,如下所示,对组件 saisie3 使用该组件的 converterMessage 属性:


        <!-- 第 4 行 -->
        <h:outputText value="#{msg['saisie3.prompt']}" />
        <h:inputText id="saisie3" value="#{form.saisie3}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['integer.required']}"/>
        <h:message for="saisie3" styleClass="error"/>
<h:outputText value="#{form.saisie3}"/>

组件 saisie3 与以下字段 form.saisie3 相关联:


  private Integer saisie3 = 0;
  • 第 3 行,converterMessage 属性明确指定了转换错误时显示的消息,
  • 第 3 行,attribute required="true" 表示该字段为必填项。该字段不能为空。如果字段不包含任何字符或仅包含一串空格,则被视为空字段。同样,在 [javax.faces.Messages.properties] 中也存在一条默认消息:
javax.faces.component.UIInput.REQUIRED={0} : erreur de validation. Vous devez indiquer une valeur.

通过 requiredMessage 属性可替换此默认消息。如果 [messages.properties] 文件包含以下消息:


...
data.required=Vous devez entrer une donnée
integer.required=Vous devez entrer un nombre entier

则可获得以下结果:

或者如下结果:

仅验证输入是否为整数往往不够。有时还需要验证输入的数值是否属于给定区间。此时便需要使用验证器。输入项4便是一个例子。其在[index.xhtml]中的代码如下:


        <!-- 第 5 行 -->
        <h:outputText value="#{msg['saisie4.prompt']}" />
        <h:inputText id="saisie4" value="#{form.saisie4}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['integer.required']}" validatorMessage="#{msg['saisie4.error']}">
          <f:validateLongRange minimum="1" maximum="10" />
        </h:inputText>
        <h:message for="saisie4" styleClass="error"/>
<h:outputText value="#{form.saisie4}"/>

第 3 行,组件 saisie4 关联到以下模板 form.saisie4


  private Integer saisie4 = 0;

第 3-5 行,标签 <h:inputText> 有一个子标签 <f:validateLongRange>,该子标签支持两个可选属性:minimummaximum。 该标签(也称为验证器)可对输入值添加约束:不仅必须是整数,而且如果同时存在 minimummaximum 这两个属性,则必须是 [minimum, maximum] 区间内的整数; 若仅存在 minimum 属性,则值必须大于等于 minimum;若仅存在 maximum 属性,则值必须小于等于 maximum。 验证器 <f:validateLongRange> 在 [javax.faces.Messages.properties] 中具有默认错误消息:

1
2
3
javax.faces.validator.LongRangeValidator.MINIMUM={1} : erreur de validation. La valeur est inférieure à la valeur minimale autorisée, "{0}".
javax.faces.validator.LongRangeValidator.NOT_IN_RANGE={2} : erreur de validation. L’attribut spécifié n’est pas compris entre les valeurs attendues {0} et {1}.
javax.faces.validator.LongRangeValidator.TYPE={0} : erreur de validation. La valeur n’est pas du type correct.

同样,这些消息可以被其他消息替换。有一个 validatorMessage 属性,可用于为该组件定义特定的消息。因此,使用以下 JSF 代码:


        <h:inputText id="saisie4" value="#{form.saisie4}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['integer.required']}" validatorMessage="#{msg['saisie4.error']}">
          <f:validateLongRange minimum="1" maximum="10" />
</h:inputText>

并在 [messages.properties] 中设置以下消息:


saisie4.error=4-Vous devez entrer un nombre entier dans l'intervalle [1,10]

将得到以下结果:

Image

2.8.5.2. 输入 5 和 6:实数输入

实数的输入规则与整数的输入规则类似。输入5和6的代码XHTML如下:


<!-- 第 6 行 -->
        <h:outputText value="#{msg['saisie5.prompt']}" />
        <h:inputText id="saisie5" value="#{form.saisie5}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['double.required']}"/>
        <h:message for="saisie5" styleClass="error"/>
        <h:outputText value="#{form.saisie5}"/>
        <!-- 第 7 行 -->
        <h:outputText value="#{msg['saisie6.prompt']}"/>
        <h:inputText id="saisie6" value="#{form.saisie6}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['double.required']}" validatorMessage="#{msg['saisie6.error']}">
          <f:validateDoubleRange minimum="0.0"/>
        </h:inputText>
        <h:message for="saisie6" styleClass="error"/>
        <h:outputText value="#{form.saisie6}"/>

与组件 saisie5saisie6 相关的 [Form.java] 模板元素:


  private Double saisie5 = 0.0;
  private Double saisie6 = 0.0;

[messages.properties] 中与组件 saisie5saisie6, 的转换器和验证器相关的错误消息:


double.required=Vous devez entrer un nombre
saisie6.error=6-Vous devez entrer un nombre >=0

以下是一个执行示例:

Image

2.8.5.3. 输入 7:布尔值的输入

布尔值的输入通常应通过复选框实现。若使用输入字段进行输入,字符串“true”将转换为布尔值true,其他字符串则转换为布尔值false

示例中的代码 XHTML:


<!-- 第 8 行 -->
        <h:outputText value="#{msg['saisie7.prompt']}"/>
        <h:inputText id="saisie7" value="#{form.saisie7}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['double.required']}"/>
        <h:message for="saisie7" styleClass="error"/>
        <h:outputText value="#{form.saisie7}"/>

组件 saisie7 的模板:


  private Boolean saisie7 = true;

以下是一个输入示例及其响应:

在 [1] 中,输入的值。经过转换,该字符串“x”变为布尔值 false。这正是 [2] 所显示的内容。 表单中的 [3] 值并未改变。该值仅在页面所有转换和验证均成功后才会改变。本示例中并非如此。

2.8.5.4. 输入 8:日期输入

本例中,日期输入通过以下代码 XHTML 实现:


<!-- 第 9 行 -->
        <h:outputText value="#{msg['saisie8.prompt']}"/>
        <h:inputText id="saisie8" value="#{form.saisie8}"  styleClass="saisie" required="true" requiredMessage="#{msg['date.required']}" converterMessage="#{msg['saisie8.error']}">
          <f:convertDateTime pattern="dd/MM/yyyy"/>
        </h:inputText>
        <h:message for="saisie8" styleClass="error"/>
        <h:outputText value="#{form.saisie8}">
          <f:convertDateTime pattern="dd/MM/yyyy"/>
        </h:outputText>

第 3 行中的组件 saisie8 使用转换器 java.lang.String <--> java.util.Date。 与组件 saisie8 关联的模板 form.saisie8 如下:


  private Date saisie8 = new Date();

第7至9行定义的组件同样使用了一个转换器,但仅支持java.util.Datejava.lang.String这一方向的转换。

转换器 <f:convertDateTime> 支持多种属性,其中包括 pattern 属性,该属性用于确定待转换为日期的字符串格式,或指定日期应以何种格式显示。

在首次请求页面 [index.xhtml] 时,上述第 8 行显示如下:

字段 [1] 和 [2] 均显示模板 form.saisie8 的值:


  private Date saisie8 = new Date();

其中 saisie8 的值取当日日期。这两种情况下用于显示日期的转换器如下:


            <f:convertDateTime pattern="dd/MM/yyyy"/>

其中 dd(day)表示日期编号,MM(Month)表示月份编号,yyyy(year)表示年份。 在 [1] 中,该转换器用于进行反向转换 java.lang.String --> java.util.Date。 因此,输入的日期必须遵循“dd/MM/yyyy”的格式才有效。

[javax.faces.Messages.properties]中针对无效日期设有默认提示信息:

javax.faces.converter.DateTimeConverter.DATE={2} : «{0}» n’a pas pu être interprété en tant que date.
javax.faces.converter.DateTimeConverter.DATE_detail={2} : «{0}» n’a pas pu être interprété en tant que date. Exemple : {1} 

这些提示信息可以替换为自定义的提示信息。例如:


<h:inputText id="saisie8" value="#{form.saisie8}"  styleClass="saisie" required="true" requiredMessage="#{msg['date.required']}" converterMessage="#{msg['saisie8.error']}">
  <f:convertDateTime pattern="dd/MM/yyyy"/>
</h:inputText>

若发生转换错误,系统将显示以下键值 saisie8.error 对应的提示信息:


saisie8.error=8-Vous devez entrer une date valide au format jj/mm/aaaa

以下是一个示例:

Image

2.8.5.5. 输入 9:输入长度受限的字符串

示例 9 演示了如何强制输入的字符串字符数必须在指定范围内:


<!-- 第 10 行 -->
        <h:outputText value="#{msg['saisie9.prompt']}"/>
        <h:inputText id="saisie9" value="#{form.saisie9}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" validatorMessage="#{msg['saisie9.error']}">
          <f:validateLength minimum="4" maximum="4"/>
        </h:inputText>
        <h:message for="saisie9" styleClass="error"/>
        <h:outputText value="#{form.saisie9}"/>

第 4 行,验证器 <f:validateLength minimum="4" maximum="4"/> 要求输入的字符串必须恰好包含 4 个字符。可以只使用其中一个属性:minimum 用于指定最小字符数,maximum 用于指定最大字符数。

第 3 行组件 saisie9 的模板 form.saisie9 如下:


  private String saisie9 = "";

此类验证存在默认错误信息:

javax.faces.validator.LengthValidator.MAXIMUM={1} : erreur de validation. La longueur est supérieure à la valeur maximale autorisée, "{0}".
javax.faces.validator.LengthValidator.MINIMUM={1} : erreur de validation. La longueur est inférieure à la valeur minimale autorisée, "{0}".

可通过使用 validatorMessage 属性进行替换,如上文第 3 行所示。键 saisie9.error 的消息如下:


saisie9.error=9-Vous devez entrer une chaîne de 4 caractères exactement

以下是一个执行示例:

Image

2.8.5.6. 输入 9B:输入必须符合特定格式的字符串

输入 9B 演示了如何强制输入的字符串字符数必须在指定范围内:


<!-- 行 10B -->
        <h:outputText value="#{msg['saisie9B.prompt']}"/>
        <h:inputText id="saisie9B" value="#{form.saisie9B}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" validatorMessage="#{msg['saisie9B.error']}">
          <f:validateRegex pattern="^\s*\d{2}:\d{2}\s*$"/>
        </h:inputText>
        <h:message for="saisie9B" styleClass="error"/>
        <h:outputText value="#{form.saisie9B}"/>

第 4 行,验证器 <f:validateRegex pattern="^\s*\d{2}:\d{2}\s*$"/> 要求输入的字符串必须符合正则表达式模式,即:0 个或多个空格、2 个数字、冒号 (:)、 2个数字,以及0个或多个空格。

第 3 行组件 saisie9B 的模式 form.saisie9B 如下:


private String saisie9B;

此类验证存在默认错误信息:

1
2
3
4
5
6
javax.faces.validator.RegexValidator.PATTERN_NOT_SET=Le modèle d’expression régulière doit être défini.
javax.faces.validator.RegexValidator.PATTERN_NOT_SET_detail=La valeur définie du modèle d’expression régulière ne peut pas être vide.
javax.faces.validator.RegexValidator.NOT_MATCHED=Discordance du modèle d’expression régulière.
javax.faces.validator.RegexValidator.NOT_MATCHED_detail=Discordance du modèle d’expression régulière «{0}».
javax.faces.validator.RegexValidator.MATCH_EXCEPTION=Erreur dans l’expression régulière.
javax.faces.validator.RegexValidator.MATCH_EXCEPTION_detail=Erreur dans l’expression régulière,  «{0}»

可通过使用 validatorMessage 属性进行替换,如上文第 3 行所示。键 saisie9.error 的消息如下:


saisie9B.error=La cha\u00eene saisie ne respecte pas le format hh:mm

以下是一个执行示例:

Image

2.8.5.7. 练习 10:编写一个特定的验证方法

总结一下:JSF 允许对输入的值进行验证,包括数字(整数、实数)的有效性、日期、字符串长度,以及输入内容是否符合正则表达式。 JSF 允许在现有验证器和转换器的基础上,添加自定义的验证器和转换器。本文不对此进行详细讨论,但可参阅 [ref2] 以深入了解。

本文将介绍另一种方法:即通过表单模型的方法对输入数据进行验证。示例如下:


<!-- 第 11 行 -->
        <h:outputText value="#{msg['saisie10.prompt']}"/>
        <h:inputText id="saisie10" value="#{form.saisie10}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" validator="#{form.validateSaisie10}"/>
        <h:message for="saisie10" styleClass="error"/>
        <h:outputText value="#{form.saisie10}"/>

与第 3 行组件 saisie10 关联的 form.saisie10 模型如下:


  private Integer saisie10 = 0;

我们希望输入的数字小于 1 或大于 7。无法使用 JSF 的基本验证器进行验证。因此,我们需要为组件 saisie10 编写自定义验证方法。通过待验证组件的 validator 属性指定该方法:


          <h:inputText id="saisie10" value="#{form.saisie10}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" validator="#{form.validateSaisie10}"/>

组件 saisie10 由方法 form.validateSaisie10 进行验证。该方法如下:


  public void validateSaisie10(FacesContext context, UIComponent component, Object value) {
    int saisie = (Integer) value;
    if (!(saisie < 1 || saisie > 7)) {
      FacesMessage message = Messages.getMessage(null, "saisie10.incorrecte", null);
      message.setSeverity(FacesMessage.SEVERITY_ERROR);
      throw new ValidatorException(message);
    }
}

验证方法的签名必须与第 1 行一致:

  • FacesContext context:页面的执行上下文——可访问各种信息,特别是 HttpServletRequest request HttpServletResponse response 对象,
  • UIComponent component:需要验证的组件。 <h:inputText> 标签由一个从 UIComponent 派生的 UIInput 类型组件表示。在此,该 UIInput 组件作为第二个参数被接收,
  • 对象值:待验证的输入值,已转换为模型类型。需注意,若字符串到模型类型的转换失败,则验证方法不会被执行。当进入 validateSaisie10 方法时,说明字符串到整数的转换已成功。 因此,第三个参数的类型为 Integer
  • 第 2 行:将输入的值转换为 int, 类型
  • 第3行:检查输入值是否小于1或大于7。如果是,则验证结束。如果不是,验证器应通过抛出类型为ValidatorException的异常来报告错误。

ValidatorException 有两个构造函数:

  • 构造函数 [1] 的参数为类型为 FacesMessage 的错误消息。此类型的消息由标签 <h:messages> 和 <h:message> 显示,
  • 构造函数 [2] 还允许封装类型为 Throwable 或该错误派生的原因。

我们需要构建一个类型为 FacesMessage 的消息。该类具有多种构造函数:

构造函数 [1] 定义了 FacesMessage 对象的属性:

  • FacesMessage.Severity severity:从以下枚举中选取的严重性级别:SEVERITY_ERROR、SEVERITY_FATAL、 SEVERITY_INFO、SEVERITY_WARN、
  • 摘要字符串:错误消息的摘要版本——由标签 <h:message showSummary="true"> 和 <h:messages> 显示,
  • 详细信息字符串:错误消息的详细版本——由 <h:message> 和 <h:messages showDetail="true"> 标签显示。

可以使用任意构造函数,缺失的参数可通过 set 方法在后续处理中进行设置。

[1] 构建器无法指定国际化消息文件中的特定消息。这显然很遗憾。 David GearyCay Horstmann 在其著作《Core JavaServer Faces》中,通过 com.corejsf.util.Messages 实用类弥补了这一缺陷。 Java代码第4行正是利用该类来生成错误消息的。该类仅包含静态方法,其中包括第4行所调用的getMessage方法


   public static FacesMessage getMessage(String bundleName, String resourceId, Object[] params)

方法 getMessage 接受三个参数:

  • String bundleName消息文件的名称,不包含后缀 .properties,但包含其包名。 在此,我们的第一个参数可以是 messages,用于指定文件 [messages.properties]。 在使用第一个参数指定的文件之前,getMessage 会尝试使用应用程序的消息文件(如果存在的话)。因此,如果在 [faces-config.xml] 中声明了一个带有以下标签的消息文件:

  <application>
...
    <message-bundle>messages</message-bundle>
</application>

则可以将 null 作为第一个参数传递给方法 getMessage。此处正是如此操作(参见 [web.xm],第 120 页),

  • 字符串 resourceId:用于在消息文件中定位待处理消息的键。我们已知一条消息可能同时存在摘要版和详细版。resourceId 是摘要版的标识符。 系统将自动使用键值 resourceId_detail 查找详细版本。因此,针对第 10 条输入错误,[messages.properties] 中将包含两条消息:

saisie10.incorrecte=10-Saisie  10 incorrecte
saisie10.incorrecte_detail=10-Vous devez entrer un nombre entier <1 ou >7

由方法 Messages.getMessage 生成的 FacesMessage 类型消息将同时包含摘要版本和详细版本(如果已找到)。这两个版本必须同时存在,否则将抛出 [NullPointerException] 类型的异常,

  • Object[] params:若消息具有形式参数 {0}, {1}, ...,则此处为消息的实际参数。这些形式参数将被数组 params 中的元素替换。

让我们回到组件 saisie10 验证方法的代码:


  public void validateSaisie10(FacesContext context, UIComponent component, Object value) {
    int saisie = (Integer) value;
    if (!(saisie < 1 || saisie > 7)) {
      FacesMessage message = Messages.getMessage(null, "saisie10.incorrecte", null);
      message.setSeverity(FacesMessage.SEVERITY_ERROR);
      throw new ValidatorException(message);
    }
}
  • 在 [4] 中,通过静态方法 Messages.getMessage 创建了类型为 FacesMessage 的消息
  • 在 [5] 中,设置消息的严重性级别,
  • 在 [6] 中,使用之前构建的消息抛出类型为 ValidatorException 的异常。验证方法是由以下代码 XHTML 调用的:

<!-- 第11行 -->
        <h:outputText value="#{msg['saisie10.prompt']}"/>
        <h:inputText id="saisie10" value="#{form.saisie10}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" validator="#{form.validateSaisie10}"/>
        <h:message for="saisie10" styleClass="error"/>
<h:outputText value="#{form.saisie10}"/>

第 3 行,针对 ID 为 saisie10 的组件执行了验证方法。因此,由方法 validateSaisie10 生成的错误消息与该组件相关联,并由第 4 行 (属性 for="saisie10")显示。标签 <h:message> 默认显示的是详细版本。

以下是一个执行示例:

Image

2.8.5.8. 输入 11 和 12:组件组的验证

到目前为止,我们遇到的验证方法仅对单个组件进行验证。如果需要对多个组件进行验证,该如何操作?接下来我们将探讨这一问题。在表单中:

Image

我们希望第11和12项输入均为两个整数,且其和等于10。

代码 JSF 将如下所示:


<!-- 第 12 行 -->
        <h:outputText value="#{msg['saisie11.prompt']}"/>
        <h:inputText id="saisie11" value="#{form.saisie11}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['integer.required']}"/>
        <h:panelGroup>
          <h:message for="saisie11" styleClass="error"/>
          <h:outputText value="#{form.errorSaisie11}" styleClass="error"/>
        </h:panelGroup>
        <h:outputText value="#{form.saisie11}"/>
        <!-- 第 13 行 -->
        <h:outputText value="#{msg['saisie12.prompt']}"/>
        <h:inputText id="saisie12" value="#{form.saisie12}" styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['integer.required']}"/>
        <h:panelGroup>
          <h:message for="saisie12" styleClass="error"/>
          <h:outputText value="#{form.errorSaisie12}" styleClass="error"/>
        </h:panelGroup>
        <h:outputText value="#{form.saisie12}"/>

以及相关的模板:


  private Integer saisie11 = 0;
  private Integer saisie12 = 0;
  private String errorSaisie11 = "";
private String errorSaisie12 = "";

在代码 JSF 的第 3 行,我们使用之前介绍过的技术来验证组件 saisie11 的输入值确实是一个整数。同样,在第 11 行,对于组件 saisie12 也是如此。 为了验证 saisie11 + saisie12 = 10,我们可以构建一个专门的验证器。这是首选的解决方案。再次阅读 [ref2] 即可了解详情。但在此我们采用另一种方法。

页面 [index.xhtml] 通过按钮 [Valider] 进行验证,该按钮的代码 JSF 如下:


<!-- 控制按钮 -->
      <h:panelGrid columns="2">
        <h:commandButton value="#{msg['submit']}" action="#{form.submit}"/>
        ...
      </h:panelGrid>

其中消息 msg['submit'] 内容如下:


submit=Valider

从第 3 行可以看出,将执行方法 form.submit 来处理对按钮 [Valider] 的点击。该方法如下:


  // 操作
  public String submit() {
    // 最近的提交
    validateForm();
    // 返回相同的表单
    return null;
  }

  // 全局验证
  private void validateForm() {
    if ((saisie11 + saisie12) != 10) {
...
}

需要重点理解的是,当 submit 方法执行时:

  • 表单中的所有验证器和转换器均已执行且成功,
  • [Form.java] 模型的字段已接收客户端提交的值。

实际上,让我们回顾一下 POST JSF 的处理流程:

方法 submit 是一个事件处理程序。它处理 [Valider] 按钮上的 clic 事件。 与所有事件处理程序一样,该方法在 [E] 阶段执行,此时所有验证器和转换器已执行并成功([C]),且模型已使用提交的值更新完毕([D])。 因此,这里不再像之前那样抛出 [ValidatorException] 类型的异常。我们只需将表单连同错误消息一并返回:

在 [1] 中,我们将向用户发出提示;而在 [2] 和 [3] 中,我们将添加错误标记。 在代码 JSF 中,将通过以下方式获取消息 [1]:


<h:form id="formulaire">
      <h:messages globalOnly="true" />
      <h:panelGrid columns="4" columnClasses="col1,col2,col3,col4" border="1">
        <!-- 第 1 行 -->
        ...

在第 2 行,<h:messages> 标签默认显示表单中所有组件输入错误的错误消息摘要,以及所有与组件无关的错误消息。globalOnly="true" 属性将显示范围限制为后者。

[2] 和 [3] 消息使用简单的 <h:outputText> 标签显示:


<!-- 第 12 行 -->
        <h:outputText value="#{msg['saisie11.prompt']}"/>
        <h:inputText id="saisie11" value="#{form.saisie11}"  styleClass="saisie" required="true" requiredMessage="#{msg['data.required']}" converterMessage="#{msg['integer.required']}"/>
        <h:panelGroup>
          <h:message for="saisie11" styleClass="error"/>
          <h:outputText value="#{form.errorSaisie11}" styleClass="error"/>
        </h:panelGroup>
        <h:outputText value="#{form.saisie11}"/>
        <!-- 第 13 行 -->
        ...
          <h:outputText value="#{form.errorSaisie12}" styleClass="error"/>
        ...

第4-7行,saisie11组件可能出现两种错误消息:

  • 一个是提示转换错误或数据缺失。该消息由 JSF 自身生成,将包含在 FacesMessage 类型中,并通过第 5 行的 <h:message> 标签显示;
  • 另一条错误信息将在输入11 + 输入12 不等于 10 时生成,由第 6 行显示。该错误信息将包含在 form.errorSaisie11 模板中。

这两条消息对应的是不可能同时发生的错误。对“输入11 + 输入12 = 10”的验证是在方法 submit 中进行的,该方法仅在表单中不存在任何错误时才会执行。 当该方法执行时,组件 saisie11 已完成验证,其模板 form.saise11 也已获得相应值。 第 5 行的消息将无法显示。反之,如果第 5 行的消息被显示,则表示表单中至少还存在一个错误,方法 submit 将不会执行。 第6行的消息将不会显示。为了使这两条可能出现的错误消息位于表格的同一列中,它们被合并到了一个<h:panelGroup>标签中(第4行和第7行)。

方法 submit 如下:


  // 操作
  public String submit() {
    // 最近的验证
    validateForm();
    // 返回相同的表单
    return null;
  }

  // 全局验证
  private void validateForm() {
    if ((saisie11 + saisie12) != 10) {
      // 全局消息
      FacesMessage message = Messages.getMessage(null, "saisies11et12.incorrectes", null);
      message.setSeverity(FacesMessage.SEVERITY_ERROR);
      FacesContext context = FacesContext.getCurrentInstance();
      context.addMessage(null, message);
      // 字段相关消息
      message = Messages.getMessage(null, "error.sign", null);
      setErrorSaisie11(message.getSummary());
      setErrorSaisie12(message.getSummary());
    } else {
      setErrorSaisie11("");
      setErrorSaisie12("");
    }
}
  • 第 4 行:方法 submit 调用方法 validateForm 进行最终验证,
  • 第11行:检查saisie11+saisie12是否等于10,
  • 若不为10,则在第13-14行,使用ID为saisies11et12.incorrectes的消息创建类型为FacesMessage的消息。该消息如下:

saisies11et12.incorrectes=La propriété saisie11+saisie12=10 n'est pas vérifiée
  • 构建好的消息(第15-16行)被添加到应用程序的错误消息列表中。该消息未与特定组件相关联,而是应用程序的全局消息。它将通过上文提到的 <h:messages globalOnly="true"/> 标签显示,
  • 第18行:创建一个ID为error.sign的新消息,类型为FacesMessage。该消息内容如下:

error.sign="!"

我们曾提到,静态方法 [Messages.getMessage] 会构建一个类型为 FacesMessage 的消息,其中包含摘要版本和详细版本(如果存在的话)。在此情况下,仅存在消息 error.sign 的摘要版本。 通过 m.getSummary() 函数,可获取消息 m 的摘要版本。 第19行和第20行,消息error.sign的摘要版本被放入模板的errorSaisie11errorSaisie12字段中。 它们将通过以下 JSF 标签显示:


          <h:outputText value="#{form.saisie11}"/>
          ...
          <h:outputText value="#{form.saisie12}"/>
  • 第 22-23 行:如果 saisie11+saisie12=10 属性被验证,则模板中的 errorSaisie11 errorSaisie12 这两个字段将被清空,以便清除任何先前的错误消息。 需注意,该表单会在不同请求之间保存在客户端会话中。

以下是一个执行示例:

在 [1] 列中可以看到,表单已接收提交的值,这表明提交值与表单之间的所有验证和转换操作均已成功。 因此,负责处理 [Valider] 按钮点击事件的事件处理程序 form.submit 得以执行。 正是该事件管理器生成了 [2] 和 [3] 中显示的消息。可以看出,即使表单被拒绝并退回给客户端,表单仍被更新了。 在这种情况下,我们可能希望模板不要被更新。事实上,假设用户通过按钮 [Annuler] 或 [4] 取消了更新,除非事先保存了模板,否则将无法恢复到初始模板。

2.8.5.9. POST:未进行数据验证的表单

考虑上文所述表单,假设用户未能察觉输入错误,希望放弃表单填写。此时,用户将使用由以下代码 JSF 生成的按钮 [Annuler]:


<!-- 命令按钮 -->
      <h:panelGrid columns="2">
        <h:commandButton value="#{msg['submit']}" action="#{form.submit}"/>
        <h:commandButton value="#{msg['cancel']}" immediate="true" action="#{form.cancel}"/>
      </h:panelGrid>

第 4 行,消息 msg['cancel'] 内容如下:


cancel=Annuler

只有当表单有效时,与按钮 [Annuler] 关联的方法 form.cancel 才会被执行。这正是我们针对与按钮 [Valider] 关联的方法 form.submit 所展示的内容。 如果用户希望取消表单输入,那么当然无需验证其输入内容的有效性。 通过 immediate="true" 属性可实现此效果,该属性指示 JSF 跳过验证和转换阶段,直接执行 form.cancel 方法。 让我们回到 POST 到 JSF 的处理流程:

具有 immediate="true" 属性的 <h:commandButton> 和 <h:commandLink> 组件的事件(其 immediate 属性为 "true")在 [C] 阶段进行处理,随后 JSF 循环直接进入 [E] 响应渲染阶段。

方法 form.cancel 如下:


  public String cancel() {
    saisie1 = 0;
    saisie2 = 0;
    saisie3 = 0;
    saisie4 = 0;
    saisie5 = 0.0;
    saisie6 = 0.0;
    saisie7 = true;
    saisie8 = new Date();
    saisie9 = "";
    saisie10 = 0;
    return null;
}

若在前一个表单中使用按钮 [Annuler],将返回以下页面:

  • 此时会再次显示表单,因为事件处理程序 form.cancel 返回了导航键 null。因此页面 [index.xhtml] 被重新返回,
  • 模板 [Form.java] 已被方法 form.cancel 修改。这体现在显示该模板的 [2] 列中,
  • 而 [3] 列则反映了组件的提交值。

让我们回到组件 saisie1 的代码 JSF [4];


          <!-- 第 1 行 -->
          <h:outputText value="#{msg['saisie1.prompt']}"/>
          <h:inputText id="saisie1" value="#{form.saisie1}" styleClass="saisie"/>
          <h:message for="saisie1" styleClass="error"/>
<h:outputText value="#{form.saisie1}"/>

第 4 行,组件 saisie1 的值与模板 form.saisie1 相关联。这会导致以下情况:

  • 当 GET 调用 [index.xhtml] 时,组件 saisie1 将显示模板 form.saisie1值,
  • 当 [index.xhtml] 调用 POST 时, 只有当表单的所有验证和转换均成功时,saisie1组件提交的值才会被赋予给form.saisie1模板。 无论模型是否已通过提交的值更新,如果表单在 POST 处理完成后被返回,组件将显示已提交的值,而非与其关联的模型中的值。 上图的屏幕截图即显示了这一现象:其中 [2] 和 [3] 列的值并不一致。

2.9. 示例 mv-jsf2-07:与组件状态变化相关的事件 JSF

2.9.1. 应用程序

该应用程序展示了一个无需按钮或链接即可实现的 POST 示例。表单如下:

列表 combo2 [2] 的内容与 combo1 [1] 中选中的元素相关联。 当在 [1] 中更改选择时, 表单中会生成一个 POST,在此过程中,combo2 的内容会被修改以反映 [1] 中选中的元素,随后表单会被返回。 在此 POST 过程中,不进行任何验证。

2.9.2. NetBeans 项目

该应用程序的 NetBeans 项目如下:

我们有一个唯一的表单 [index.xhtml] 及其模板 [Form.java]。

2.9.3. 应用程序环境

消息文件 [messages_fr.properties]:


app.titre=intro-07
app.titre2=JSF - Listeners
combo1.prompt=combo1
combo2.prompt=combo2
saisie1.prompt=Nombre entier de type int
submit=Valider
raz=Raz
data.required=Donnée requise
integer.required=Entrez un nombre entier
saisie.type=Type de la saisie
saisie.champ=Champ de saisie
saisie.erreur=Erreur de saisie
bean.valeur=Valeurs du modèle du formulaire

样式表 [styles.css]:


.info{
   font-family: Arial,Helvetica,sans-serif;
   font-size: 14px;
   font-weight: bold
}

.col1{
   background-color: #ccccff
}

.col2{
   background-color: #ffcccc
}

.col3{
   background-color: #ffcc66
}

.col4{
   background-color: #ccffcc
}

.error{
   color: #ff0000
}

.saisie{
   background-color: #ffcccc;
   border-color: #000000;
   border-width: 5px;
   color: #cc0033;
   font-family: cursive;
   font-size: 16px
}

.combo{
  color: green;
}

.entete{
   font-family: 'Times New Roman',Times,serif;
   font-size: 14px;
   font-weight: bold
}

2.9.4. 表单 [index.xhtml]

表单 [index.xhtml] 如下:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <h:head>
    <title>JSF</title>
    <h:outputStylesheet library="css" name="styles.css"/>
    ...
  </h:head>
  <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
    <h2><h:outputText value="#{msg['app.titre2']}"/></h2>
    <h:form id="formulaire">
      <h:messages globalOnly="true"/>
      <h:panelGrid columns="4" border="1" columnClasses="col1,col2,col3,col4">
        <!-- 头部 -->
        <h:outputText value="#{msg['saisie.type']}" styleClass="entete"/>
        <h:outputText value="#{msg['saisie.champ']}" styleClass="entete"/>
        <h:outputText value="#{msg['saisie.erreur']}" styleClass="entete"/>
        <h:outputText value="#{msg['bean.valeur']}" styleClass="entete"/>
        <!-- 第 1 行 -->
        <h:outputText value="#{msg['combo1.prompt']}"/>
        <h:selectOneMenu id="combo1" value="#{form.combo1}" immediate="true" onchange="submit();" valueChangeListener="#{form.combo1ChangeListener}" styleClass="combo">
          <f:selectItems value="#{form.combo1Items}"/>
        </h:selectOneMenu>
        <h:panelGroup></h:panelGroup>
        <h:outputText value="#{form.combo1}"/>
        <!-- 第 2 行 -->
        <h:outputText value="#{msg['combo2.prompt']}"/>
        <h:selectOneMenu id="combo2" value="#{form.combo2}" styleClass="combo">
          <f:selectItems value="#{form.combo2Items}"/>
        </h:selectOneMenu>
        <h:panelGroup></h:panelGroup>
        <h:outputText value="#{form.combo2}"/>
        <!-- 第3行 -->
        <h:outputText value="#{msg['saisie1.prompt']}"/>
        <h:inputText id="saisie1" value="#{form.saisie1}" required="true" requiredMessage="#{msg['data.required']}" styleClass="saisie" converterMessage="#{msg['integer.required']}"/>
        <h:message for="saisie1" styleClass="error"/>
        <h:outputText value="#{form.saisie1}"/>
      </h:panelGrid>
      <!-- 命令按钮 -->
      <h:panelGrid columns="2" border="0">
        <h:commandButton value="#{msg['submit']}"/>
        ...
      </h:panelGrid>
    </h:form>
  </h:body>
</html>

新变化体现在列表代码 combo1 的第 24-26 行。出现了新的属性:

  • onchange:HTML 属性——声明当 combo1 中选中的元素发生变化时,应执行的 JavaScript 函数或代码。此处,JavaScript 代码 submit() 将表单提交至服务器,
  • valueChangeListener:JSF 属性——声明当 combo1 中选中的元素发生变化时,需在服务器端执行的方法名称。总共执行两个方法:一个在客户端,另一个在服务器端
  • immediate=true:JSF 属性——确定服务器端事件处理程序的执行时机:在表单根据用户输入重新构建之后,但在输入有效性检查之前。 此处的目的是根据在列表 combo1 中选中的元素来填充列表 combo2,即使表单中可能存在错误输入。以下是一个示例:
  • 在 [1] 中,首次输入,
  • 在 [2] 中,将 combo1 中选定的元素从 A 改为 B。

最终结果如下:

已执行 POST。combo2 和 [2] 的内容已调整为combo1 [1]中选定的元素进行了调整,尽管[3]的输入有误。 正是 immediate=true 属性导致在有效性检查之前执行了 form.combo1ChangeListener 方法。 如果没有该属性,该方法就不会被执行,因为处理循环会在有效性检查处因 [3] 中的错误而停止。

[messages.properties] 中与该表单相关的消息如下:


app.titre=intro-07
app.titre2=JSF - Listeners
combo1.prompt=combo1
combo2.prompt=combo2
saisie1.prompt=Nombre entier de type int
submit=Valider
raz=Raz
data.required=Donnée requise
integer.required=Entrez un nombre entier
saisie.type=Type de la saisie
saisie.champ=Champ de saisie
saisie.erreur=Erreur de saisie
bean.valeur=Valeurs du modèle du formulaire

[Form.java] 的生命周期被设置为 request


package forms;

...

@ManagedBean
@RequestScoped
public class Form {

第 6 行,将 Bean 的作用域设置为 request

2.9.5. 模板 [Form.java]

[Form.java] 模板如下:


package forms;

import java.util.logging.Logger;
import javax.enterprise.context.RequestScoped;
import javax.faces.bean.ManagedBean;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.SelectItem;

@ManagedBean
@RequestScoped
public class Form {
  
  public Form() {
  }
  
// 表单字段
  private String combo1="A";
  private String combo2="A1";
  private Integer saisie1=0;
  
  // 工作字段
  final private String[] combo1Labels={"A","B","C"};
  private String combo1Label="A";
  private static final Logger logger=Logger.getLogger("forms.Form");
  
  // 方法
  public SelectItem[] getCombo1Items(){
    // 初始化 combo1
    SelectItem[] combo1Items=new SelectItem[combo1Labels.length];
    for(int i=0;i<combo1Labels.length;i++){
      combo1Items[i]=new SelectItem(combo1Labels[i],combo1Labels[i]);
    }
    return combo1Items;
  }
  
  public SelectItem[] getCombo2Items(){
    // 根据 combo1 初始化 combo2
    SelectItem[] combo2Items=new SelectItem[5];
    for(int i=1;i<=combo2Items.length;i++){
      combo2Items[i-1]=new SelectItem(combo1Label+i,combo1Label+i);
    }
    return combo2Items;
  }
  
  // 监听器
  public void combo1ChangeListener(ValueChangeEvent event){
    // 跟踪
    logger.info("combo1ChangeListener");
    // 获取combo1提交的值
    combo1Label=(String)event.getNewValue();
    // 返回响应,因为我们想绕过验证
    FacesContext.getCurrentInstance().renderResponse();
  }
  
  public String raz(){
    // 后续
    logger.info("raz");
    // 清空表单
    combo1Label="A";
    combo1="A";
    combo2="A1";
    saisie1=0;
    return null;
  }
  
// getter - setter
  ...
}

将表单 [index.xhtml] 与其模板 [Form.java] 关联:

列表 combo1 由以下代码 JSF 生成:


        <h:selectOneMenu id="combo1" value="#{form.combo1}" immediate="true" onchange="submit();" valueChangeListener="#{form.combo1ChangeListener}" styleClass="combo">
          <f:selectItems value="#{form.combo1Items}"/>
</h:selectOneMenu>

它通过其模板中的方法 getCombo1Items(第 2 行)获取其元素。该方法在 Java 代码的第 28-35 行中定义。它生成一个包含三个元素的列表 {"A","B","C"}。

列表 combo2 由以下代码 JSF 生成:


        <h:selectOneMenu id="combo2" value="#{form.combo2}" styleClass="combo">
          <f:selectItems value="#{form.combo2Items}"/>
</h:selectOneMenu>

它通过模型中的 getCombo2Items 方法获取这些元素(第 2 行)。该方法在 Java 代码的第 37-44 行中定义。它生成一个包含五个元素的列表:{"X1","X2","X3","X4","X5"},其中 X 代表第 16 行中的 combo1Label 元素。 因此,在表单的初始生成时,列表 combo2 包含以下元素:{"A1","A2","A3","A4","A5"}。

当用户更改列表 combo1 中选中的元素时,

  • 客户端浏览器将处理事件 onchange="submit();"。表单将因此被提交至服务器,
  • 在服务器端,JSF 将检测到组件 combo1 的值已发生变化。第 47-54 行中的 combo1ChangeListener 方法将被执行。 类型为 ValueChangeListener 的方法接收一个类型为 javax.faces.event.ValueChangeEvent 的对象作为参数。该对象可通过以下方法获取值发生变化的组件的旧值和新值:

Image

此处的组件是类型为 UISelectOne 的列表 combo1。其值类型为 String

  • Java 模型第 51 行:combo1 的新值被存储在 combo1Label 中,该值用于生成列表 combo2 的元素,
  • 第53行:返回响应。需注意,combo1ChangeListener处理程序使用immediate="true"属性执行的。因此,它是在页面组件树已根据提交值更新之后、提交值验证过程开始之前执行的。 但我们需要跳过此验证过程,因为即使表单中仍存在错误输入,列表 combo2 也必须被更新。因此,我们要求立即发送响应,而无需经过输入验证阶段。
  • 表单将按输入时的原样发送。 然而,列表 combo1 combo2 中的元素并非已提交的值。它们将通过调用方法 getCombo1Items getCombo2Items 重新生成。 随后,该方法将使用由 combo1ChangeListener 确定的 combo1Label 的新值,而列表 combo2 中的元素也将随之改变。

2.9.6. 按钮 [Raz]

我们希望通过按钮 [Raz] 将表单恢复到初始状态,如下所示:

在 [1] 中,表单状态为 [Raz] 按钮执行 POST 操作之前;而在 [2] 中,表单状态为 POST 操作的结果。

尽管功能上很简单,但此用例的管理却相当复杂。我们可以尝试各种解决方案,特别是前一个示例中用于按钮 [Annuler] 的方案:


       <h:commandButton value="#{msg['raz']}" immediate="true" action="#{form.raz}"/>

其中方法 form.raz 如下:


  public String raz(){
    // 清空表单
    combo1Label="A";
    combo1="A";
    combo2="A1";
    saisie1=0;
    return null;
}

因此,前例中按钮 [Raz] 生成的结果如下:

[1] 列显示已执行方法 form.raz。然而,[1] 列仍显示已提交的值:

  • 对于 combo1,已发布的值是“B”。因此该条目在列表中被选中,
  • 对于 combo2,其已发布值为“B5”。由于执行了 form.raz,combo2 中的元素 {"B1", ..., "B5"} 被更改为 {"A1", ..., "A5"}。 元素“B5”已不存在,因此无法被选中。此时将显示列表中的第一个元素,
  • 对于 saisie1,其发布值是 10。

这是设置 immediate="true" 属性时的正常行为。若要获得不同的结果,即使用户输入了其他值,也需将期望显示的值直接提交至新表单。这可以通过少量客户端 JavaScript 代码实现。表单代码如下:


<script language="javascript">
  function raz(){
    document.forms['formulaire'].elements['formulaire:combo1'].value="A";
    document.forms['formulaire'].elements['formulaire:combo2'].value="A1";
    document.forms['formulaire'].elements['formulaire:saisie1'].value=0;
    //document.forms['formulaire'].submit();
  }
</script>
...
<h:commandButton value="#{msg['raz']}" onclick='raz()' immediate="true" action="#{form.raz}"/>
  • 第 10 行,onclick='raz()' 属性指示当用户点击按钮 [Raz] 时,执行 JavaScript 函数 raz
  • 第3行:将值“A”赋给名称为'form:combo1'的元素HTML。第3行的各个元素如下:
    • document:浏览器显示的页面,
    • document.forms:文档中的所有表单集合,
    • document.forms['formulaire']:具有属性 name="formulaire", 的表单
    • documents.forms['formulaire'].elements:具有 name="formulaire", 属性的表单元素集合
    • document.forms['formulaire'].elements['formulaire:combo1']:具有属性 name="formulaire:combo1" 的表单元素
    • document.forms['formulaire'].elements['formulaire:combo1'].value:具有属性 name="formulaire:combo1". 的表单元素将提交的值

要了解浏览器显示的页面中各个元素的 name 属性,可以查看其源代码(如下所示,包含 IE7):

<form id="formulaire" name="formulaire" ...>
...
<select id="formulaire:combo1" name="formulaire:combo1" ...>

基于上述说明,我们可以理解在函数 raz 的 JavaScript 代码中:

  • 第 3 行使得提交给组件 combo1 的值将是字符串 A,
  • 第4行导致发送给组件combo2的值是字符串A1,
  • 第 5 行导致 saisie1 组件的提交值为字符串 0。

完成上述操作后,表单中的 POST 将与任何 <h:commandButton> 类型的按钮(第 10 行)关联并触发。方法 form.raz 将被执行,表单将按提交时的原样返回。 此时将得到以下结果:

这一结果背后隐藏着许多信息。组件 combo1combo2saisie1 的值“A”、“A1”、“0”会被提交至服务器。 假设 combo1 的先前值为“B”。那么 combo1 组件的值发生了变化,form.combo1ChangeListener 方法也应被执行。 我们有两个具有 immediate="true" 属性的事件处理程序。它们都会被执行吗?如果是,顺序如何?还是只执行其中一个?如果是,哪个会被执行?

为了进一步了解情况,我们在应用程序中创建了日志:


package forms;

import java.util.logging.Logger;
...
public class Form {
  
...  
// 表单字段
  private String combo1="A";
  private String combo2="A1";
  private Integer saisie1=0;
  
  // 工作字段
  final private String[] combo1Labels={"A","B","C"};
  private String combo1Label="A";
  private static final Logger logger=Logger.getLogger("forms.Form");
  
  // 监听器
  public void combo1ChangeListener(ValueChangeEvent event){
    // 跟踪
    logger.info("combo1ChangeListener");
    // 获取 combo1 的提交值
    combo1Label=(String)event.getNewValue();
    // 返回响应,因为我们希望绕过验证
    FacesContext.getCurrentInstance().renderResponse();
  }
  
  public String raz(){
    // 后续
    logger.info("raz");
    // 清空表单
    combo1Label="A";
    combo1="A";
    combo2="A1";
    saisie1=0;
    return null;
  }
...
}
  • 第 16 行:创建了一个日志生成器。getLogger 的参数用于区分日志的来源。此处,日志生成器名为 forms.Form,
  • 第 21 行:记录进入方法 combo1ChangeListener,
  • 第 30 行:记录进入方法 raz

点击按钮 [Raz] 或修改 combo1 的值会生成哪些日志?让我们考虑几种情况:

  • combo1 中选中的元素为“A”时,点击按钮 [Raz]。“A”因此是组件 combo1 的最新值。 我们已知按钮 [Raz] 会执行一个 JavaScript 函数,该函数会将值“A”提交给组件 combo1。因此,组件 combo1 的值不会发生变化。 日志显示,此时仅执行了 form.raz 方法:
  
  • 此时使用了按钮 [Raz],而 combo1 中选中的元素并非“A”。 因此,组件 combo1 的值发生了变化:其上一个值并非“A”,而按钮 [Raz] 将向其发送值“A”。日志显示此时执行了两个方法。 顺序依次为:combo1ChangeListenerraz
  
  • 我们不使用按钮 [Raz] 直接修改 combo1 的值。日志显示仅执行了方法 combo1ChangeListener
  

2.10. 示例 mv-jsf2-08:<h:dataTable> 标签

2.10.1. 应用程序

该应用程序显示了一份人员列表,并支持删除功能:

  • 在 [1] 中,显示人员列表,
  • 在 [2] 中,显示用于删除人员的链接。

2.10.2. NetBeans 项目

该应用程序的 NetBeans 项目如下:

我们有一个唯一的表单 [index.xhtml] 及其模板 [Form.java]。

2.10.3. 应用程序环境

配置文件 [faces-config.xml]:


<?xml version='1.0' encoding='UTF-8'?>

<!-- =========== FULL CONFIGURATION FILE ================================== -->

<faces-config version="2.0"
              xmlns="http://java.sun.com/xml/ns/javaee" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
    <message-bundle>messages</message-bundle>
  </application>
</faces-config>

消息文件 [messages_fr.properties]:


app.titre=intro-08
app.titre2=JSF - DataTable
submit=Valider
personnes.headers.id=Id
personnes.headers.nom=Nom
personnes.headers.prenom=Pr\u00e9nom

样式表 [styles.css]:


.headers {
   text-align: center;
   font-style: italic;
   color: Snow;
   background: Teal;
}

.id {
   height: 25px;
   text-align: center;
   background: MediumTurquoise;
}

.nom {
   text-align: left;
   background: PowderBlue;
}
.prenom {
   width: 6em;
   text-align: left;
   color: Black;
   background: MediumTurquoise;
}

2.10.4. 表单 [index.xhtml] 及其模板 [Form.java]

回顾与页面 [index.xhtml] 关联的视图:

  

表单 [index.xhtml] 如下所示:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core">
  <h:head>
    <title>JSF</title>
    <h:outputStylesheet library="css" name="styles.css"/>
  </h:head>
  <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
    <h2><h:outputText value="#{msg['app.titre2']}"/></h2>
    <h:form id="formulaire">
      <h:dataTable value="#{form.personnes}" var="personne" headerClass="headers" columnClasses="id,nom,prenom">
  ........................
      </h:dataTable>
    </h:form>
  </h:body>
</html>

第 14 行,标签 <h:dataTable> 使用字段 #{form.personnes} 作为数据源。具体如下:

private List<Personne> personnes;

[Personne] 类如下:


package forms;

public class Personne {
  // 数据
  private int id;
  private String nom;
  private String prénom;
  
  // 制造商
  public Personne(){
    
  }
  
  public Personne(int id, String nom, String prénom){
    this.id=id;
    this.nom=nom;
    this.prénom=prénom;
  }
  
  // toString
  public String toString(){
    return String.format("Personne[%d,%s,%s]", id,nom,prénom);
  }
  
  // getter 和 setter
...
}

让我们回到 <h:dataTable> 标签的内容:


<h:dataTable value="#{form.personnes}" var="personne" headerClass="headers" columnClasses="id,nom,prenom">
...
</h:dataTable>
  • var="personne" 属性定义了在 <h:datatable> 标签内表示当前人员的变量名称,
  • 属性 headerClass="headers" 设定了表格列标题的样式,
  • columnClasses="...." 属性用于设置表格中各列的样式。

让我们查看表格中的一列,了解它是如何构建的:

  

Id 列的代码 XHTML 如下:


<h:dataTable value="#{form.personnes}" var="personne" headerClass="headers" columnClasses="id,nom,prenom">
        <h:column>
          <f:facet name="header">
            <h:outputText value="#{msg['personnes.headers.id']}"/>
          </f:facet>
          <h:outputText value="#{personne.id}"/>
        </h:column>
        ...
      </h:dataTable>

lignes 3-5 : la balise <f:facet name="header"> définit le titre de la colonne,
ligne 4 : le titre de la colonne est pris dans le fichier des messages,
ligne 6 : personne fait référence à l'attribut var de la balise <h:dataTable ...> (ligne 1). On écrit donc l'id de la personne courante.


<h:dataTable value="#{form.personnes}" var="personne" headerClass="headers" columnClasses="id,nom,prenom">
        <h:column>
          <f:facet name="header">
            <h:outputText value="#{msg['personnes.headers.id']}"/>
          </f:facet>
          <h:outputText value="#{personne.id}"/>
        </h:column>
        <h:column>
          <f:facet name="header">
            <h:outputText value="#{msg['personnes.headers.nom']}"/>
          </f:facet>
          <h:outputText value="#{personne.nom}"/>
        </h:column>
        <h:column>
          <f:facet name="header">
            <h:outputText value="#{msg['personnes.headers.prenom']}"/>
          </f:facet>
          <h:outputText value="#{personne.prénom}"/>
        </h:column>
...
      </h:dataTable>
  • 第 3-7 行:表格的 id 列,
  • 第 8-13 行:表格的姓氏列,
  • 第14-19行:表格的“名字”列。

现在,让我们查看链接列 [Retirer]:

该列由以下代码生成:


<h:dataTable value="#{form.personnes}" var="personne" headerClass="headers" columnClasses="id,nom,prenom">
...
        <h:column>
          <h:commandLink value="Retirer" action="#{form.retirerPersonne}">
            <f:setPropertyActionListener target="#{form.personneId}" value="#{personne.id}"/>
          </h:commandLink>
        </h:column>
      </h:dataTable>

链接 [Retirer] 由第 4-6 行生成。当点击该链接时,将执行方法 [Form].retirerPersonne。 现在让我们来查看类 [Form.java]:


package forms;

import java.util.ArrayList;
import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

@ManagedBean
@SessionScoped
public class Form {

  // 模型
  private List<Personne> personnes;
  private int personneId;

  // 构造函数
  public Form() {
    // 人员列表的初始化
    personnes = new ArrayList<Personne>();
    personnes.add(new Personne(1, "dupont", "jacques"));
    personnes.add(new Personne(2, "durand", "élise"));
    personnes.add(new Personne(3, "martin", "jacqueline"));
  }

  public String retirerPersonne() {
    // 查找所选人员
    int i = 0;
    for (Personne personne : personnes) {
      // 当前人员 = 选定人员?
      if (personne.getId() == personneId) {
        // 从列表中删除当前人员
        personnes.remove(i);
        // 操作完成
        break;
      } else {
        // 下一位
        i++;
      }
    }
    // 在同一页面上进行测试
    return null;
  }
  
  // getter 和 setter
...
}
  • 第 18-24 行:构造函数初始化第 14 行中的人员列表,
  • 第 10 行:由于该列表需要在请求过程中保持有效,因此 Bean 的作用域为会话。

当第26行的[retirerPersonne]方法执行时,第15行的字段已被点击[Retirer]链接时对应人员的ID初始化:


          <h:commandLink value="Retirer" action="#{form.retirerPersonne}">
            <f:setPropertyActionListener target="#{form.personneId}" value="#{personne.id}"/>
</h:commandLink>

标签 <f:setPropertyActionListener> 用于向表单传输信息。在此,value 属性的值会被复制到由 target 属性标识的表单字段中。 因此,当前人员的 ID(即需要从人员列表中删除的那个)将通过该字段的 getter 方法复制到字段 [Form].personneId 中。此操作在执行第 1 行 action 属性所引用的方法之前完成。

第 26-43 行,方法 [supprimerPersonne] 删除 id 等于 personneId 的联系人。

2.11. 示例 mv-jsf2-09:JSF 应用程序的页面布局

2.11.1. 该应用程序

该应用程序演示了如何为具有两个视图的 JSF 应用程序进行页面布局:

该应用程序包含两个视图:

  • 在 [1] 中,第 1 页,
  • [2] 为第 2 页。

用户可以在这两个页面之间进行导航。本文旨在说明,如上图所示,第 1 页和第 2 页共享相同的布局。

2.11.2. NetBeans 项目

该应用程序的 NetBeans 项目如下:

该应用程序仅包含 XHTML 页面。没有关联的 Java 模板。

2.11.3. 页面 [layout.xhtml]

页面 [layout.xhtml] 确定了应用程序页面的样式:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <h:head>
    <title>JSF</title>
    <h:outputStylesheet library="css" name="styles.css"/>
  </h:head>
  <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
    <h:form id="formulaire">
      <table style="width: 400px">
        <tr>
          <td colspan="2" bgcolor="#ccccff">
            <ui:include src="entete.xhtml"/>
          </td>
        </tr>
        <tr style="height: 200px">
          <td bgcolor="#ffcccc">
            <ui:include src="menu.xhtml"/>
          </td>
          <td>
            <ui:insert name="contenu" >
              <h2>Contenu</h2>
            </ui:insert>
          </td>
        </tr>
        <tr bgcolor="#ffcc66">
          <td colspan="2">
            <ui:include src="basdepage.xhtml"/>
          </td>
        </tr>         
      </table>
    </h:form>
  </h:body>
</html>

第 7 行出现了一个新的命名空间 ui。该命名空间包含用于格式化应用程序页面的标签。该命名空间中的标签在第 17、22、25、32 行中使用。

页面 [layout.xhtml] 通过表格 HTML(第 14 行)显示信息。可通过浏览器访问该页面:

  • 在 [1] 中,请求的是 URL。

字段 [2] 由以下代码 XHTML 生成:


  <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
    <h:form id="formulaire">
      <table style="width: 400px">
        <tr>
          <td colspan="2" bgcolor="#ccccff">
            <ui:include src="entete.xhtml"/>
          </td>
        </tr>
...       
      </table>
    </h:form>
</h:body>

第 6 行中的 <ui:include> 标签用于将外部代码 XHTML 包含到页面中。文件 [entete.xhtml] 内容如下:


<?xml version='1.0' encoding='UTF-8' ?>
<!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">
  <body>
    <h2>entête</h2>
  </body>
</html>

第3至8行的全部代码将被插入到[layout.xhtml]中。 因此,标签 <html> 和 <body> 将被插入到标签 <td> 中。这不会引发错误。 因此,通过 <ui:include> 插入的页面是完整的 XHTML 页面。从视觉效果来看,只有第 6 行会产生效果。 标签 <html> 和 <body> 的存在仅出于语法上的考虑。

[3] 区域由以下 XHTML 代码生成:


<h:form id="formulaire">
      <table style="width: 400px">
        <tr style="height: 200px">
          <td bgcolor="#ffcccc">
            <ui:include src="menu.xhtml"/>
          </td>
...
        </tr>
...
      </table>
    </h:form>

第 5 行中的 <ui:include> 标签包含以下文件 [menu.xhtml]:


<?xml version='1.0' encoding='UTF-8' ?>
<!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">
  <body>
    <h2>menu</h2>
  </body>
</html>

区域 [4] 由以下代码 XHTML 生成:


<h:form id="formulaire">
      <table style="width: 400px">
...
        <tr bgcolor="#ffcc66">
          <td colspan="2">
            <ui:include src="basdepage.xhtml"/>
          </td>
        </tr>         
      </table>
    </h:form>

第 6 行中的 <ui:include> 标签包含以下 [basdepage.xhtml] 文件:


<?xml version='1.0' encoding='UTF-8' ?>
<!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">
  <body>
    <h2>bas de page</h2>
  </body>
</html>

区域 [5] 由以下代码 XHTML 生成:


    <h:form id="formulaire">
...
          <td>
            <ui:insert name="contenu" >
              <h2>Contenu</h2>
            </ui:insert>
          </td>
 ...
      </table>
</h:form>

第 5 行中的 <ui:insert> 标签定义了一个名为“内容”的区域。这是一个可以接收可变内容的区域。 我们将了解具体实现方式。当我们请求页面 [layout.xhtml] 时,名为“内容”的区域未被定义任何内容。在此情况下,将使用第 4-6 行中 <ui:insert> 标签的内容。因此,第 5 行被显示出来。

2.11.4. 页面 [page1.xhtml]

页面 [layout.xhtml] 并非用于直接查看。它作为页面 [page1.xhtml] 和 [page2.xhtml] 的模板。这被称为页面模板。页面 [page1.xhtml] 如下所示:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <ui:composition template="layout.xhtml">
    <ui:define name="contenu">
      <h2>page 1</h2>
      <h:commandLink value="page 2" action="page2"/>
    </ui:define>
  </ui:composition>
</html>
  • 第6行使用了ui命名空间,
  • 第7行,通过<ui:composition>标签指定该页面关联模板[layout.xhtml],
  • 第8行,此关联使得每个<ui:define>标签将与所用模板(此处为[layout.xhtml])中的一个<ui:insert>标签建立关联。关联通过这两个标签的name属性实现,它们必须完全一致。

显示的页面是 [layout.xhtml],其中每个 <ui:insert> 标签的内容都被请求页面中 <ui:define> 标签的内容所替换。在此情况下,效果就好像显示的页面是:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <h:head>
    <title>JSF</title>
    <h:outputStylesheet library="css" name="styles.css"/>
  </h:head>
  <h:body style="background-image: url('${request.contextPath}/resources/images/standard.jpg');">
    <h:form id="formulaire">
      <table style="width: 400px">
        <tr>
          <td colspan="2" bgcolor="#ccccff">
            <ui:include src="entete.xhtml"/>
          </td>
        </tr>
        <tr style="height: 200px">
          <td bgcolor="#ffcccc">
            <ui:include src="menu.xhtml"/>
          </td>
          <td>
              <h2>page 1</h2>
              <h:commandLink value="page 2" action="page2"/>
          </td>
        </tr>
        <tr bgcolor="#ffcc66">
          <td colspan="2">
            <ui:include src="basdepage.xhtml"/>
          </td>
        </tr>         
      </table>
    </h:form>
  </h:body>
</html>

[page1.xhtml] 的第 25-26 行已插入到 [layout.xml] 的 <ui:insert> 标签位置。

页面 [page2.xhtml] 与 [page1.xhtml] 类似:


<?xml version='1.0' encoding='UTF-8' ?>
<!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:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <ui:composition template="layout.xhtml">
    <ui:define name="contenu">
      <h2>page 2</h2>
      <h:commandLink value="page 1" action="page1"/>
    </ui:define>
  </ui:composition>
</html>

2.12. 结论

上述对 JSF 2 的研究远非详尽。但足以理解后续的示例。如需深入了解,请参阅 [ref2]。

2.13. 使用 Eclipse 进行测试

下面演示如何使用 SpringSource 工具套件对 Maven 项目进行测试:

  • 在 [1] 中,导入一个 Maven 项目 [2],并通过按钮 [3] 进行指定。此处我们以 Maven 项目 [mv-jsf2-09] 为例,将其导入 Eclipse
  • 在 [4] 中,导入的项目已被正确识别为 Maven 项目 [5],
  • 转换为 [6],该项目已导入项目资源管理器,
  • 在 [7] 中,该项目在 [8] Tomcat 服务器上运行 [9],
  • 在 [10] 中,Tomcat 7 已启动,
  • 在 [11] 中,项目 [mv-jsf2-09] 的主页 [11] 在 Eclipse 内部浏览器中显示。