内容


Facelets 非常适合 JSF

最后,专为 JSF 设计的视图技术!

Comments

由于最近在 Java™ 服务器外观(JSF)项目上工作,我很有幸第一次使用了 Facelets。关于 Facelets,我最喜欢的是它让我可以创建可重用的复合组件。能够拿出一个页面(例如 JSP)并把它变成组件,对于我的 JSF 开发来说真是莫大的好处。我的结论是什么?如果不用 Facelets,那么就无法得到能从 JSF 获得的最大收获。

JSF 和 Java 服务器页面技术之间的不匹配,是 JSF 开发中的一个严重问题。问题是如何把 JSP 的动态内容集成到 JSF 基于组件的模型中。JSP 非常重视生成动态内容输出,而 JSF 需要 JSP 来协调组件模型的构建。因为这个任务超出了 JSP 原来的目的,所以产生了距离。

大多数 JSF 开发人员只是学会了一事一议地解决这类问题,但是这就像在锤子上放一个枕头,最终还会掉下来打伤脑袋。Facelets 是更加全面的解决方案:专为 JSF 组件模型度身定制的模板化语言。

Facelets 有以下吸引人的特性:

  • 模板化(像 Tiles)
  • 复合组件
  • 定制的逻辑标记
  • 表达式语言
  • 对设计师友好的页面开发
  • 创建组件库

这些特性比我想像的要更相关和统一。在这篇文章中,我讨论前两个:模板化和复合组件。我使用的 Web 应用程序基于为我的针对怀疑者的 JSF 系列开发的一个应用程序,我把它更新成使用 Facelets 视图而不是 Tiles。在进一步阅读之前,应当 下载示例代码。如果要随着讨论一起操作,还需要 安装 Facelets

Facelets 概述

对于 Facelets 可能会做的最大一个错误假设,就是它只是 Tiles 的替代品。Facelets 远不止如此:它是思考 JSF 的新方式。

JSP 是种生成 servlet 的模板化语言。JSP 的主体与 servlet 的 doGet()doPost() 方法等价(也就是说,成为 jspService() 方法)。JSF 定制标记(例如 f:viewh:form)只是调用 JSF 组件来呈现它们自己的当前状态。JSF 组件模型的生命周期独立于 JSP 生成的 servlet 的生命周期。这种独立性就是混淆的来源。

与 JSP 不同,Facelets 这个模板化语言,从构建之初,就考虑了 JSF 的组件生命周期。使用 Facelets,生成的模板会构建组件树,而不是 servlet。这就允许更好的重用,因为可以把组件组合成另一个组件。

Facelets 减少了编写定制标记才能使用 JSF 的需求。Facelets 本身就可以使用 JSF 定制组件。沟通 JSF 和 Facelets 只需要很少的特殊编码:要做的全部工作就是在 Facelet 标记库文件中声明 JSF 组件。在 Facelets 模板化语言中可以直接使用 JSF 组件,不用任何额外的开发。

Facelets 模板框架

在提供针对组件构建设计的模板框架方面,Facelets 与 Tapestry (请参阅 参考资料)类似。但是,对于具有 JSP 背景的我们来说,Facelets 看起来比 Tapestry 友好得多。它允许使用熟悉的 JSTL 样式的标记和 JSTL/JSF/JSP 样式的表达式语言。大大降低的学习曲线意味着可以更加迅速地开始开发。

Facelets 允许定义能够直接包含进页面或者容易地添加到 Facelet 标记库的组件集。实际上让人高兴的是在 Facelets 中定义定制标记(复合组件和类似 JSP 定制标记的标记)的迅速。使用这些组件集,Facelets 还允许定义站点模板(和更小的模板)。这与使用 Tiles 很相似,但是少了定义文件。也可以在定制 JSF 组件内部使用 Facelets,因为 Facelets API 提供了可以容易地与 JSF 组件集成的接口。

从 Tiles 到 Facelets

如前所述,在这里使用的示例 Web 应用程序基于为我的 针对怀疑者的 JSF 系列创建的示例。它为一家在线 CD 店管理库存,创建、读取、更新和删除(CRUD)清单。它包含一个表单,让用户向系统输入新 CD,有一个单选按钮列表,允许用户选择音乐分类。当用户选择了一个分类时,就触发某些 JavaScript 立即把表单提交回服务器。应用程序还包含一个 CD 清单,用户可以根据标题或艺术家对清单中的 CD 排序。图 1 是应用程序类的 UML 图表:

图 1. 在线 CD 商店示例的类图
示例应用程序类
示例应用程序类

图 2 提供了商店的 CD 清单页面:

图 2. 在线 CD 商店的清单页面
CD 清单
CD 清单

原来的应用程序从 Tiles 得到视图支持,现在我将用 Facelets 构建视图。我先从用 Facelets 替换示例中的 Tiles 支持开始,然后编写复合组件。在开始之前,需要已经安装了 Facelets。

安装 Facelets

安装 Facelets 的步骤很容易。请注意,我假设已经下载并安装了 示例应用程序

  1. 下载 Facelets 发行包 并解压缩。
  2. 把 jsf-facelets.jar 拷贝到 WEB-INF/lib 目录(在应用程序部署时,它最终必须放在 WEB-INF/lib 目录中)。
  3. 把 Facelet 初始化参数添加到 web.xml 文件中。
  4. 把 FaceletViewHandler 添加到 faces-config.xml 文件中。

步骤 1 和 2 比较基本。我将详细介绍其他两个步骤。

添加初始化参数

这一步假设已经安装了工作正常的 JSF 应用程序(例如 在线 CD 商店示例),然后编辑现有的 web.xml 页面,添加以下参数:

<context-param>
	<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
	<param-value>.xhtml</param-value>
</context-param>

这告诉 JSF 采用 xhtml 前缀,Facelet 渲染器能够解释这个前缀。

Facelets 有许多参数,请参阅 参考资料 获得完整清单。如果示例有问题,请参考 DEVELOPMENT init 参数,它适合调试。把 REFRESH_PERIOD 参数设置为 low 在开发期间会有帮助。

添加 FaceletViewHandler

要让 Facelets 模板生效,需要把 Facelets 视图处理器告诉 JSF。JSF ViewHandler 是个插件,为不同的响应生成技术(包括 Facelets)处理 JSF 请求处理生命周期的 “渲染器响应和恢复视图” 阶段。(任何认为 JSF 不能扩展的人都是被误导了!)通过添加以下视图处理器到 faces-config.xml 中,就把 Facelets 插进了 JSF 中:

  <application>
    <locale-config>
      <default-locale>en</default-locale>
    </locale-config>
	<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
  </application>

用 Facelets 进行模板化

首先介绍 Facelets 模板化框架,因为它相对容易理解。创建和使用 Facelets 模板的步骤如下:

  1. 创建 layout.xhtml 页面。
  2. 定义 Facelet 的命名空间,导入对 Facelets 的使用。
  3. ui:insert 标记定义页面的逻辑区域。
  4. 用纯文本和 ui:include 标记定义合理的默认值。

我要逐步介绍这些步骤,用在线 CD 商店清单页面作为我的布局示例。

步骤 1. 创建 layout.xhtml 页面

layout.xhtml 页面就是一个一般的 XHTML 文本文件,使用了以下文档类型声明:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
</html>

不要求进一步细节!

步骤 2. 定义 Facelets 的命名空间

为了用 Facelets 标记进行模板化,需要用 XML 命名空间像下面这样导入它们:

<!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:ui="http://java.sun.com/jsf/facelets">
...

请注意 ui 命名空间的定义。

步骤 3. 用 ui:insert 标记定义页面的逻辑区域

下面,定义布局的逻辑区域,例如页面标题、小标题、导航、内容等等。下面是定义页面标题的示例:

<!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:ui="http://java.sun.com/jsf/facelets">
<head>
  <title><ui:insert name="title">Default title</ui:insert></title>
  <link rel="stylesheet" type="text/css" href="./css/main.css"/>
</head>
...

请注意使用 ui:insert 标记定义了标题的逻辑区域。ui:insert 元素内的文本 “Default title” 定义了模板用户不传递标题时显示的文本。也可以像下面这样编写上面的内容:

<title>#{title}</title>

步骤 4. 用纯文本和 ui:includes 定义默认值

可以传递更多的纯文本作为默认值。例如,请研究 layout.xhtml 中的以下代码片段:

<div id="header">
    <ui:insert name="header">
    	<ui:include src="header.xhtml"/>
    </ui:insert>
</div>

在这里,我用了 ui:insert 标记定义逻辑区域,用 ui:include 标记插入默认值。默认情况下,使用布局的页面会采用 header.xhtml 的内容作为标题文本,但是因为标题是 ui:insert 定义的逻辑区域,所以用这个模板也能传递不同的标题。对于拥有前端(例如,带有购物车的目录)和后端管理(例如添加新产品)的应用程序,后端站点在标题或导航上可能不同的链接。ui:include 标记可以容易地用新标题换掉默认标题。

清单 1 显示了示例应用程序的清单页面 list.xhtml 的完整代码:

清单 1. 完整的 list.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets">
<head>
  <title><ui:insert name="title">Default title</ui:insert></title>
  <link rel="stylesheet" type="text/css" href="./css/main.css"/>
</head>
<body>
<div id="header">
    <ui:insert name="header">
    	<ui:include src="header.xhtml"/>
    </ui:insert>
</div>
<div id="left">
  <ui:insert name="navigation" >
    <ui:include src="navigation.xhtml"/>
  </ui:insert>
</div>
<div id="center">
  <br />
  <span class="titleText"> <ui:insert name="title" /> </span>
  <hr />
  <ui:insert name="content">
  	<div>
    <ui:include src="content.xhtml"/>  
    </div>
  </ui:insert>
</div>
<div id="right">
  <ui:insert name="news">
    <ui:include src="news.xhtml"/>
  </ui:insert>
</div>
<div id="footer">
  <ui:insert name="footer">
    <ui:include src="footer.xhtml"/>  
  </ui:insert>
</div>
</body>
</html>

现在已经知道了如何定义布局,我将介绍如何使用布局!

使用 Facelets 模板

为了调用模板,要使用 ui:composition 标记。为了把参数传递给模板,要使用 ui:define 标记,它是 ui:composition 标记的子元素。在清单 2 中,我调用了在线 CD 商店示例的布局页面:

清单 2. 调用布局页面
<!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:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
<ui:composition template="/WEB-INF/layout/layout.xhtml">
  <ui:define name="title">CD form</ui:define>
  <ui:define name="content">
	<!-- use the form tag to set up this form -->
	<h:form id="cdForm">
		...
		...
...
	</h:form>
  </ui:define>
</ui:composition>
</html>

请注意在上面的调用中包含了以下命名空间:

  • xmlns:h="http://java.sun.com/jsf/html"
  • xmlns:f="http://java.sun.com/jsf/core"

有了 Facelets,就不必依赖 JSF 标记库,所以要使用核心和 HTML JSF 组件,必须通过以上命名空间导入它们。

html 标记的使用看起来可能有些奇怪。毕竟,清单 2 所示的布局页面要调用的模板已经有了一个 html 标记;所以这是不是意味着会得到两个 html 标记?如果真的这样,那么在 ui:composition 标记之外的内容全部被忽略,所以 html 标记所做的只是让 HTML 编辑器能够看到 HTML 片段。它不会影响运行时行为。

位置是一切

当页面调用布局模板时,只需指定模板的位置,如下所示:

<ui:composition template="/WEB-INF/layout/layout.xhtml">

这个标记调用清单 1 所示的模板,所以我要做的全部工作就是把参数传递给模板。然后,在复合标记内部,可以传递像标题这样的简单文本:

  <ui:define name="title">CD form</ui:define>

或者传递整个组件树:

  <ui:define name="content">
	<!-- use the form tag to setup this form -->
	<h:form id="cdForm">
		...
		...
...
	</h:form>
  </ui:define>

请注意,在我定义和传递的许多逻辑区域中,cdForm.xhtml 只传递两个:内容和标题。

复合组件

如果只用 Facelets 定义和使用模板,那么可能会有点失败。虽然 Facelets 的模板化特性完整而且丰富,但是它没有 Tiles 之类的框架那么多的特性,后者还擅长定义默认值、相关模板的层次结构以及类似的东西。

但模板化不是 Facelets 真正出色的地方:Facelets 把它的精华放在复合组件上。(有趣的是,复合组件也给 Facelets 模板化带来了一些好处;例如,在 Facelets 中可以舍弃 f:verbatim 标记和各种 h:outputText 标记,因为所有的东西都被当成组件树中的组件。关于这方面的更多内容稍后介绍。)

对于这篇文章余下的部分,我将重点放在创建和使用复合组件的步骤上。但在开始之前,先要确保能够清楚地理解是什么让这些方便的小代码段这么棒。

破坏 DRY 原则

您是否曾经编写过像清单 3 所示的代码片段?

清单 3. 复合组件之前的生活
<h:dataTable id="items" value="#{CDManagerBean.cds}" var="cd"
	rowClasses="oddRow, evenRow" headerClass="tableHeader">
<!--  Title -->
<h:column>
	<f:facet name="header">
		<h:panelGroup>
			<h:outputText value="Title" />
				
                 <f:verbatim>[</f:verbatim>
			<h:commandLink styleClass="smallLink"
                              action="#{CDManagerBean.sort}">
				<h:outputText id="ascTitle" value="asc" />
				<f:param name="by" value="title"/>
				<f:param name="order" value="asc"/>
			</h:commandLink>
			<h:outputText value="," />
			<!-- Sort descending -->
			<h:commandLink styleClass="smallLink"
                         action="#{CDManagerBean.sort}">
				<h:outputText id="decTitle" value="dec" />
				<f:param name="by" value="title"/>
				<f:param name="order" value="dec"/>
			</h:commandLink>
			     <f:verbatim>]</f:verbatim>
		</h:panelGroup>
	</f:facet>
	<h:outputText value="#{cd.title}" />
</h:column>
<!--  Artist -->
<h:column>
	<f:facet name="header">
		<h:panelGroup>
			<h:outputText value="Artist" />
			   <f:verbatim>[</f:verbatim>
		      <h:commandLink styleClass="smallLink"
                           action="#{CDManagerBean.sort}">
			     <h:outputText id="ascArtist" value="asc" />
				<f:param name="by" value="artist"/>
				<f:param name="order" value="asc"/>
			   </h:commandLink>
			<h:outputText value="," />
					<!-- Sort descending -->
			<h:commandLink styleClass="smallLink"
  				           action="#{CDManagerBean.sort}">
				<h:outputText id="decArtist" value="dec" />
				<f:param name="by" value="artist"/>
				<f:param name="order" value="dec"/>
			</h:commandLink>
			     <f:verbatim>]</f:verbatim>
		</h:panelGroup>
	</f:facet>
	<h:outputText value="#{cd.artist}" />
</h:column>

这段来自 listing.xhtml 的代码为示例应用程序的清单页面生成列标题和升序/降序排列链接。请注意,必须在多个地方重复代码,才能输出多列。(在上面的示例中您还会注意到,我在 ${..}#{..} 之间切换;这可能让人迷惑,但它们做的是同样的事!)

所有这些渲染标题列和艺术家列的重复代码都破坏了 DRY 原则 —— 即,不要重复自己。那么您说,这里错在哪儿呢?假设在清单中平均有 5 列,应用程序中有 20 个不同的清单。使用清单 3 的方法,就不得不重复这 35 行代码 100 次,合计 3,500 行代码!维护所有这些代码会是种痛苦,但是如果决定修改清单的表示或添加一种通用的清单过滤方式,会怎么样?需要更多、更多的工作。

现在拿清单 3 和这个代码比较:

清单 4. 创建字段的新方式
<h:dataTable id="items" value="#{CDManagerBean.cds}" var="cd"
		rowClasses="oddRow, evenRow" headerClass="tableHeader">
			
 <a:column entity="${cd}" fieldName="title" backingBean="${CDManagerBean}"/>
 <a:column entity="${cd}" fieldName="artist" backingBean="${CDManagerBean}"/>

看起来好像我只用 4 行代码就替代了 70 行或更多行代码!可以猜出,a:column 是个复合组件。在 Facelets 中,可以容易地定义这样的组件,如清单 5 所示:

清单 5. column.xhtml 渲染带有排序链接的列
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:z="http://www.qualcomm.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core"
      xmlns:fn="http://java.sun.com/jsp/jstl/functions">
THIS TEXT WILL BE REMOVED
<ui:composition>
	<!--  The label attribute is optional. Generate it if it is missing. -->
	<c:if test="${empty label}">
	    <c:set var="label" value="${fieldName}" />
	</c:if>
	<!--  The sort attribute is optional. Set it to true if it is missing. -->
	<c:if test="${empty sort}">
	    <c:set var="sort" value="${true}" />
	</c:if>
	<h:column>
	  <f:facet name="header">
	    <h:panelGroup>
	      ${label} 
	      <c:if test="${sort}">
	          [
	        <h:commandLink styleClass="smallLink"
	      action="#{backingBean.sort}">
	          <h:outputText value="asc" />
	          <f:param name="by" 
              value="${fieldName}"/>
	          <f:param name="order" value="asc"/>
	        </h:commandLink>
					,
	        <!-- Sort descending -->
	        <h:commandLink styleClass="smallLink"
              action="#{backingBean.sort}">	
	          <h:outputText value="asc" />
	          <f:param name="by"                
              value="${fieldName}"/>
	          <f:param name="order" value="dec"/>
	        </h:commandLink>
					]
	      </c:if>			
	    </h:panelGroup>
	  </f:facet>
	  <!--  Display the field name -->
	  <h:outputText value="${entity[fieldName]}"/>
        </h:column>
</ui:composition>
THIS TEXT WILL BE REMOVED AS WELL
</html>

要点

在进入更高级的示例之前,我想把您的注意力引到几件事上。首先,请注意在清单 5 中,我如何用一种通用方式引用值绑定:

<h:outputText value="${entity[fieldName]}"/>

第二,当调用这个复合组件时,我把 entityfieldName 作为属性传递,如下所示:

<a:column entity="${cd}" fieldName="title" backingBean="${CDManagerBean}"/>

Facelets 使用的 EL 规范允许用圆点(.)表示法或较少使用的映射表示法引用字段。例如,如果像上面那样调用,那么 ${entity[fieldName]} 等价于 CDManager.title。还请注意,我不需要 f:verbatim 标记或辅助的 h:outputText。对于您编写的任何 Facelets 页面,都可以这样。Facelets 知道 JSF 组件树,它的唯一目的就是构建这个组件树。这是使用 Facelets 与使用 JSP 和 Tiles 相比的另一个优势。

编写完成之后,就可以在其他许多地方使用 column.xhtml 复合组件。作为一个通用规则:如果正在破坏 DRY 原则,那么请考虑改用复合组件。

创建组件

现在通过 column.xhtml 示例已经快速查看了复合组件。下面一步一步地介绍创建复合组件的过程。以下是创建复合组件的步骤:

  1. 创建 Facelets 标记库。
  2. 在 web.xml 中声明标记库。
  3. 用命名空间导入标记文件。

步骤 1. 创建 Facelets 标记文件

标记文件 是符合 facelet_taglib_1_0.dtd 的文件。在概念上它与 JSP 中的 TLD 文件相似。清单 6 是一个示例标记库文件:

清单 6. 标记库文件 —— arcmind.taglib.xml
<?xml version="1.0"?>
<!DOCTYPE facelet-taglib PUBLIC
  "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
  "facelet-taglib_1_0.dtd">
<facelet-taglib>
      <namespace>http://www.arc-mind.com/jsf</namespace>
	<tag>
		<tag-name>field</tag-name>
		<source>field.xhtml</source>
	</tag>
	<tag>
		<tag-name>column</tag-name>
		<source>column.xhtml</source>
	</tag>
	<tag>
		<tag-name>columnCommand</tag-name>
		<source>columnCommand.xhtml</source>
	</tag>
</facelet-taglib>

arcmind.taglib.xml 文件声明了三个标记:fieldcolumn(已经看过这个!)和 columnCommand。需要做的只是用 tag-name 指定标记的名称和实现文件的位置。实现文件的名称是相对的。可以在示例 Web 应用程序下的 WEB-INF\facelets\tags 文件中找到所有这些代码,包括 DTD。

请一定注意在上面的标记元素之前声明的 namespace 元素:稍后需要通过它在其他 Facelets 页面中使用这个标记库。

步骤 2. 在 web.xml 中声明标记库

有了一个标记库是很好,但是要让它有用,还必须把它的存在告诉 Facelets。在 web.xml 文件中用 facelets.LIBRARIES init 参数做这件事,如下所示:

<context-param>
	<param-name>facelets.LIBRARIES</param-name>
	<param-value>
		/WEB-INF/facelets/tags/arcmind.taglib.xml
	</param-value>
</context-param>

facelets.LIBRARIES 以分号分隔的列表形式传递,就可以想定义多少就定义多少标记文件。

步骤 3. 用命名空间导入标记文件

创建了标记文件并在 Facelets 标记库中定义了它之后,就可以使用它了。标记文件的使用要求把它声明为 XML 命名空间,如下所示:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:a="http://www.arc-mind.com/jsf">
...
...
<a:column entity="${cd}" fieldName="title"  
  backingBean="${CDManagerBean}"/>
<a:column entity="${cd}" fieldName="artist" 
  backingBean="${CDManagerBean}"/>
<a:column entity="${cd}" fieldName="price" 
  backingBean="${CDManagerBean}" sort="${false}"/>
<a:columnCommand label="Edit" action="editCD"
              backingBean="${CDManagerBean}"/>

请注意命名空间的定义如下所示:

xmlns:a="http://www.arc-mind.com/jsf"

命名空间的值与前面步骤 1 中标记库中声明的命名空间元素一样。

高级技巧

上面只介绍了复合组件的基础知识。用我目前为止介绍的内容能够创建可重用组件。在我自己使用 Facelets 时,发现了一些可以让复合组件更有用的小技巧,而且在某些情况下能够解决一些小问题。例如,请考虑来自 cdForm.xhtml 模板的以下代码片段:

清单 7. cdForm.xhtml 的片段
<h:form id="cdForm">
  <h:inputHidden id="cdid" value="#{CDManagerBean.cd.id}" />
  <h:panelGrid id="formGrid" columns="3" rowClasses="row1, row2">
	<!-- Title                                   -->
	<h:outputLabel id="titleLabel" for="title" styleClass="label"
                     value="Title" />
	<h:inputText id="title" value="#{CDManagerBean.cd.title}"
				     required="true" />
	<h:message id="titleMessage" for="title" styleClass="errorText"/>
	<!-- Artist                                   -->
	<h:outputLabel  id="artistLabel" for="artist" styleClass="label" 
                     value="Artist" />
	<h:inputText id="artist" value="#{CDManagerBean.cd.artist}"
				     required="true" />
	<h:message id="titleMessage" for="artist"  
                     styleClass="errorText"/>
	<!-- Price                                   -->
	<h:outputLabel  id="priceLabel" for="price" styleClass="label" value="Price" />
	<h:inputText id="price" value="#{CDManagerBean.cd.price}"
				     required="true">
		<f:validateDoubleRange minimum="15.0" maximum="100.0" />
	</h:inputText>
	<h:message id="priceMessage" for="price" styleClass="errorText"/>

以上页面在概念上与 清单 3 类似,但它为 Facelets 和一个字段复合组件留出了空间,可以避免重复代码。基于这个代码,应当可以容易地创建显示字段的复合组件,但是有一个小麻烦。不知道您看到没有?请看表单的 price 字段:它包含一个验证器。

现在,如何把验证器传递给复合组件?

传递子元素

这里有一个关于 Facelets 的小秘密:复合组件基本上也是一类模板。所以,可以用 ui:define 标记传递模板参数,带上具体的 ui:insert,或者也可以把体作为默认 ui:insert 传递。

清单 8 就是字段组件的这样一种实现(field.xhtml):

清单 8. field.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:z="http://www.qualcomm.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core"
      xmlns:fn="http://java.sun.com/jsp/jstl/functions"
      xmlns:t="http://myfaces.apache.org/tomahawk">
THIS TEXT WILL BE REMOVED
<ui:composition>
	      <!--  The label is optional. 
                Generate it if it is missing. -->
		<c:if test="${empty label}">
			<c:set var="label" value="${fieldName}" />
		</c:if>
	 
	 	<!-- The required attribute is optional, 
                 initialize it to true if not found. -->
		<c:if test="${empty required}">
			<c:set var="required" value="true" />
		</c:if>
	
		<h:outputLabel id="${fieldName}Label" 
                      value="${label}" for="#{fieldName}" />			
		<h:inputText id="#{fieldName}" value="#{entity[fieldName]}" 
			             required="${required}">
			  <ui:insert />
		</h:inputText>
		<!--  Display any error message that are found -->
		<h:message id="${fieldName}Message" 
			style="color: red; text-decoration: overline" 
			for="#{fieldName}" />
</ui:composition>
THIS TEXT WILL BE REMOVED AS WELL
</html>

目前为止,清单 8 的工作基本上是不出所料。请注意 h:inputText 内部未命名 ui:insert 标记的使用。理解了它之后,就可以像清单 9 所示那样使用这个复合组件:

清单 9. 字段标记复合组件
<!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:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:a="http://www.arc-mind.com/jsf">
...
<h:form id="cdForm">
	<!-- Title, Artist, Price -->
	<a:field fieldName="title" entity="#{CDManagerBean.cd}" />
	<a:field fieldName="artist" entity="#{CDManagerBean.cd}" />
	<a:field fieldName="price" entity="#{CDManagerBean.cd}" >
		<f:validateDoubleRange minimum="15.0" maximum="100.0" />
	</a:field>
...

price 的字段标记被传递给验证器,作为匿名插入。因为其他字段没有定义体,所以匿名插入对于默认值没有影响。

传递动作

在想传递动作绑定来创建像工具栏或导航列表这样的元素时,问题就是使用标准的表达式语言,不能传递,但是有方法可以做到!使用从对象中引用字段的相同方式,可以引用对象中的方法。所以,要创建可以创建动作绑定的组件,可以像下面这样做(来自 columnCommand.xhtml):

<h:commandLink id="#{action}" value="#{label}"  
                              action="#{backingBean[action]}"/>

请研究动作属性的 value。请注意,我使用与前面从实体引用字段相同的方式,访问到了方法。可以用以下语法调用这个组件:

<a:columnCommand label="Edit" action="editCD" 
backingBean="${CDManagerBean}"/>

这个调用把 CDManagerBeaneditCD() 方法绑定到链接。清单 10 显示了 columnCommand.xhtml 的完整清单:

清单 10. columnCommand.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:z="http://www.qualcomm.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core">
THIS TEXT WILL BE REMOVED
<ui:composition>
	<!--  The label is optional. Generate it if it is missing. -->
	<c:if test="${empty label}">
			<c:set var="label" value="${action}" />
	</c:if>
	
	<h:column>
		<f:facet name="header">
			<h:panelGroup>
				<h:outputText value="#{label}" />
			</h:panelGroup>
		</f:facet>
		<h:commandLink id="#{action}" value="#{label}" 
                                     action="#{backingBean[action]}"/>
	</h:column>
		
</ui:composition>
THIS TEXT WILL BE REMOVED AS WELL
</html>

Facelets 的不足

我已经清楚地演示了使用 Facelets 的好处:即组件复合和模板框架,它的核心 是组件,而不是 Servlets 输出。但是采用 Facelets 也有些不足。其中之一就是,对 Facelets 的 IDE 支持极少。只有一个 Eclipse IDE 实现完全支持 Facelets(商业版的实现,请参阅 参考资料),而且看起来还不支持代码补足。

而且也没对 Facelets 调试的 IDE 支持(也就是说,设置断点之类的东西)。要想调试,需要阅读 Facelets 手册,打开 JDK 1.4 样式的日志,根据开发情况设置它的 init 参数。

在有利方面来说,我发现使用 Facelets API 非常自然和直观。调试在开始的时候有些怪异,但是后来就适应了。随 Facelets 发行包提供的演示应用程序没有定制标记或功能的示例,但是核心项目代码中有,所以请用核心项目代码作为指导。

如果要使用新的 JSF 组件库,必须有公开这个库的 Facelets 标记库。有些主要的组件库(例如 Oracle 和 Tomahawk)的标记库存在,但是即使这些也需要调整。我必须调整 Tomahawk 标记库才能在应用程序中得到 Tomahawk 的日历组件。虽然编写导出组件的标记库文件比较容易,但是也是件麻烦事。如果想使用新的定制组件库,就必须编写标记库文件。

因为在其他实现中的问题,Facelets 看起来只能用于 MyFaces 1.1.1 和 Sun 1.2 JSF 的参考实现(Sun 的 JSF RI 1.2 还没有正式发布)。不能把 Facelets 用于 1.1 RI。虽然可以把 MyFaces 用于 IBM WebSphere,但不能把 Facelets 用于 IBM 的实现。(如果使用最新版本的 Facelets,必须使用最新构建的 MyFaces 1.1.2,它现在还没推出。)

还要注意的是,MyFaces 1.1 和 JSF RI 1.2 的底层机制不同。尽管如此,Facelets 试图把这两者的实现保持为它们当前的形式(MyFaces 1.1.2 和 JSF RI 1.2),这看起来解释了花在 Facelets 上的大量时间。如果双方更团结协调一点,让 Facelets 在两个环境中做同样的事上少花些时间,就可以把更多时间花在改进 Facelets 上。

结束语

虽然有些缺点,我还是强烈推荐您下载 Facelets 并尽快开始使用它。Facelets 是 JSF 的未来,或者将会是,使用它可以使您在任何 JSF 暴风雨中都干爽(DRY 的双关语,译者注)。如果还不能在当前项目中使用它,请在下一个项目中想着它。

我已经用 Facelets 创建了复合组件、定制 Facelet 标记和内部 CRUD 框架功能的整个库。我发现了许多构建复合组件的技巧和技术(例如,自动生成复选框、日历组件的字段标记,或者根据绑定到组件的值绑定类型的文本字段),超过了在这样一篇介绍性文章中能涉及的内容。相反,我把重点放在让您了解和运行复合组件。通过在这里学到的知识,只用最少的定制功能和定制 Facelets 标记,就可以创建动人的组件。

致谢

特别感谢 Jacob Hookom,他是 Facelets 的创造者,感谢他对本文的审阅和贡献,还要感谢 Athenz 细致入微的编辑。


下载资源


相关主题


评论

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=107086
ArticleTitle=Facelets 非常适合 JSF
publish-date=02212006