JSF 2 简介,第 2 部分: 模板及复合组件
 

2009-07-23 作者:David Geary 来源:ibm

 
本文内容包括:
模板和复合组件是 Java™Server Faces (JSF) 2 的两个功能强大的特性,借助这两个特性,您就可以实现易于修改和扩展的用户界面。在本文 — 共三部分的 系列文章 的第 2 部分 — 中,JSF 2 专家组成员 David Geary 将向您展示如何在您的 Web 应用程序中利用模板和复合组件。

早在 2000,当我还是 JavaServer Pages(JSP)邮件列表中的一个活跃分子的时候,我遇到了 Craig McClanahan,当时他正忙着开发一个新的 Web 框架,称为 Struts。在那时,我还正在从 Swing 转向服务器端 Java 编程,所以我已经实现了一个很小的框架来分离 JSP 视图布局及其内容,这非常类似于 Swing 布局管理器的理念。Craig 问我,是否愿意将我的模板 库包含在 Struts 内,我欣然同意了。这样一来,与 Struts 1.0 捆绑的 Struts Template Library 遂成为了 Struts 流行的 Tiles 库的基础,而 Tiles 库最终成为了一个顶级的 Apache 框架。

JSF 2 现在的默认显示技术 — Facelets — 就是一个模板框架,在很大程度上基于的是 Tiles。JSF 2 还提供了一个功能强大的机制,称为复合组件,该机制构建在 Facelets 的模板特性之上,因此,在无需任何 Java 代码和 XML 配置的情况下就可以实现定制组件。在本文中,我将向您介绍模板和复合组件,并且还会给出如何充分利用 JSF 2 的三个技巧:

  • 技巧 1:遵守 DRY 原则
  • 技巧 2:使用组合的方式
  • 技巧 3:牢记 LEGO 拼装玩具的理念

技巧 1:遵守 DRY 原则

在我作为软件开发人员从事的第一项工作中,我的任务是为基于 UNIX® 的计算机辅助设计和计算机辅助制造(CAD/CAM)系统实现一个 GUI。

最初,一切进行顺利,但是一段时间后,我的代码开始问题不断。待到代码发布的时候,系统已经相当脆弱,我甚至都害怕修复 bug,而这次的代码发布自然也伴随着一连串的 bug 报告。

如果我在这个项目中遵循了 DRY 原则 — 不重复自己(Don't Repeat Yourself),我本可以让自己不至于这么悲惨。DRY 原则最初由 Dave Thomas 和 Andy Huntprinciple 提出(参见 参考资料),它要求:

每条知识都必须在系统内具有一个单一、清晰和权威的表示。

我的 CAD/CAM 应用程序并不符合 DRY 原则 — 它具有太多关注点之间的交叉 — 因此在一个地方所做的更改常常会在其他地方引起意想不到的更改。

JSF 1 在几个方面违背了 DRY 原则,比如,它强迫您提供托管 beans 的两种表示 — 一个使用 XML,一个使用 Java 代码。对多重表示的需求让创建和更改托管 bean 更加困难。正如我在本系列 第 1 部分 中介绍的,JSF 2 让您能够使用注释取代 XML 来配置托管 bean,这样一来,托管 bean 就具有了一个单一、权威的表示。

除托管 beans 之外,就连一些看似有益的实践 — 比如在所有视图中包括相同的样式表 — 也违背了 DRY 原则,并会导致混乱。比如,如果要更改样式表的名字,就必须更改多个视图。如果可能,最好是封装此样式表包含。

DRY 原则同样适用于代码设计。如果多个方法均包含遍历树的代码,一种好的做法是(比如在一个子类中)封装遍历树的算法。

在实现 UI 时,因大多数更改均在开发过程中发生,所以遵守 DRY 原则尤其重要。

JSF 2 模板

JSF 2 在很多方面都支持 DRY 原则,其中之一就是通过模板。模板能够封装在应用程序视图中十分常见的功能,因此该功能只需被指定一次。在 JSF 2 应用程序中,一个模板可供多个组装(compositions)用于创建视图。

我在 第 1 部分 中所介绍的 places 应用程序具有三个视图,如图 1 所示:

图 1. places 应用程序的视图:Login、source viewer 和 places

places 应用程序的视图

places 应用程序的图标

places 应用程序的图标

与很多 Web 应用程序一样,这个 places 应用程序包含多个具有相同布局的视图。JSF 模板功能让您能够在一个模板内封装该布局 — 及其他共享工件,比如 JavaScript 和 Cascading Style Sheets(CSS)。清单 1 是 图 1 中所示的这三个视图的模板:

清单 1. places 模板:/templates/masterLayout.xhtml
 
				
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

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

  <h:head>
    <title>
      <ui:insert name="windowTitle">
        #{msgs.placesWindowTitle}
      </ui:insert>
    </title> 
  </h:head>
  
  <h:body>  
    <h:outputScript library="javascript" name="util.js" target="head"/>      
    <h:outputStylesheet library="css" name="styles.css" target="body"/>	    
    
    <div class="pageHeading">
      <ui:insert name="heading">
        #{msgs.placesHeading}
      </ui:insert>     
    </div> 
      
    <div class="menuAndContent"> 
      <div class="menuLeft"> 
        <ui:insert name="menuLeft"/>
      </div>    
	    
      <div class="content" style="display: #{places.showContent}">
        <ui:insert name="content"/>
      </div> 
	    
      <div class="menuRight">
        <ui:insert name="menuRight">
          <ui:include src="/sections/shared/sourceViewer.xhtml"/>
        </ui:insert>
      </div> 
    </div>  
  </h:body> 
</html>

清单 1 中的模板为此应用程序的所有视图提供了如下的基础设施:

  • HTML <head><body> <title>
  • 一个默认标题(可由使用此模板的那些组装覆盖)
  • 一个 CSS 样式表
  • 某些实用 JavaScript
  • 一个布局,格式为 <div>,以及对应的 CSS 类
  • 头的默认内容(可被覆盖)
  • 右菜单的默认内容(可被覆盖)

正如 清单 1 所示,模板通过 <ui:insert> 标记将内容插入到布局中。

如为 <ui:insert> 标记指定了主体,正如我在 清单 1 中为窗口标题、头和右菜单所做的,JSF 会将此标记的主体作为默认内容。借助 <ui:define> 标记,使用此模板的那些封装可以定义内容或者覆盖默认内容,如清单 2 所示,它给出了 login 视图的标记:

清单 2. login 视图
 
				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
   xmlns:ui="http://java.sun.com/jsf/facelets"
   template="/templates/masterLayout.xhtml">
   
  <ui:define name="menuLeft">
    <ui:include src="/sections/login/menuLeft.xhtml"/>
  </ui:define>

  <ui:define name="content"> 
    <ui:include src="/sections/login/content.xhtml"/>           
  </ui:define>
     
</ui:composition>

这个 login 视图为窗口的标题、头和右菜单使用了模板的默认内容。它只定义了特定于此 login 视图的功能:内容部分和左菜单。

通过为窗口标题、头或右菜单提供 <ui:define> 标记,我也可以覆盖此模板的默认内容。比如,清单 3 显示了这个 source-viewer 视图(图 1 中间的图片):

清单 3. source-viewer 视图
 
				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
   xmlns:ui="http://java.sun.com/jsf/facelets"
   template="/templates/masterLayout.xhtml">

  <ui:define name="content">
    <ui:include src="/sections/showSource/content.xhtml"/>
  </ui:define>

  <ui:define name="menuLeft">
    <ui:include src="/sections/showSource/menuLeft.xhtml"/>      
  </ui:define>
     
  <ui:define name="menuRight">
    <ui:include src="/sections/showSource/menuRight.xhtml"/>      
  </ui:define>

</ui:composition>    

source-viewer 视图定义了内容部分以及右菜单的内容。它还覆盖了由 清单 1 中的模板定义的针对左菜单的默认内容。

清单 4 显示了 places 视图(图 1 底部的图片):

清单 4. places 视图
 
				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
   xmlns:ui="http://java.sun.com/jsf/facelets"
   template="/templates/masterLayout.xhtml">

   <ui:define name="menuLeft">
    <ui:include src="/sections/places/menuLeft.xhtml"/>
   </ui:define>

  <ui:define name="content">
    <ui:include src="/sections/places/content.xhtml"/>
  </ui:define>

</ui:composition>    

请注意清单 234 之间的相似性。所有这三个视图均指定模板并定义内容。另外,也请注意创建新视图十分容易,因为大多数视图的基础设施都封装在模板及所包含的文件内。

使用 JSF 模板功能的另一个有趣之处是类似清单 234 中的这些视图并不会随时间有太多变化,所以大部分视图代码基本不需要维护。

与使用模板的视图类似,模板本身也更改甚少。由于大量常见功能都封装在几乎不用维护的代码中,这样一来,您就可以将精力集中于视图的实际内容 — 比如,login 页面的左菜单应该有些什么内容。专心于视图的实际内容就是下一个技巧的主旨所在。

技巧 2:使用组合的方式

在我的 CAD/CAM GUI 发布后不久,我花了几个月的时间与另一位开发人员 Bob 致力于一个新的项目。我们以 Bob 的代码为基础,而且不可思议地是,我们还能轻松进行更改并修复 bug。

我很快意识到 Bob 的代码和我的代码之间的最大区别是他编写了 方法 — 通常是在代码的 5 至 15 行之间 — 并且他的整个系统都是由这些小方法拼接而成的。在我还在忙着修改我之前项目中具有很多关注点的长方法时,Bob 已经开始机敏地组合小方法和原子功能性了。Bob 的代码和我的代码在维护性和可扩展性方面自然也有着天壤之别,从那以后,我开始信服小方法。

虽然 Bob 和我那时都没有意识到,但是我们过去一直在使用 Smalltalk 的一种设计模式,称为 Composed Method(参见 参考资料):

在一个抽象级别,将软件分成能执行单个任务的多个方法。

使用 Composed Method 模式的好处已经有大量书面记载(详细说明,请参见 Neal Ford 的 “演化架构与紧急设计:组合方法和 SLAP” )。在这里,我将侧重于介绍如何在 JSF 视图中使用 Composed Method 模式。

JSF 2 鼓励使用较小的视图段组装视图。模板封装了常见功能,进而将视图分成了更小的块。JSF 2 还提供了一个 <ui:include> 标记,正如我在先前的代码清单中所展示的,这个标记可以让您将视图进一步分成更小的功能块。比如,图 2 展示了 places 应用程序的 login 页面的左菜单:

图 2. login 页面的左菜单
login 视图的左菜单

清单 5 显示了定义该菜单内容的文件:

清单 5. login 视图左菜单的实现
 
				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:ui="http://java.sun.com/jsf/facelets">

  <div class="menuLeftText">
    #{msgs.welcomeGreeting}

    <div class="welcomeImage">
      <h:graphicImage library="images" name="cloudy.gif"/>
    </div>
  </div>
    
</html>

清单 5 内的标记很简单,这就使文件更易于阅读、理解、维护和扩展。如果相同的代码埋藏在一个很长的、包含实现 login 视图所需的全部内容的 XHTML 页面内,那么它更改起来将会很繁琐。

图 3 显示了 places 视图的左菜单:

图 3. places 视图的左菜单
places 视图的左菜单

places 视图的左菜单的实现如清单 6 所示:

清单 6. places 视图的左菜单的实现
 
				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:util="http://java.sun.com/jsf/composite/components/util">

  <div class="placesSearchForm"> 
    <div class="placesSearchFormHeading">
      #{msgs.findAPlace}
    </div>    
	
    <h:form prependId="false">
      <h:panelGrid columns="2">
	   
        #{msgs.streetAddress}
        <h:inputText value="#{place.streetAddress}" size="15"/>
	     
        #{msgs.city}  <h:inputText value="#{place.city}"  size="10"/>
        #{msgs.state} <h:inputText value="#{place.state}" size="3"/>
        #{msgs.zip}   <h:inputText value="#{place.zip}"   size="5"/>
  	     
        <h:commandButton value="#{msgs.goButtonText}" 
          style="font-family:Palatino;font-style:italic"
          action="#{place.fetch}"/>
	       	            
      </h:panelGrid>	        
    </h:form>
  </div>
	
  <util:icon image="#{resource['images:back.jpg']}"
    actionMethod="#{places.logout}"
    style="border: thin solid lightBlue"/>

</ui:composition>

清单 6 实现了一个表单,并且此表单使用了一个图标组件。(我随后会在 图标组件 一节对该图标组件进行详细讨论。目前,只需知道页面作者可以用一个图标关联图像和方法。)这个 logout 图标的图像显示在 图 3 的底部,而此 logout 图标的方法 — places.logout() — 则如清单 7 所示:

清单 7. Places.logout() 方法
 
				
package com.clarity;
...
@ManagedBean()
@SessionScoped

public class Places {
  private ArrayList<Place> places = null;
  ...  
  private static SelectItem[] zoomLevelItems = {
  ... 
  public String logout() {
    FacesContext fc = FacesContext.getCurrentInstance();     
    ELResolver elResolver = fc.getApplication().getELResolver();
	     
    User user = (User)elResolver.getValue(
      fc.getELContext(), null, "user");
	
    user.setName("");
    user.setPassword("");
	   
    setPlacesList(null);

    return "login"; 
  }
}

对我而言,清单 6 — places 视图的左菜单的实现 — 已经十分接近 30 行的代码长度限制。此清单有点难于读懂,并且该代码片段内的表单和图标可被重构成各自的文件。清单 8 显示了 清单 6 的重构版,其中,表单和图标被封装进各自的 XHTML 文件:

清单 8. 重构 places 视图的左菜单
 
				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets">

  <div class="placesSearchForm"> 
    <div class="placesSearchFormHeading">
      #{msgs.findAPlace}
    </div>    

    <ui:include src="addressForm.xhtml">	
    <ui:include src="logoutIcon.xhtml">	
  </div>    

</ui:composition>

清单 9 显示了 addressForm.xhtml:

清单 9. addressForm.xhtml
 
				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">
    
  <h:form prependId="false">
    <h:panelGrid columns="2">
    
      #{msgs.streetAddress}
      <h:inputText value="#{place.streetAddress}" size="15"/>
      
      #{msgs.city}  <h:inputText value="#{place.city}"  size="10"/>
      #{msgs.state} <h:inputText value="#{place.state}" size="3"/>
      #{msgs.zip}   <h:inputText value="#{place.zip}"   size="5"/>
      
      <h:commandButton value="#{msgs.goButtonText}" 
        style="font-family:Palatino;font-style:italic"
        action="#{place.fetch}"/>
                     
    </h:panelGrid>         
  </h:form>
  
</ui:composition>

清单 10 显示了 logoutIcon.xhtml:

清单 10. logoutIcon.xhtml
 
				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:util="http://java.sun.com/jsf/composite/components/util">

  <util:icon image="#{resource['images:back.jpg']}"
    actionMethod="#{places.logout}"
    style="border: thin solid lightBlue"/>

</ui:composition> 

在从多个小文件组装视图时,就可享受到 Smalltalk 的 Composed Method 模式的益处。您还可以组织这些文件以便更易于对更改做出反应。例如,图 4 显示了构成 places 应用程序内的这三个视图的文件:

图 4. places 应用程序的视图
places 应用程序的视图

我所创建的这三个目录 — views、sections 和 templates — 包含了用来实现 places 应用程序视图的大多数 XHTML 文件。由于 views 和 templates 目录内的文件很少更改,因此我更多关注的是 sections 目录。例如,若我想要更改 login 页面左菜单内的图标,我就知道该到哪里去更改:sections/login/menuLeft.xhtml。

当然,您可以使用任何目录结构来组织您的 XHTML 文件。如果组织得合理,定位想要修改的代码就会非常容易。

除了遵循 DRY 原则和使用 Composed Method 模式之外,还有一种好的做法是在定制组件内封装功能。组件是一种功能强大的重用机制,而且您应该充分利用这种强大性。与 JSF 1 不同,使用 JSF 2 更易于实现定制组件。

技巧 3:牢记 LEGO 拼装玩具的理念

在我还是一个男孩的时候,我有两个最喜欢的玩具:一个是化学组合(chemistry set),一个是 LEGO 拼装玩具。这两种玩具让我能够通过组合基本的构建块来创建东西,而这也成为了我一生的爱好,只不过现在是打着软件开发的幌子。

JSF 的优势一直都在于其组件模型,但这种优势直到现在才完全实现,因为用 JSF 1 很难实现定制组件。您必须要编写 Java 代码、指定 XML 配置,并对 JSF 的生命周期有深刻的理解。有了 JSF 2,您就能够轻松实现定制组件:

  • 无需配置、XML 或其他。
  • 无需 Java 代码。
  • 开发人员可以向其附加功能。
  • 修改后执行热部署。

在本文的剩余部分,我将向您介绍如何为 places 应用程序实现三个定制组件:一个图标、一个 login 面板和一个显示了地址地图和天气信息的面板。但是首先,让我先来概括介绍一下 JSF 2 复合组件。

实现定制组件

JSF 2 综合了 Facelets 模板、资源处理(在 第 1 部分 中讨论过)和一个简单的命名约定来实现复合组件。复合组件,正如其名字所示,让您能够从现有组件组装一个新组件。

一般情况下,是在 resources 目录下的 XHTML 内实现复合组件,并将它们完全通过约定链接到一个名称空间和标记。图 5 展示了我是如何为 places 应用程序组织这些复合组件的:

图 5. places 应用程序的组件
places 应用程序的组件

要使用复合组件,需要声明一个名称空间并使用标记。此名称空间通常为 http://java.sun.com/jsf/composite 外加目录名,这个目录就是 resources 目录下组件所在之处。组件名本身是其 XHTML 文件的名字,只不过没有 .xhtml 扩展名。这种约定消除了对配置的需要。比如,要在 places 应用程序中使用 login 组件,应该这样做:

<html xmlns="http://www.w3.org/1999/xhtml"
    ...
    xmlns:util="http://java.sun.com/jsf/composite/component/util">
  ...
  <util:login.../>
  ...
<html>

而要使用 icon 组件,则需要像下面这样:

<html xmlns="http://www.w3.org/1999/xhtml"
    ...
    xmlns:util="http://java.sun.com/jsf/composite/components/util">
  ...
  <util:icon.../>
  ...
<html>

最后,若要使用 place 组件,则可按如下所示的这样做:

<html xmlns="http://www.w3.org/1999/xhtml"
    ...
    xmlns:util="http://java.sun.com/jsf/composite/components/places">
  ...
  <places:place.../>
  ...
<html>

icon 组件:一个简单的复合组件

places 应用程序使用了图 6 所示的这两个图标:

图 6. places 应用程序的图标

places 应用程序的图标 places 应用程序的图标

每个图标都是一个链接。当用户单击 图 6 左侧的图标时,JSF 就会显示当前视图的标记,而激活右侧图标则会使用户登出此应用程序。

可以为链接指定一个 CSS 类名和图像,并且还可以向链接附加方法。当用户单击一个被关联的链接时,JSF 就会调用那些方法。

清单 11 给出了 icon 组件是如何被用来在 places 应用程序中显示标记的:

清单 11. 使用 icon 组件显示标记
 
				
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:util="http://java.sun.com/jsf/composite/components/util">

  <util:icon actionMethod="#{sourceViewer.showSource}" 
                      image="#{resource['images:disk-icon.jpg']}"/>
  ...
</html>

清单 12 给出了如何使用 icon 组件执行登出:

清单 12. 使用 icon 组件执行登出
 
				
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:util="http://java.sun.com/jsf/composite/components/util">

  <util:icon actionMethod="#{places.logout}" 
                      image="#{resource['images:back-arrow.jpg']}"/>
  ...
</html>

清单 13 给出了 icon 组件的代码:

清单 13. icon 组件
 
				
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:composite="http://java.sun.com/jsf/composite">
    
  <!-- INTERFACE -->
  <composite:interface>
    <composite:attribute name="image"/>
    <composite:attribute name="actionMethod" 
             method-signature="java.lang.String action()"/>        
  </composite:interface>

  <!-- IMPLEMENTATION -->          
    <composite:implementation>
    <h:form>  
      <h:commandLink action="#{cc.attrs.actionMethod}" immediate="true">

      <h:graphicImage value="#{cc.attrs.image}"
                styleClass="icon"/>

      </h:commandLink>
    </h:form>
  </composite:implementation>
</html>

与其他复合组件类似,清单 13 中的 icon 组件包含两节:<composite:interface><composite:implementation><composite:interface> 节定义了一个界面,可用来配置此组件。icon 组件具有两个属性:imageactionMethod,前者定义了组件的外观,后者定义了组件的行为。

<composite:implementation> 节包含组件的实现。它使用 #{cc.attrs.ATTRIBUTE_NAME} 表达式来访问组件的界面内定义的属性。(cc 是 JSF 2 表达式语言中的保留关键字,代表的是复合组件。)

请注意,清单 13 中的 icon 组件用 <h:graphicImage> styleClass 属性为其图像指定了一个 CSS 类。该 CSS 类的名字被硬编码为 icon,所以您就能够指定一个具有该名称的 CSS 类,JSF 将为应用程序中的所有图标使用该类。但是如果您想要覆盖该 CSS 类名,又该如何呢?在这种情况下,我可以为该 CSS 添加另一个属性并提供一个默认,可供 JSF 在未指定属性的时候使用。清单 14 给出了该属性:

清单 14. 重构后的 icon 组件
 
				
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<html xmlns="http://www.w3.org/1999/xhtml"
    ...
    xmlns:composite="http://java.sun.com/jsf/composite">
    
    <composite:interface>
      ...
      <composite:attribute name="styleClass" default="icon" required="false"/>
      ...
    </composite:interface>

    <composite:implementation>
      ...
      <h:graphicImage value="#{cc.attrs.image}"
                 styleClass="#{cc.attrs.styleClass}"/>
      ...
    </composite:implementation>
</html>

清单 14 中,我已经向这个图标组件的界面添加了一个属性,名为 styleClass,并已经在此组件的实现中引用了该属性。有了这种更改,现在就可以为此图标的图像指定一个可选的 CSS 类,如下所示:

<util:icon actionMethod="#{places.logout}" 
                  image="#{resource['images:back-arrow.jpg']}"
            styleClass="customIconClass"/>

如果不能指定 styleClass 属性,JSF 将使用默认值 icon

login 组件:一个完全可配置的组件

有了 JSF 2,就可以实现完全可配置的复合组件。例如,places 应用程序就包含了一个 login 组件,如图 7 所示:

图 7. places 应用程序的 login 组件
places 应用程序的 login 组件

清单 15 显示了这个 places 应用程序是如何使用 login 组件的:

清单 15. 使用 login 组件
 
				
<!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:comp="http://java.sun.com/jsf/composite/component/util">

  <util:login loginPrompt="#{msgs.loginPrompt}"
                namePrompt="#{msgs.namePrompt}"
            passwordPrompt="#{msgs.passwordPrompt}"
               loginAction="#{user.login}"
           loginButtonText="#{msgs.loginButtonText}"
               managedBean="#{user}">
                 
    <f:actionListener for="loginButton" 
                        type="com.clarity.LoginActionListener"/>
                            
  </util:login>
  ...
</html>

清单 15 不仅参数化 login 组件的属性,比如名字和密码提示,它还将一个动作侦听器附加到了此组件的 Log In 按钮。该按钮由 login 组件的界面公开,如清单 16 所示:

清单 16. login 组件
 
				
<!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:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:composite="http://java.sun.com/jsf/composite">

  <!-- INTERFACE -->
  <composite:interface>
    <composite:actionSource name="loginButton" targets="form:loginButton"/>
    <composite:attribute name="loginButtonText" default="Log In" required="true"/>
    <composite:attribute name="loginPrompt"/>
    <composite:attribute name="namePrompt"/>
    <composite:attribute name="passwordPrompt"/>
    <composite:attribute name="loginAction" 
      method-signature="java.lang.String action()"/>
    <composite:attribute name="managedBean"/>
  </composite:interface>
    
  <!-- IMPLEMENTATION -->
  <composite:implementation>
   <h:form id="form" prependId="false">

     <div class="prompt">
       #{cc.attrs.loginPrompt}
     </div>

     <panelGrid columns="2">
       #{cc.attrs.namePrompt}
       <h:inputText id="name" value="#{cc.attrs.managedBean.name}"/>

       #{cc.attrs.passwordPrompt} 
       <h:inputSecret id="password" value="#{cc.attrs.managedBean.password}" />

     </panelGrid>

     <p>
       <h:commandButton id="loginButton"
                     value="#{cc.attrs.loginButtonText}" 
                    action="#{cc.attrs.loginAction}"/>
     </p>
   </h:form>
   
   <div class="error" style="padding-top:10px;">
     <h:messages layout="table"/>
   </div>
  </composite:implementation>
</html>

在 login 组件的界面,我已经在 loginButton 名称下公开了 Log In 按钮。该名称所针对的是位于 form 表单内的 Log In 按钮,因此 targets 属性的值为:form:loginButton

清单 16 内的 Log In 按钮相关联的动作侦听器如清单 17 所示:

清单 17. Log In 按钮的动作侦听器
 
				
package com.clarity;

import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import javax.faces.event.ActionListener;

public class LoginActionListener implements ActionListener {
  public void processAction(ActionEvent e) 
    throws AbortProcessingException {
    System.out.println("logging in ...........");
  }
}

清单 17 内的动作侦听器完全是为了展示的目的 — 当用户登录时,我只简单地将一条消息写出到 servlet 容器日志文件。但是我希望您能体会到这样一个概念:有了 JSF 2,您可以实现完全可配置的组件,并且还可以向这些组件附加功能,所有这些均不需要任何 Java 代码或 XML 配置。这才真正称得上是功能强大。

place 组件:嵌套复合组件

JSF 2 让您能够在无需任何 Java 代码或配置的情况下实现完全可配置的组件。除此之外,您还可以嵌套复合组件,这样一来,您就可以将复杂的组件拆分成更小的、更易于管理的块。比如,图 8 所示的 place 组件,它能显示针对给定地址的地图和天气信息。

图 8. places 应用程序的 place 组件
place 组件

清单 18 给出了 places 应用程序是如何使用 place 组件的:

清单 18. 使用 place 组件
 
				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:places="http://java.sun.com/jsf/composite/components/places">

  <h:form id="form"> 
    <ui:repeat value="#{places.placesList}" var="place">
      <places:place location="#{place}"/>
    </ui:repeat>
  </h:form>
</ui:composition>

place 组件的代码如清单 19 所示:

清单 19. place 组件
 
				
<!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:composite="http://java.sun.com/jsf/composite"
    xmlns:places="http://java.sun.com/jsf/composite/components/places">

  <!--  INTERFACE -->
  <composite:interface>
    <composite:attribute name="location" required="true"/>     
  </composite:interface>
         
  <!-- IMPLEMENTATION -->
  <composite:implementation>
    <div class="placeHeading">

      <places:map     title="Map"/>
     <places:weather title="Weather"/>

    </div>
  </composite:implementation>    

</html>

清单 19 中,place 组件使用了两个嵌套组件:<places:map><places:weather>。清单 20 给出了 map 组件:

清单 20. map 组件
 
				
<!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:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:composite="http://java.sun.com/jsf/composite">
    
   <!-- INTERFACE -->
   <composite:interface>
     <composite:attribute name="title"/>
   </composite:interface>
        
   <!-- IMPLEMENTATION --> 
   <composite:implementation>
      <div class="map">
      <div style="padding-bottom: 10px;">
        <h:outputText value="#{cc.attrs.title}"
                      style="color: blue"/>
      </div>
        
      <h:panelGrid columns="1">
       <h:panelGroup>
         <div style="padding-left: 5px;">
           <i>
             <h:outputText value="#{cc.parent.attrs.location.streetAddress}, "/>
           </i>
            
           <h:outputText value=" #{cc.parent.attrs.location.city}" />
           <h:outputText value="#{cc.parent.attrs.location.state}"/><hr/>
         </div>
       </h:panelGroup>
 
       <h:panelGrid columns="2">
         <div style="padding-right: 10px;margin-bottom: 10px;font-size:14px">
           #{msgs.zoomPrompt}
         </div>
 
         <h:selectOneMenu onchange="submit()"
                        value="#{cc.parent.attrs.location.zoomIndex}"
          valueChangeListener="#{cc.parent.attrs.location.zoomChanged}"
                        style="font-size:13px;font-family:Palatino">
 
           <f:selectItems value="#{cc.parent.attrs.location.zoomLevelItems}"/>
 
         </h:selectOneMenu>
       </h:panelGrid>
 
       <h:graphicImage url="#{cc.parent.attrs.location.mapUrl}" 
        style="border: thin solid gray"/>
 
     </h:panelGrid>
     </div>
   </composite:implementation>
</html>

请注意 清单 20 中表达式 #{cc.parent.attrs.location.ATTRIBUTE_NAME} 的使用。您可以使用一个复合组件的 parent 属性来访问父组件的属性,这一点极大地方便了组件的嵌套。

但是,您无需严格依赖于嵌套组件中的父属性,正如我在 清单 19 中对 place 组件所做的那样,您也可以将属性(比如地图的标题)从父组件传递给其内嵌套的组件,与向其他任何组件(不管嵌套与否)传递属性无异。

清单 21 显示了这个 weather 组件:

清单 21. weather 组件
 
				
<!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:composite="http://java.sun.com/jsf/composite">

  <!-- INTERFACE -->
  <composite:interface>
    <composite:attribute name="title"/>
  </composite:interface>
          
  <!-- IMPLEMENTATION -->
  <composite:implementation>
    
   <div class="weather">
     <div style="padding-bottom: 10px;">
       <h:outputText value="#{cc.attrs.title}"
         style="color: blue"/>
     </div>
        
     <div style="margin-top: 10px;width:250px;">
       <h:outputText style="font-size: 12px;"
                     value="#{cc.parent.attrs.location.weather}"
                    escape="false"/>
     </div>
   </div>   
       
   </composite:implementation>
</html>

weather 组件与 map 组件一样,使用了父组件属性(来自天气 Web 服务的天气 HTML )和一个特定于组件的属性(标题)。(参见 第 1 部分 来了解此应用程序是如何获得某个地区的地图和天气信息的。)

因此,在想要实现嵌套组件时,您就有了选择。您可以让嵌套的组件依赖于其父组件的属性,也可以要求父组件将属性显式地传递给其内嵌套的组件。比如,清单 19 中的 place 组件显式地将标题属性传递给了其内所嵌套的组件,但所嵌套的组件依赖于这个父组件的属性,比如地图 URL 和天气 HTML。

是选择实现组件-显式属性,还是选择依赖于父属性,这是耦合和方便性之间的权衡问题。在本例中,mapweather 组件紧密耦合到它们的父组件(place 组件),因为它们依赖于父组件的属性。我本可以通过将 mapweather 组件的属性指定为组件-显式属性来去掉 mapweather 组件与 place 组件间的耦合。但是如果那样做的话,我就会牺牲一些方便性,因为 place 组件需要将所有属性显式地传递给 mapweather 组件。

结束语

在本文中,我向您展示了如何使用 JSF 2 的模板和复合组件特性来实现易于维护和扩展的 UI。在本系列的最后一篇文章,我将探讨如何在复合组件中使用 JavaScript、如何使用 JSF 2 的新事件模型以及如何利用 JSF 2 对 Ajax 的内置支持。

下载

描述 名字 大小 下载方法
示例源代码
jsf2fu2.zip
7.4MB

参考资料

学习 获得产品和技术
  • JSF:下载 JSF 2.0。
讨论

火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织