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 位于这一层,且仅位于这一层;
  • [业务]层实现应用程序的业务规则,例如计算工资或发票。该层通过[Web]层获取用户数据,并通过[DAO]层获取DBMS数据,
  • [DAO](数据访问对象)层、[JPA](Java持久化API)层以及JDBC驱动程序负责管理对DBMS数据的访问。[JPA]层充当ORM(对象关系映射器),它在[DAO]层处理的对象与关系型数据库中的数据行和列之间起到桥梁作用;
  • 这些层的集成可通过 Spring 容器或 EJB3(企业级 JavaBeans)实现。

下文提供的用于说明 JSF 的示例将仅使用单一层,即 [Web] 层:

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

2.2. JSF 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]。该组件位于 [javaee.jar] 库中,
  • 视图(V)由采用 Facelets 技术的页面实现,
  • M 模型和事件处理程序由 Java 类实现,这些类通常被称为“后端 Bean”或简称为 Bean

现在,让我们厘清 MVC Web 架构与分层架构之间的关系。这两个概念虽常被混淆,但实则不同。以单层 JSF Web 应用程序为例:

如果我们使用 JSF 实现 [Web] 层,确实会形成 MVC Web 架构,但并非多层架构。在此情况下,[Web] 层将包揽所有工作:呈现、业务逻辑和数据访问。在 JSF 中,这些工作将由 Bean 来完成。

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

Web 层可以在不使用框架且不遵循 MVC 模型的情况下实现。这样我们就得到了一个多层架构,但 Web 层并未实现 MVC 模型。

在MVC中,我们曾提到M模型即V视图所呈现的数据集。关于MVC中M模型的另一种常见定义是:

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

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

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

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

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

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

2.3.1. 项目生成

我们将使用 NetBeans 7 生成我们的第一个 JSF2 项目。

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

让我们来查看项目元素,并解释每个元素的作用。

  • 在 [1] 中:项目的不同分支:
    • [Web Pages]:将包含网页文件(.xhtml、.jsp、.html)、资源(图片、各类文档)、Web层配置以及JSF框架配置;
    • [源代码包]:项目的 Java 类;
    • [依赖项]:项目所需的、由 Maven 框架管理的 .jar 归档文件;
    • [Java 依赖项]:项目所需且未由 Maven 框架管理的 .jar 归档文件;
    • [项目文件]: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 框架的库 groupIdorg.springframework,而 JSF 框架的库 groupIdjavax.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、version、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] 文件夹由项目构建生成,
  • [5]: 这里,项目构建生成了一个归档文件 [mv-jsf2-01-1.0-SNAPSHOT.war]。这是由 Tomcat 服务器执行的归档文件。

2.3.4. 为 JSF 配置项目

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

  • 在 [1] 中,进入项目属性,
  • 在 [2] 中,选择 [框架] 类别,
  • 在 [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 会自动下载它们。它从所谓的仓库中获取这些依赖项。默认使用中央仓库。可以使用 <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 命名空间)的 <html> 标签。

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

当遇到命名空间声明时,Web 服务器将在应用程序类路径中的 [META-INF] 目录下搜索后缀为 .tld(TagLib 定义)的文件。在此示例中,它将在 [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">

<!-- ============== Tag Library Description Elements ============= -->

    <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>

<!-- ============== Tag Library Validator ============= -->
...
  • 第 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,即一个能够处理客户端请求的 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] 仅处理形式为 [/faces/*] 的 URL。我们已经看到,应用程序上下文的名称为 [/mv-jsf2-01]。 因此,由 [Faces Servlet] 处理的客户端 URL 将采用 [http://machine:port/mv-jsf2-01/faces/*] 的形式。 .html 和 .jsp 页面默认由 Servlet 容器本身处理,而非由特定的 Servlet 处理。这是因为 Servlet 容器知道如何处理它们,
  • 第 7–10 行:定义 [Faces Servlet]。由于所有被接受的 URL 都会被定向到它,因此它是 MVC 模型中的 C(控制器),
  • 第 10 行:表明该 Servlet 必须在 Web 服务器启动时立即加载到内存中。默认情况下,Servlet 仅在收到针对它的第一个请求时才会被加载,
  • 第 3–6 行:为 [Faces Servlet] 定义参数。javax.faces.PROJECT_STAGE 参数定义了项目运行的阶段。在开发阶段,[Faces Servlet] 会显示有助于调试的错误消息。在生产阶段,这些消息将不再显示,
  • 第 17–19 行:会话时长(以分钟为单位)。客户端通过一系列请求/响应循环与应用程序交互。每个循环使用独立的 TCP/IP 连接,且每次循环都会重新建立连接。 因此,如果客户端 C 发出两个请求 D1 和 D2,服务器 S 无从得知这两个请求属于同一个客户端 C。服务器 S 并不拥有客户端的 内存。这是 HTTP 协议(超文本传输协议)的一个特征:客户端通过一系列客户端请求/服务器响应循环与服务器通信,每个循环都使用新的 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] 中,如前所述,返回了主页(欢迎文件)[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] 中,选择 [窗口 / 其他 / Maven 仓库浏览器] 选项,
  • 在 [2] 处,将打开一个 [Maven 仓库] 选项卡,
  • 在 [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、version、packaging(Type)和 scope)。首先指定 [groupId] [3],
  • 在 [4] 处,我们按 [空格键] 显示可能的工件列表。这里显示 [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] 文件。读者只需使用该文件即可复现文档中描述的条件。此外,主流 Java IDE(Eclipse、NetBeans、IntelliJ、JDeveloper)均支持 Maven 项目。因此,读者可以使用自己喜欢的 IDE 来测试这些示例。

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] 中:在 [文件] 选项卡中创建一个 [src/main/resources] 文件夹,
  • 在 [6] 的 [Projects] 选项卡中,这将创建 [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> 标签出现的位置。locale 属性允许您为页面指定显示语言。在此,我们将使用两种语言:英语和法语。locale 属性的值以 EL(表达式语言)表达式 #{expression} 的形式表示。该表达式的形式可以多种多样。 我们通常将其表示为 bean['key'] bean.field。在我们的示例中,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.field} 会被解析为 [Bean].getField(),其中 [Bean] 是被赋予名称 bean 的 Java 类的实例,而 getField 该 bean 字段关联的 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{
  // page locale
  private String locale="fr";
  
  public ChangeLocale() {
  }
  
  ...
  public String getLocale() {
    return locale;
  }
  
}
  • 第 11 行:locale 字段,
  • 第 17 行:其 getter 方法,
  • 第 7 行:ManagedBean 注解使 Java 类 ChangeLocale 成为 JSF 识别的 Bean。Bean 通过名称进行标识。可通过注解的 name 属性进行设置:@ManagedBean(name="xx")。如果省略 name 属性,则使用类名,并将首字母转换为小写。 因此,ChangeLocale Bean 的名称为 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] 接口。这是会话范围 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> 标签以 #{bean['field']} 的形式显示 EL 表达式 #{msg['welcome.title']} 的值bean 可以是 Java 类的名称,也可以是消息文件的名称。此处,它是一个消息文件的名称。该消息文件必须在 [faces-config.xml] 配置文件中进行声明。msg Bean 的声明如下:

<?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[_LanguageCode][_CountryCode].properties。<base-name> 标签仅定义名称的前缀部分,其余部分由系统自动生成。可能存在多个消息文件,每种语言对应一个:
  • 在 [1] 中,我们可以看到四个消息文件,它们对应于 [faces-config.xml] 中定义的基名 `messages`
    • 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 行定义。系统将在项目的类路径中搜索该文件。如果该文件位于某个包内,则必须在第 14 行定义该包,例如 resources.messages,如果 [messages.properties] 文件位于类路径的 [resources] 文件夹中。 由于第 14 行中的名称未包含包名,因此 [messages.properties] 文件必须放置在 [src/main/resources] 文件夹的根目录下:

在 [1] 中,NetBeans 项目的 [Projects] 选项卡中,[messages.properties] 文件会以已定义的不同消息版本列表的形式显示。 这些版本通过一到三个代码组成的序列 [语言代码_国家代码_变体代码] 进行标识。在 [1] 中,仅使用了 [语言代码]: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.title 键的消息(即存在 msg 标识符)的值。该消息会在当前活动语言的 [messages.properties] 文件中进行查找并获取。例如,对于法语:


welcome.titre=Tutoriel JSF (JavaServer Faces)

消息采用 key=value 格式。在评估表达式 #{msg['welcome.title']} 后,[index.xhtml] 文件的第 8 行将变为如下内容:


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

这种消息文件机制使得在 JSF 项目中更改页面语言变得非常简单。这被称为项目国际化,或更常以其缩写 i18n 来称呼,因为“internationalization”一词以 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 "form" 进行标识。在此示例中,表单的 id 不会被使用,因此可以省略。

  • 第 18–21 行:此处的 `<h:panelGrid>` 标签定义了一个两列的 HTML 表格。它生成了 HTML 标签 `<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 标签 <a> onclick 属性。当用户点击 [French] 链接时,JavaScript 代码将被执行。该代码嵌入在浏览器接收的页面中,并由浏览器执行。 JavaScript 在 JSF 和 AJAX(异步 JavaScript 和 XML)中被广泛使用其主要目的是提高 Web 应用程序的易用性和响应速度。它通常由软件工具自动生成,因此无需深入理解。不过,有时开发人员可能需要在 JSF 页面中添加 JavaScript 代码。在这种情况下,掌握 JavaScript 知识是必要的。

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

  • JavaScript 代码会使用我们为 JSF <h:form> 标签分配的表单 ID,
  • JSF 会为所有未定义 id 属性的标签自动生成标识符。例如:j_idt8。为标签赋予清晰的标识符,有助于在必要时更轻松地理解生成的 JavaScript 代码。当开发人员必须自行添加 JavaScript 代码来操作页面组件时,这一点尤为重要。在这种情况下,他们需要知道组件的 ID

当用户点击上图页面中的 [French] 链接时会发生什么?让我们来探讨一下 JSF 应用程序的架构:

[Faces Servlet] 控制器将以以下 HTTP 格式接收来自客户端浏览器的请求:

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]。情况总是如此:最初通过 URLFormulaire URL 获取的 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=value1&element2=value2& ... 的格式,其编码格式由第 3 行定义。在此编码格式中,某些字符会被替换为相应的十六进制值。最后一个元素便是如此:

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

其中 %3A 代表冒号字符。因此,字符串 form:j_idt8=form:j_idt8 会被发送至服务器。您可能还记得,在分析为该标签生成的 HTML 代码时,我们已经遇到过标识符 j_idt8


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

它是由 JSF 自动生成的。这里的关键在于,客户端浏览器发送的值字符串中包含此标识符,这使得 JSF 能够识别 [French] 链接已被点击。随后,它将利用上述 action 属性来确定如何处理接收到的字符串。 action="#{changeLocale.setFrenchLocale}" 属性告知 JSF,客户端请求必须由名为 changeLocale 的对象的 [setFrenchLocale] 方法处理。请注意,该 Bean 是通过 Java 类 [ChangeLocale] 中的注解定义的:


@ManagedBean
@SessionScoped
public class ChangeLocale implements Serializable{

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

让我们回到浏览器请求:

以及生成我们点击的 [French] 链接的 <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}"/>

中,[French] 链接的点击事件处理程序是方法 [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{
  // page locale
  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() 方法将返回以下字典:

表单
form
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"。要切换为英语,则将其设置为 "en"locale 属性的值是通过表达式 [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] 文件。 点击 [English] 链接(第 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. 运行项目

我们的项目现已完成。我们可以进行构建(清理并构建):

  • 构建项目会在 [文件] 选项卡中生成 [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],即 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 页面的名称(不带 .xhtml 后缀)作为导航键在页面之间进行导航。

您可以选择不使用这些功能,而是像在 JSF 1 中那样在 [faces-config.xml] 中声明这些 JSF 项目元素。在这种情况下,[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 -->
  <application>
    <resource-bundle>
      <base-name>
        messages
      </base-name>
      <var>msg</var>
    </resource-bundle>
  </application>
  
  <!-- managed beans -->
  <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 -->
  <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 的作用域。可能的值包括 requestsessionapplication
  • 第 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] 中定义的导航键 </span>**<span style="color: #000000">welcome</span>**<span style="color: #000000"> 赋值给 action 属性;

  • setFrenchLocale setEnglishLocale 方法必须返回导航键,因此无需修改,因为它们此前已通过返回 null 来表示用户仍留在当前页面。

2.4.10. 结论

让我们回到之前编写过的 NetBeans 项目:

该项目遵循以下架构:

在每个 JSF 项目中,我们都会发现以下元素:

  • JSF 页面 [A],由控制器 [Faces Servlet] [3] 发送 [4] 至客户端浏览器,
  • 消息文件 [C],用于切换 JSF 页面的语言,
  • Java 类 [B],用于处理客户端浏览器 [2a, 2b] 上发生的事件,并/或作为 JSF 页面的模型 [3]。 通常情况下,[业务]层和[DAO]层是分别开发和测试的。随后,[Web]层会与一个模拟的[业务]层一起进行测试。如果[业务]层和[DAO]层已准备就绪,我们通常会使用它们的.jar文件。
  • 配置文件 [D] 用于将这些不同元素相互关联。第 44 页已介绍过 [web.xml] 文件,该文件很少会被修改。同样的情况也适用于 [faces-config],我们始终使用其简化版本。

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

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

2.5.1. 该应用程序

该应用程序仅包含一个视图:

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

  • 第 [1] 列显示所使用的 JSF/HTML 标签名称,
  • 第 [2] 列展示了每个标签的数据输入示例,
  • 第 [3] 列显示作为页面模型的 Bean 的值,
  • 通过 [4] 按钮对 [2] 中的输入进行验证。此验证仅更新页面的模型 Bean。随后返回同一页面。因此,验证完成后,第 [3] 列将显示模型 Bean 的新值,使用户能够验证其输入对页面模型的影响。

2.5.2. NetBeans 项目

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

  • 在 [1] 中,JSF 项目配置文件,
  • 在 [2] 中,项目的单页文件:index.xhtml,
  • 在 [3] 中,一个样式表 [styles.css] 用于配置 [index.xhtml] 页面的外观
  • 在 [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] 的参数。它要求在 facelet 中添加如下注释:

        <!-- langues -->

被忽略。如果没有此参数,这些注释会导致难以理解的问题,

  • 第 3–6 行:一个用于 [Faces 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] 页面中的一个示例来说明模型的概念:


<!-- line 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 具有请求作用域。这意味着在客户端请求/服务器响应的循环中,当请求需要时它会被实例化,并在向客户端返回响应后被销毁。

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


<!-- line 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,处理方式会有所不同。

[情况 1 – 页面 P 的初始请求]

浏览器使用 GET 请求请求该页面的 URL。[Faces Servlet] 控制器将直接进入响应渲染的第 [4] 步,并将 [index.xhtml] 页面发送给客户端。JSF 控制器将指示页面上的每个标签进行渲染。让我们以 [index.xhtml] 代码的第 5 行为例:


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

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

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

请注意,如果在页面 P 上使用表达式 #{M.field}(其中 M 是页面 P 的模型 Bean),则该 Bean 必须具有 public getField() 方法。该方法返回的类型必须可转换为 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 页)。

[案例 2 – 处理页面 P]

我们现在处于上文的步骤 [1],此时 [Faces Servlet] 控制器将收到来自客户端浏览器的 POST 请求,而该浏览器正是此前接收过 [index.xhtml] 页面的。我们正在处理一个页面事件。在 [2a] 中开始处理该事件之前,将经历若干步骤。JSF 控制器处理 POST 请求的流程如下:

Image

  • 在 [A] 处,得益于隐藏字段 `javax.faces.ViewState`,最初发送给客户端浏览器的视图被重建。此时,页面组件恢复了发送页面中的原始值。我们的 `inputText` 组件恢复了其值 "text",
  • 在 [B] 阶段,客户端浏览器提交的值将用于更新视图组件。因此,如果用户在名为 inputText 的 HTML 输入字段中输入了 "jean",则值 "jean" 将替换原值 "text"。此时视图反映的是用户修改后的页面,而非最初发送给浏览器的原始页面,
  • 在 [C] 中,对提交的值进行验证。假设前面的 inputText 组件是一个年龄输入字段。输入的值必须是整数。浏览器提交的值总是字符串类型。它们在与页面 P 关联的 M 模型中的最终类型可能完全不同。因此需要将字符串类型转换为另一种类型 T。这种转换可能会失败。 在此情况下,请求/响应循环将终止,且若页面 P 的作者已提供错误信息,则将 [B] 中构建的页面 P 连同错误信息一并发回客户端浏览器。请注意,用户看到的页面完全与其输入内容一致,开发者无需进行任何额外操作。而在 JSP 等其他技术中,开发者必须使用用户输入的值自行重建页面 P。 组件的值还可能需要经过验证过程。继续以 inputText 组件(即年龄输入字段)为例,输入的值不仅必须是整数,还必须属于 [1,N] 范围。如果输入的值通过了转换步骤,也可能在验证步骤中失败。在这种情况下,请求/响应循环同样会结束,并在 [B] 中构建的页面 P 将被发回客户端浏览器,
  • 在 [D] 中,如果页面 P 的所有组件都通过了转换和验证步骤,其值将被赋给页面 PM 模型。如果由以下标签生成的输入字段的值:

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

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

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

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

  • 页面 P 使用方法 [M].getC() 显示其模型 M 的字段 C,
  • 页面 P 上模型 M 的字段 C 通过方法 [M].setC(input) 采用页面 P 上输入的值进行初始化。在此步骤中,可能会发生转换验证过程,这些过程可能失败。若发生这种情况,则不会处理触发页面 P POST 的事件,页面将完全按照客户端输入的状态原样发回给客户端。

[index.xhtml] 页面的 [Form.java] 模型如下:


package forms;

import javax.enterprise.context.RequestScoped;
import javax.faces.bean.ManagedBean;
 
 
@ManagedBean
@RequestScoped
public class Form {
  
  /** Creates a new instance of Form */
  public Form() {
  }
  
  // form fields
  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";
  
  // events
  public String submit(){
    return null;
  }
  
  // getters and setters
  ...
}

第 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">
        <!-- languages -->
        <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">
          <!-- headers -->
          <h:outputText value="#{msg['form.headerCol1']}" styleClass="entete"/>
          <h:outputText value="#{msg['form.headerCol2']}" styleClass="entete"/>
          <h:outputText value="#{msg['form.headerCol3']}" styleClass="entete"/>
          <!-- line 1 -->
          ...
          <!-- line 2 -->
          ...
          <!-- line 3 -->
          ...
          <!-- line 4 -->
          ...
          <!-- line 5 -->
          ...
          <!-- line 6 -->
          ...
          <!-- line 7 -->
          ...
          <!-- line 8 -->
          ...
          <!-- line 9 -->
          ...
          <!-- line 10 -->
          ...
          <!-- line 11 -->
          ...
          <!-- line 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` 样式用于定义表格第一行文本的样式:

          <!-- entêtes -->
          <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”样式来定义表格第一列文本的样式:

          <!-- ligne 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="background-image: url('${request.contextPath}/resources/images/standard.jpg');"

该写法在任何上下文中均有效。

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

让我们回顾一下第2.5.7节中已阐述的一般情况,并将其应用于当前讨论的表单。该表单将在标准的JSF环境中进行测试:

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

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

根据浏览器请求是 GET 还是 POST,[Faces Servlet] 控制器会以不同的方式处理该请求。

[案例 1 – 表单 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代码。这是浏览器解析并显示的代码。

[案例 2 – 处理在表格 F 中输入的数值]

用户填写表单后,点击[提交]按钮将其提交。随后,浏览器向表单的URL发送一个POST请求。[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 行之后是一个空行,接着是表单的 HTML 代码,该代码由源自 POST 请求的新模板更新。

现在我们将考察此表单的各个组成部分。

2.5.11. <h:inputText> 标签

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

请看以下代码:


          <!-- line 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] 中输入一个值,并使用 [提交] 按钮 [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 的组件被还原为初始值 "text",
  • 在 [B] 处,浏览器提交的值(由用户输入)被赋值给页面 P 的组件。此时,id 为 inputText 的组件接收值 "new text",
  • 在 [C],进行转换和验证。此处没有相关操作。在模型 M 中,与 id 为 inputText 的组件关联的字段如下:

private String inputText="texte";

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

  • 在 [D] 中,输入的值被赋给模型。[Form.java] 中的 inputText 字段接收值 "new text",
  • 在 [E] 中,由于 [Validate] 按钮未关联任何事件处理程序,因此没有任何反应。
  • 在 [F] 中,页面 P 被发回客户端,因为 [Validate] 按钮没有 action 属性。随后将执行 [index.xhtml] 中的以下代码行:

          <!-- line 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 字段的值,该值现在为“new text”。这将导致以下显示效果:

2.5.12. <h:inputSecret> 标签

<h:inputSecret> 标签会生成一个 HTML <input type="password" ...> 标签。它是一个与 JSF <h:inputText> 标签类似的输入字段,不同之处在于用户输入的每个字符都会被星号 (*) 替换显示。

请看以下代码:


          <!-- line 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 方法来生成输入字段的文本。当输入字段类型为“password”时,情况有所不同。<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] 中输入一个值,并使用 [提交] 按钮 [2] 提交表单。我们将收到如下页面作为响应 [3]:

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

formulaire%3AinputSecret=mdp

通过 [2] 提交表单后,[Form.java] 模型被 [1] 中的条目更新。随后,[Form.java] 中的 inputSecret 字段接收到了值 "mdp"。由于 [index.xhtml] 表单未定义任何导航规则或事件处理程序,因此在模型更新后,该表单会被重新显示。 随后,我们回到了最初请求 [index.xhtml] 页面时显示的视图,此时仅模型中 inputSecret 字段的值发生了变化 [3]。

2.5.13. <h:inputTextArea> 标签

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

请看以下代码:


          <!-- line 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 方法。字符串 "line1\nline2" 包含 \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行:由JSF标签<h:inputTextArea>生成的HTML标签<textarea>...</textarea>

现在,请在下方输入字段 [1] 中输入一个值,然后点击 [提交] 按钮 [2] 提交表单。随后我们会看到如下页面 [3]:

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

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

通过 [2] 提交表单后,[Form.java] 模型已根据 [1] 的输入进行了更新。随后,[Form.java] 中的 textArea 字段接收到了值“JSF Tutorial\npart1”。重新加载 [index.xhtml] 后,可以看到模型的 textArea 字段确实已更新 [3]。

2.5.14. <h:selectOneListBox> 标签

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

请看以下代码:


<!-- line 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] 一个新值,并使用 [Submit] 按钮 [2] 提交表单。我们收到以下响应页面 [3]:

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

formulaire%3AselectOneListBox1=3

通过 [2] 提交表单后,[Form.java] 模型已根据 [1] 中的条目进行了更新。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> 标签的一个变体:


<!-- line 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}"/>

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


  private String selectOneListBox2="3";

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

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

通过调用 getSelectOneListBox2() 方法(第 5 行),确定了 [3] 中选中的元素。得到的结果“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] 一个新值,并使用 [提交] 按钮 [2] 提交表单。随后会得到以下页面响应 [3]:

字段 [1] 的值如下:

formulaire%3AselectOneListBox2=5

通过 [2] 提交表单后,[Form.java] 模板已根据 [1] 中输入的数据进行了更新。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>,允许用户从列表中选择多个项目。

请看以下代码:


<!-- line 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"};
  ...
  // getters et setters
  
  public String getSelectManyListBoxValue(){
    return getValue(selectManyListBox);
  }
  
  private String getValue(String[] chaines){
    String value="[";
    for(String chaine : chaines){
      value+=" "+chaine;
    }
    return value+"]";
  }

如果我们调用了 getSelectManyListBox 方法,将会得到一个字符串数组。为了将该元素包含在 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 行:当点击 [Clear] 按钮时,onclick 属性中的 JavaScript 代码将被执行。页面在浏览器中通过一个常被称为 DOM(文档对象模型)的对象树来表示。JavaScript 代码可以通过每个对象的 name 属性访问该对象。上述 HTML 代码第 3 行中的列表名为 formulaire:selectManyListBox。表单本身可以通过多种方式引用。 此处使用 this.form 这种表示法,其中 this 指代 [Reset] 按钮,而 this.form 指代包含该按钮的表单。列表 form:selectManyListBox 位于该表单内。因此,this.form['form:selectManyListBox'] 表示该列表在表单组件树中的位置。 表示列表的对象具有一个 selectedIndex 属性,其值即为列表中选中项的索引。该索引从 0 开始,表示列表中的第一项。值 -1 表示列表中未选中任何项。若列表中有选中项,将 selectedIndex 属性设置为 -1 的 JavaScript 代码将取消选中列表中的所有项。

现在,让我们从列表中选择 [1] 个新值(要选择列表中的多个项目,请按住 Ctrl 键并点击),然后使用 [提交] 按钮 [2] 提交表单。我们收到以下响应页面 [3,4]:

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

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

通过 [2] 提交表单后,[Form.java] 模型已根据条目 [1] 进行了更新。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 行,我们可以看到该方法的参数是一个字符串数组。这里,它将是数组 {"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 代码如下:


<!-- line 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 代码如下:


<!-- line 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] 包含文本“one”、……、“five”,其中“one”和“two”已被选中。生成的 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 行所示,元素“one”和“two”已被选中(存在 selected 属性)。

很难提供一个实际操作的示例截图,因为我们无法在菜单中显示所选项目。建议读者亲自尝试一下(要在列表中选择多个项目,请按住 Ctrl 键并点击)。

2.5.18. <h:inputHidden> 标签

<h:inputHidden> 标签没有视觉呈现效果。它仅用于在页面的 HTML 流中插入一个 HTML 标签 <input type="hidden" value="..."/>。当包含在 <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 代码如下:


<!-- ligne 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>

当表单提交时,第 3 行中名为 form:inputHidden 的字段的值“initial”将与其他表单值一同提交。该字段


  private String inputHidden;

将更新为该值,而这正是它最初拥有的值。该值将被包含在发回给客户端的新页面中。因此,我们总是会得到上方的截图。

提交给隐藏字段的值如下:

formulaire%3AinputHidden=initial

2.5.19. <h:selectBooleanCheckBox> 标签

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

请看以下 JSF 代码:


<!-- line 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}"/>

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


  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] 提交表单后,模型 [Form.java] 被输入 [1] 更新。随后,[Form.java] 中的 selectBooleanCheckbox 字段接收到了值 false。 重新加载 [index.xhtml] 显示,模型中的 selectBooleanCheckbox 字段确实已被更新 [3] 和 [4]。值得注意的是,正是得益于隐藏字段 javax.faces.ViewState,JSF 才能确定最初被选中的复选框已被用户取消选中。事实上,未被选中的复选框的值不会包含在浏览器提交的值中。 借助存储在隐藏字段 javax.faces.ViewState 中的组件树,JSF 确定表单中存在一个名为“selectBooleanCheckbox”的复选框,且其值未包含在客户端浏览器提交的数据中。因此,JSF 可以推断该复选框在提交的表单中处于未选中状态,从而将其布尔值 false 赋值给关联的 Java 模型:


  private boolean selectBooleanCheckbox;

2.5.20. <h:selectManyCheckBox> 标签

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

请看以下 JSF 代码:


          <!-- line 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}"/>

[Form.java] 文件中第 5 行 <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"};

该数组定义了:

  • 页面显示时,哪些复选框应被选中。这是通过其值实现的,即其 itemValue 字段。在上例中,数组 {"1","3"} 中包含值的复选框将被选中。这正是上图所示的内容;
  • 当页面提交时,selectManyCheckbox 模型会接收用户已选中复选框的值数组。这就是我们稍后将看到的;
  • XHTML 代码的第 12 行生成了 [4]。正是接下来的 getSelectManyCheckboxValue 方法生成了 [4]:

  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 代码:


<!-- line 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";

该模型定义了:

  • 页面显示时,必须选中的单个单选按钮。这是通过其值实现的,即其 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="form: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">
        <!-- languages -->
        <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">
...
          <!-- line 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}"/>
          <!-- line 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}"/>
          <!-- line 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}"/>
          <!-- line 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}"/>
          <!-- line 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"/>
...
          <!-- line 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}"/>
          <!-- line 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. 测试

欢迎读者测试此新版本。

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

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

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

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

2.7.1. 该应用程序

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

  • 在[1]中,该表单的第1页——也可通过[2]中的链接1访问
  • 在[2]中,一组5个链接。
  • 在[3]中,表单的第2页,可通过[2]中的链接2访问
  • 在[4]中,表单的第3页,通过[2]中的链接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">
        <!-- links -->
        <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"/>
          <!-- line 1 -->
          <h:outputText value="inputText"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.loginPrompt']}"/>
            <h:inputText id="inputText" value="#{form.inputText}"/>
          </h:panelGroup>
          <!-- line 2 -->
          <h:outputText value="inputSecret"  styleClass="info"/>
          <h:panelGroup>
            <h:outputText value="#{msg['form.passwdPrompt']}"/>
            <h:inputSecret id="inputSecret" value="#{form.inputSecret}"/>
          </h:panelGroup>
          <!-- line 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>
        <!-- links -->
        <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] 中引用的方法如下:

// événements
  public String doAction2(){
    return "form2";
  }
  
  public String doAction4(){
    return "form4";
  }
  
  public String doAlea(){
    // un nombre aléatoire entre 1 et 3
    int i=1+(int)(3*Math.random());
    // on rend la clé de navigation
    return "form"+i;
  }
  
  public String throwException() throws java.lang.Exception{
    throw new Exception("Exception test");
}

我们暂时忽略第 17 行中的 throwException 方法,稍后再回来讨论。doAction2doAction4 方法只是返回导航键,而不进行任何处理。我们同样可以这样写:


<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*.xhtml] 页面所用 [Form.java] 模型的生命周期

考虑以下操作序列:

  • 在 [1] 中,我们填写第 1 页并转到第 3 页,
  • 在[2]中,我们填写第3页并返回第1页,
  • 在[3]中,第1页被检索出来,与输入时完全一致。随后我们返回第3页,
  • 在[4]中,第3页被找到,与输入时完全一致。

[javax.faces.ViewState] 隐藏字段的机制不足以解释这一现象。

从 [1] 跳转到 [2] 时,会发生以下几个步骤:

  • 模型 [Form.java] 通过 [form1.jsp] 的 POST 请求得到更新。具体来说,inputText 字段接收了值 "another text",
  • 导航“form3”触发了 [form3.xhtml] 的显示。[form3.xhtml] 中嵌入的 ViewState 仅包含 [form3.xhtml] 中组件的状态,而不包含 [form1.xhtml] 中的组件状态。

从 [2] 跳转到 [3] 时:

  • [Form.java] 模型通过 [form3.xhtml] 的 POST 请求进行更新。如果 [Form.java] 模型的生命周期设置为 "request",则在通过 [form3.xhtml] 的 POST 请求进行更新之前,会创建一个全新的 [Form.java] 对象。在这种情况下,模型的 inputText 字段将恢复为其默认值:

  private String inputText="texte";

并保留该值:实际上,在 [form3.xhtml] 的 POST 请求中,没有任何内容会更新 inputText 字段,因为该字段属于 [form1.xhtml] 模型而非 [form3.xhtml] 模型,

  • 导航键“form1”会触发显示 [form1.xhtml]。页面将显示其模板。在我们的案例中,与 inputText 模板关联的登录输入字段将显示“text”,而非在 [1] 中输入的值“another text”。 若要使 inputText 字段保留在 [1] 中输入的值,[Form.java] 模板的作用域必须设置为 session 而非 request。在此情况下,
    • 在 [form1.xhtml] 发送 POST 请求后,模型将被存入客户端的会话中。inputText 字段将包含值 "some other text";
    • 当 [form3.xhtml] 发送 POST 请求时,模型将从该会话中检索,并由 [form3.xhtml] 的 POST 请求进行更新。inputText 字段不会因本次 POST 请求而更新,而是保留 [form1.xhtml] [1] 发送 POST 请求后获取的值“some other text”。

因此,[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] 可以拦截来自 [业务] 层的任何异常,并向控制器 [Faces Servlet] 返回一个导航键,该键指向针对该异常的特定错误页面;
  • 对于模型而言,此方案不可行,因为当模型被调用时[3,4],系统正处于特定 XHTML 页面的渲染阶段,而非选择阶段。 如何在某个页面的渲染阶段切换页面?一个简单的解决方案(尽管并非总是适用)是不处理该异常,使其向上传播至运行应用程序的 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],我们确保所有异常都能得到处理,
  • 通过 <error-code> 标签(第 33 行)定义 HTTP 错误代码。例如,如果浏览器请求 URL [http://machine:port/contexte/P],而应用程序上下文中不存在页面 P,则应用程序不会干预响应。此时,Servlet 容器会通过发送默认错误页面来生成此响应。 HTTP响应的第一行包含404错误代码,表明请求的页面P不存在。您可能希望生成符合应用程序视觉风格指南或提供问题解决链接的响应。在这种情况下,您应使用包含<error-code>404</error-code>标签的<error-page>标签。

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

2.7.4.2. 模拟异常

通过 [抛出异常] 链接可以人工触发异常:

点击 [抛出异常] 链接 [1] 会显示页面 [2]。

在 [formx.xhtml] 页面的代码中,[抛出异常] 链接的生成方式如下:


<!-- liens -->
        <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 通道。 参与处理请求对象的所有层 t1、t2、...、tn 均可访问这两个对象。每一层都可以访问初始请求的元素,并通过丰富响应对象来准备响应。例如,本地化层可以使用方法 response.setLocale(Locale l) 来设置响应的区域设置

各个处理层可以通过请求对象相互传递信息。该对象有一个属性字典,创建时为空,可由后续的处理层逐步填充。这些处理层可以将下一层处理所需的信息放入请求对象的属性中。管理请求对象属性的方法有两种:

  • void setAttribute(String s, Object o),该方法将由字符串 s 标识的对象 o 添加到属性中
  • Object getAttribute(String s),用于检索由字符串 s 标识的属性 o。

当异常向上传播至 Servlet 容器时,容器会在正在处理的请求中设置以下属性:

key
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>
        <!-- links -->
        <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,即转换为标准的 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 代码。该代码中包含若干对象,这些对象由上文第 1 行中的 _jspService 方法初始化:

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

在 JSF 页面中编写 #{expression},expression 可以是上述 requestsession application 对象中某个属性的。系统会依次在这三个对象中搜索对应的属性。因此,#{key} 的求值过程如下:

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

一旦获得非空值,#{key} 的求值就会停止。您可能希望通过指定应搜索属性的上下文来进一步明确:

  • #{requestScope['key']} 请求对象中搜索该属性
  • #{sessionScope['key']} 会话对象中搜索该属性
  • #{applicationScope['key']} 应用程序对象中搜索该属性

这就是第 116 页 [exception.xhtml] 页面上所做的操作。使用的属性如下:

key
domain
value
javax.servlet.error.status_code
请求
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">
        <!-- line 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"/>
        <!-- line 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}"/>
        <!-- line 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}"/>
        <!-- line 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}"/>
        <!-- line 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}"/>
        <!-- line 6 -->
        ...
        <!-- line 7 -->
        ...
        <!-- line 8 -->
        ...
        <!-- line 9 -->
        ...
        <!-- line 10 -->
        ...
        <!-- line 11 -->
        ...
        <!-- line 12 -->
        ...
        <!-- line 13 -->
        ...
      </h:panelGrid>
      <!-- control buttons -->
      <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() {
}
// foreclosures
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 = "";
 
// actions
public String submit() {
...
}
 
public String cancel() {
...
}
 
// validators
public void validateSaisie10(FacesContext context, UIComponent component, Object value) {
...
}
// getters and setters
...
}

这里的新特性在于,模型字段不再仅限于 String 类型,而是可以是多种类型。

2.8.5. 不同的表单输入项

现在我们将逐一探讨不同的表单输入项。

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

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


<!-- ligne 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.saisie1 模型在 [Form.java] 中定义如下:


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] 失败,页面将重新显示,并再次执行以下代码:


<!-- ligne 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] 的内容:

...
# ==============================================================================
# Component Errors
# ==============================================================================
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.

# ==============================================================================
# Converter Errors
# ==============================================================================
...
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}
...


# ==============================================================================
# Validator Errors
# ==============================================================================
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" 上发生的错误字符串类型到整数类型的转换错误。相关的错误消息是消息文件第 18 行中的那条。

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

显示的错误信息如下:

从该消息中我们可以看到:

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

大多数与组件相关的消息都有两个版本:摘要版和详细版。第 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> 标签默认显示详细消息。可以通过 showSummaryshowDetail 属性来更改此行为。对于 id 为 saisie2 的组件就是这样实现的


        <!-- ligne 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)> 标签,应先搜索 [messages.properties] 文件,再搜索 [javax.faces.Messages.properties] 文件。因此,如果我们在 [messages_fr.properties] 文件中添加以下内容:


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

input1input2 组件返回的错误变为:

Image

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


        <!-- ligne 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 行,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] 中的代码如下:


        <!-- ligne 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 代码如下:


<!-- ligne 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}"/>
        <!-- ligne 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}"/>

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


  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 代码:


<!-- ligne 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 代码输入日期:


<!-- ligne 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.Date --> java.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(日)表示日期,MM(月)表示月份,yyyy(年)表示年份。在 [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 演示了如何强制要求输入的字符串长度在指定范围内:


<!-- ligne 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 演示了如何强制要求输入的字符串长度必须在指定范围内:


<!-- ligne 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]以获取更多详细信息。

在此我们介绍另一种方法:利用表单模型中的方法来验证输入数据。以下是一个示例:


<!-- ligne 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 请求HttpServletResponse 响应对象
  • UIComponent component:待验证的组件。<h:inputText> 标签由继承自 UIComponent 的 UIInput 组件表示。在此,该 UIInput 组件作为第二个参数传递,
  • Object value:待检查的输入值,已转换为模型的类型。这里需要特别注意的是,如果字符串到模型类型的转换失败,则验证方法不会被执行。当调用 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、
  • String summary:错误消息的摘要版本——由 <h:message showSummary="true"> 和 <h:messages> 标签显示,
  • String detail:错误消息的详细版本——由 <h:message> 和 <h:messages showDetail="true"> 标签显示。

可以使用任意构造函数;缺失的参数可通过 set 方法在后续设置。

构造函数 [1] 不允许您指定来自国际化消息文件的消息。这显然令人遗憾。 David GearyCay Horstmann [ref2] 在其著作《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.xml],第 120 页),

  • String 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] 中,FacesMessage 是通过静态方法 Messages.getMessage 创建的
  • 在 [5] 中,设置了消息的严重性级别,
  • 在 [6] 中,抛出一个包含先前构建的消息的 ValidatorException。该验证方法由以下 XHTML 代码调用:

<!-- ligne 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 代码如下:


<!-- line 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}"/>
        <!-- line 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] 页面的验证由 [Validate] 按钮负责,其 JSF 代码如下:


<!-- boutons de commande -->
      <h:panelGrid columns="2">
        <h:commandButton value="#{msg['submit']}" action="#{form.submit}"/>
        ...
      </h:panelGrid>

其中消息 msg['submit'] 如下所示:


submit=Valider

如第 3 行所示,将执行 form.submit 方法来处理 [Validate] 按钮的点击事件。具体如下:


  // actions
  public String submit() {
    // latest validations
    validateForm();
    // we return the same form
    return null;
  }
 
  // global validations
  private void validateForm() {
    if ((saisie11 + saisie12) != 10) {
...
}

需要理解的是,当提交方法被执行时:

  • 所有表单验证器和转换器都已执行并通过,
  • [Form.java] 模型中的字段已接收客户端提交的值。

实际上,让我们重新回顾一下 JSF POST 的处理流程:

submit 方法是一个事件处理程序。它处理 [Validate] 按钮的点击事件。与所有事件处理程序一样,它运行在 [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">
        <!-- ligne 1 -->
        ...

在第 2 行,<h:messages> 标签默认会显示所有无效表单组件输入的错误消息摘要,以及所有未关联到组件的错误消息。globalOnly="true" 属性将显示范围限制为后者。

消息 [2] 和 [3] 使用简单的 <h:outputText> 标签显示:


<!-- line 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}"/>
        <!-- line 13 -->
        ...
          <h:outputText value="#{form.errorSaisie12}" styleClass="error"/>
        ...

第 4–7 行:saisie11 组件有两种可能的错误消息:

  • 一种表示转换无效或数据缺失。该消息由 JSF 自身生成,将封装在 FacesMessage 类型中,并通过第 5 行的 <h:message> 标签显示;
  • 另一种是我们将在 input11 + input12 不等于 10 时生成的。它将显示在第 6 行。该错误消息将包含在 form.errorSaisie11 模板中。

这两条消息对应的是无法同时发生的错误。input11 + input12 = 10检查是在 submit 方法中进行的,该方法仅在表单中不存在剩余错误时才会执行。当它执行时,input11 组件已经过验证,且其 form.input11 模型已接收其值。 第 5 行的提示信息将不再显示。反之,如果第 5 行的提示信息显示出来,则表示表单中至少还存在一个错误,submit* 方法将不会执行。第 6 行的提示信息也不会显示。为了确保这两条可能出现的错误提示信息位于表格的同一列中,它们已被归入 <h:panelGroup*> 标签内(第 4 行和第 7 行)。

提交方法如下:


  // actions
  public String submit() {
    // latest validations
    validateForm();
    // we return the same form
    return null;
  }
 
  // global validations
  private void validateForm() {
    if ((saisie11 + saisie12) != 10) {
      // global msg
      FacesMessage message = Messages.getMessage(null, "saisies11et12.incorrectes", null);
      message.setSeverity(FacesMessage.SEVERITY_ERROR);
      FacesContext context = FacesContext.getCurrentInstance();
      context.addMessage(null, message);
      // field-related msg
      message = Messages.getMessage(null, "error.sign", null);
      setErrorSaisie11(message.getSummary());
      setErrorSaisie12(message.getSummary());
    } else {
      setErrorSaisie11("");
      setErrorSaisie12("");
    }
}
  • 第 4 行:submit 方法调用 validateForm 方法来执行最终验证,
  • 第 11 行:检查 input11 + input12 是否等于 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] 列,模型已接收到了提交的值,这表明提交值与模型之间的所有验证和转换操作均已成功。因此,处理 [Validate] 按钮点击事件的 form.submit 事件处理程序得以执行。正是该处理程序生成了显示在 [2] 和 [3] 中的消息。 我们可以看到,尽管表单被拒绝并退回给客户端,模型仍被更新了。在这种情况下,您可能希望模型不要被更新。事实上,如果用户通过 [取消] 按钮 [4] 取消了更新,除非您已保存模型,否则将无法恢复到初始模型。

2.8.5.9. 在不验证输入的情况下提交表单

考虑上面的表单,假设用户没有意识到自己的错误,想要放弃表单提交。他们将使用由以下 JSF 代码生成的 [Cancel] 按钮:


<!-- boutons de commande -->
      <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

与 [取消] 按钮关联的 form.cancel 方法仅在表单有效时才会执行。这正是我们之前针对与 [提交] 按钮关联的 form.submit 方法所演示的内容。 如果用户想要取消表单提交,当然无需检查其输入内容的有效性。通过使用 immediate="true" 属性即可实现这一效果,该属性指示 JSF 在不经过验证和转换阶段的情况下直接执行 form.cancel 方法。让我们回到 JSF POST 处理循环:

带有 immediate="true" 属性的 <h:commandButton> 和 <h:commandLink> 操作组件的事件在 [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;
}

如果您点击上一表单中的 [取消] 按钮,将显示以下页面:

  • 表单再次显示是因为 form.cancel 事件处理程序将导航键设置为 null。因此返回了 [index.xhtml] 页面,
  • 而 [Form.java] 模型已被 form.cancel 方法修改。这体现在显示该模型的第 [2] 列中,
  • 而第 [3] 列则反映了组件的提交值。

让我们回到 saisie1 组件 [4] 的 JSF 代码;


          <!-- ligne 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 模型相关联。这会产生以下几个结果:

  • 在向 [index.xhtml] 发送 GET 请求时,saisie1 组件将显示 form.saisie1 模型的值
  • 在向 [index.xhtml] 发送 POST 请求时,只有当所有表单验证和转换均成功时,saisie1 组件提交的值才会被赋值给 form.saisie1 模型。 无论 `form.saisie1` 模型是否已被提交的值更新,只要在 POST 请求后提交表单,组件显示的将是提交的值,而非与其关联的模型值。如上图所示,第 [2] 和 [3] 列的值并不相同。

2.9. 示例 mv-jsf2-07:与 JSF 组件状态变化相关的事件

2.9.1. 该应用程序

该应用程序演示了一个不使用按钮或链接执行 POST 请求的示例。表单如下:

下拉列表2 [2] 的内容与下拉列表1 [1] 中选中的项目相关联。当 [1] 中的选择发生变化时,系统会发送一个 POST 请求,在此过程中,下拉列表2 的内容会更新以反映 [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">
        <!-- headers -->
        <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"/>
        <!-- line 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}"/>
        <!-- line 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}"/>
        <!-- line 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>
      <!-- control buttons -->
      <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 请求已发送。尽管输入 [3] 存在错误,combo2 [2] 的内容仍已更新,以匹配 combo1 [1] 中选中的项目。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] 的作用域设置为请求


package forms;
 
...
 
@ManagedBean
@RequestScoped
public class Form {

第 6 行:我们将 Bean 的作用域设置为请求

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() {
  }
  
// form fields
  private String combo1="A";
  private String combo2="A1";
  private Integer saisie1=0;
  
  // working fields
  final private String[] combo1Labels={"A","B","C"};
  private String combo1Label="A";
  private static final Logger logger=Logger.getLogger("forms.Form");
  
  // methods
  public SelectItem[] getCombo1Items(){
    // init 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(){
    // init combo2 as a function of combo1
    SelectItem[] combo2Items=new SelectItem[5];
    for(int i=1;i<=combo2Items.length;i++){
      combo2Items[i-1]=new SelectItem(combo1Label+i,combo1Label+i);
    }
    return combo2Items;
  }
  
  // listeners
  public void combo1ChangeListener(ValueChangeEvent event){
    // follow-up
    logger.info("combo1ChangeListener");
    // retrieve the posted value of combo1
    combo1Label=(String)event.getNewValue();
    // we return the answer because we want to short-circuit the validations
    FacesContext.getCurrentInstance().renderResponse();
  }
  
  public String raz(){
    // follow-up
    logger.info("raz");
    // raz du formulaire
    combo1Label="A";
    combo1="A";
    combo2="A1";
    saisie1=0;
    return null;
  }
  
// getters - setters
  ...
}

让我们将 [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

此处,该组件是类型为 UISelectOnecombo1 下拉列表。其值类型为 String

  • Java 模型的第 51 行:combo1 的新值存储在 combo1Label 中,该值用于生成 combo2 列表中的项目,
  • 第 53 行:发送响应。此处需特别注意,combo1ChangeListener 处理程序是在 immediate="true" 属性下执行的。因此,它会在页面组件树( )使用提交的值更新之后但在对提交值进行验证之前执行。然而,我们需要绕过此验证过程,因为即使表单的其他位置仍存在无效条目,combo2 下拉列表也必须被更新。 因此,我们要求立即发送响应,而不经过输入验证阶段。
  • 表单将按用户输入的原始状态原样提交。但 combo1 combo2 列表中的项目并非提交值,而是通过调用 getCombo1Items getCombo2Items 方法重新生成的。后者将使用 combo1ChangeListener 设置的 combo1Label 新值,从而更新 combo2 列表中的项目。

2.9.6. [ 重置]按钮

我们希望 [重置] 按钮能将表单恢复到初始状态,如下所示:

在[1]中,[Raz]按钮前的表单被提交;在[2]中,显示POST操作的结果。

虽然功能上很简单,但处理这个用例却相当复杂。我们可以尝试各种解决方案,包括前一个示例中用于 [Cancel] 按钮的那种:


       <h:commandButton value="#{msg['raz']}" immediate="true" action="#{form.raz}"/>

其中 form.raz 方法如下:


  public String raz(){
    // raz du formulaire
    combo1Label="A";
    combo1="A";
    combo2="A1";
    saisie1=0;
    return null;
}

点击前一个示例中的 [清除] 按钮后,结果如下:

第 [1] 列显示已执行 form.raz 方法。然而,第 [1] 列仍显示提交的值:

  • 对于 combo1,提交的值为“B”。因此,列表中选中了该项;
  • 对于 combo2,提交的值为 "B"。执行 form.raz,combo2 的元素 {"B1", ..., "B5"} 已更改为 {"A1", ..., "A5"}。元素 "B5" 不再存在,因此无法被选中。随后显示列表中的第一个元素,
  • 对于 input1,提交的值为 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['form'].submit();
  }
</script>
...
<h:commandButton value="#{msg['raz']}" onclick='raz()' immediate="true" action="#{form.raz}"/>
  • 第 10 行:onclick='raz()' 属性指示浏览器在用户点击 [重置] 按钮时执行 JavaScript 函数 raz
  • 第 3 行:将值“A”赋给名为 'form:combo1' 的 HTML 元素。第 3 行中的各项说明如下:
    • document:浏览器显示的页面,
    • document.forms:文档中的所有表单,
    • document.forms['form']: 具有 name="form" 属性的表单,
    • document.forms['form'].elements:具有 name="form" 属性的表单中的所有元素,
    • document.forms['form'].elements['form:combo1']: 名称属性设置为 "form:combo1" 的表单元素
    • document.forms['form'].elements['form:combo1'].value名称为 "form: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。

完成上述操作后,与任何 <h:commandButton> 类型按钮关联的表单 POST 操作(第 10 行)将触发。form.raz 方法将被执行,表单将按提交时的状态返回。随后我们将得到以下结果:

这一结果背后隐藏着许多细节。来自 combo1combo2saisie1 组件的值“A”、“A1”和“0”会被提交至服务器。假设 combo1 的先前值为“B”。此时 combo1 组件的值发生了变化,form.combo1ChangeListener 方法也应被执行。 我们有两个带有 immediate="true" 属性的事件处理程序。它们都会被执行吗?如果是,顺序如何?还是只执行一个?如果是,哪个会被执行?

为了进一步了解情况,我们在应用程序中创建日志:


package forms;
 
import java.util.logging.Logger;
...
public class Form {
  
...  
// form fields
  private String combo1="A";
  private String combo2="A1";
  private Integer saisie1=0;
  
  // working fields
  final private String[] combo1Labels={"A","B","C"};
  private String combo1Label="A";
  private static final Logger logger=Logger.getLogger("forms.Form");
  
  // listener
  public void combo1ChangeListener(ValueChangeEvent event){
    // follow-up
    logger.info("combo1ChangeListener");
    // retrieve the posted value of combo1
    combo1Label=(String)event.getNewValue();
    // we return the answer because we want to short-circuit the validations
    FacesContext.getCurrentInstance().renderResponse();
  }
  
  public String raz(){
    // follow-up
    logger.info("raz");
    // raz du formulaire
    combo1Label="A";
    combo1="A";
    combo2="A1";
    saisie1=0;
    return null;
  }
...
}
  • 第 16 行:创建了一个日志器。getLogger 参数允许我们区分日志的来源。此处,日志器名为 forms.Form,
  • 第 21 行:记录了对 combo1ChangeListener 方法的调用,
  • 第 30 行:记录进入 raz 方法的日志。

点击 [清除] 按钮或 combo1 的值发生变化时,会生成哪些日志?让我们来考虑各种情况:

  • combo1 中选中的项为“A”时,我们点击 [Clear] 按钮。“A”因此是 combo1 组件的最后一个值。我们已经看到,[Clear] 按钮会执行一个 JavaScript 函数,该函数会为 combo1 组件提交值“A”。因此,combo1 组件的值不会发生改变。日志随后显示,仅执行了 form.raz 方法:
  
  • combo1 中选中的项目不是“A”时,我们点击 [Clear] 按钮。因此,combo1 组件会更改其值:其上一个值不是“A”,而 [Clear] 按钮会将其设置为“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<Person> people;

[Person] 类的定义如下:


package forms;
 
public class Personne {
  // data
  private int id;
  private String nom;
  private String prénom;
  
  // manufacturers
  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 and setters
...
}

让我们回到 <h:dataTable> 标签的内容:


<h:dataTable value="#{form.personnes}" var="personne" headerClass="headers" columnClasses="id,nom,prenom">
...
</h:dataTable>
  • var="person" 属性用于设置 <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 行:表中的名字列。

现在,让我们来看看 [Remove] 链接列:

该列由以下代码生成:


<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>

[Remove] 链接由第 4–6 行生成。点击该链接时,将执行 [Form].removePerson 方法。现在是时候查看 [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 {
 
  // model
  private List<Personne> personnes;
  private int personneId;
 
  // manufacturer
  public Form() {
    // initialization of the list of persons
    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() {
    // search for the selected person
    int i = 0;
    for (Personne personne : personnes) {
      // current person = selected person?
      if (personne.getId() == personneId) {
        // delete the current person from the list
        personnes.remove(i);
        // we're done
        break;
      } else {
        // next person
        i++;
      }
    }
    // we test on the same page
    return null;
  }
  
  // getters and setters
...
}
  • 第 18–24 行:构造函数初始化了第 14 行中的人员列表,
  • 第10行:由于该列表必须在请求之间保持持久化,因此该Bean的作用域为会话。

当第26行的[removePerson]方法被调用时,第15行的字段已初始化为点击了[Remove]链接的那个人的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 等于 *personId* 的用户。

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> 标签定义了一个名为content”的区域。这是一个可以容纳可变内容的区域。我们将看到具体实现方式。当我们请求页面 [layout.xhtml] 时,名为“content”的区域尚未定义内容。在这种情况下,将使用第 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 Tool Suite 测试 Maven 项目:

  • 在 [1] 中,点击按钮 [3] 导入 Maven 项目 [2]。这里,我们使用适用于 Eclipse 的 Maven 项目 [mv-jsf2-09]
  • 在 [4] 中,导入的项目已被正确识别为 Maven 项目 [5],
  • 在[6]中,该项目被导入到项目资源管理器中,
  • 在[7]中,我们在Tomcat[8]服务器[9]上运行它,
  • 在[10]中,Tomcat 7已启动,
  • 在[11]中,[mv-jsf2-09]项目的首页[11]在Eclipse内的浏览器中显示。