如今,许多Web应用程序开发人员都在转而使用JSF,但是他们发现,预先定制的JSF UI组件受到基本DHTML窗口部件的限制。监管或业务流程监控之类的高级应用程序需要能与JSF框架兼容的高级可视化组件。JSF框架的标准化使它易于开发能够重用的自定义Web GUI组件。另外,Web组件开发商现在能提供更复杂的组件,并承诺Web应用程序开发人员能够轻松地使用这些组件。此类JSF用户界面组件必须集成并部署到JSF运行时框架中去,并在其中完全展开,还必须在设计时很好地集成到提供JSF支持的IDE中去。
尽管JSF带来了标准用户界面框架,但对于开发第一个自定义JSF组件而言,还是存在几个缺陷和漏洞。让我们看看怎样创建一个纯HTML无法轻松创建的图形JSF组件。图形JSF组件的特点不仅要求生成DHTML,而且还需要对图像生成和客户端交互提供补充支持。我们将以一个图形组件的例子来阐述这些特点。该图形组件能够提供曲线图,并为各种客户端导航和交互提供便利。我们还会了解到将该图形组件集成到JSF-enabled IDE中所需要的步骤。通过理解图形组件的设计方法,您将会更好地理解如何实现JSF组件,而这应该能使您开发出定制的JSF图形组件。
什么是JSF?
JSF是一种能够简化Web应用程序表示层结构的标准服务器端框架。定义JSF框架的JSR 127(参见参考资料)带有一个能提供基本UI组件(如输入栏和按纽)的参考实现。您可以将可重用用户界面组件集中起来创建Web页,将这些组件绑定到应用数据源上,并用服务器端事件控制程序处理客户端事件。根据说明书介绍,组件供应商能编写与JSF运行时框架集成的组件,并将其集成到在设计时与JSF兼容的IDE中去。
从很大程度上讲,JSF组件同在HTML 2.0技术要求下可用的HTML组件和标签直接相符合。对许多Web应用程序而言,这套相对简单的组件是够用的。然而,许多应用程序如监管或监控程序需要更复杂的数据显示与交互,比如制表、制图和映射。由于JSF组件在HTML中直接提交复杂图形小部件的能力有限,所以设计这些高级组件的能力并不突出。解决方案要求服务器端组件向客户传输图像,却会给自身带来问题,因为在基本HTML图像上进行交互要受到限制。最后,使用JavaScript时,必须能调用客户端交互来使用户能对数据进行导航和交互。
让我们看看开发一个简单的、将CSS输入HTML页面的JSF组件需要哪些步骤。当开发高级JSF图形组件时,这一简单组件的描述和代码样本会作为背景。图1显示了如何使用即将开发的组件,并显示将要得到的结果。使用这种组件的好处是能够通过改变某个JSF动作的组件值,来改变整个页面的外观。
图1:显示了我们如何使用一个非常简单的JSF组件将CSS输入某个HTML页面并得出结果。
开发组件
JSF组件包含若干个Java类和配置文件。为创建一个自定义JSF组件,您需要开发一个扩展JSF基本组件类的Java类;为默认呈现软件包开发呈现程序;开发一个将在JSP页面中用于描述标签的Java类;编写一个标签库定义(TLD)文件;编写JSF配置文件。让我们更深入地了解这5个步骤。
开发组件Java类。组件类负责管理代表组件状态的属性。因此,我们必须根据组件的行为(如输入组件或输出组件),给组件选择适当的基类(参见清单1)。这里描述的组件可进行扩展javax.faces.component.UIOutput,以显示指向某个样式表文件的URL,或内联式样式表的内容。该组件可用于在JSF动作中将某个样式表转换成另一个样式表。关联属性规定着值的类型:要么是一个URL,要么是内联样式。该组件还必须能够在向服务器发送请求期间,使用经过JSF框架处理的对象,来存储并修复自己的状态。组件的状态由重建对象所需的重要属性值组成。JSF框架自动调用saveState()和restoreState()方法,我们可以在组件中实现这两种方法来达到这一目标。
清单1. 组件类管理显示组件状态的属性。可依据组件的行为,为其选择一个适当的基类。在本例中,该组件扩展javax.faces.component.UIOutput,以显示指向某个样式表文件的URL,或者某个内联式样式表的内容。
import javax.faces.component.*; public class CSSComponent extends UIOutput { private Boolean link; public String getFamily() { return "faces.CSSFamily"; } public boolean isLink() { if (link != null) return link.booleanValue(); ValueBinding vb = getValueBinding("link"); if (vb != null) { Boolean bvb = (Boolean) vb.getValue( FacesContext.getCurrentInstance()); if (bvb != null) return bvb.booleanValue(); } return false; } public void setLink(boolean link) { this.link = new Boolean(link); } public Object saveState(FacesContext context) { return new Object[] { super.saveState(context), link }; } public void restoreState(FacesContext context, Object stateObj) { Object[] state = (Object[]) stateObj; super.restoreState(context, state[0]); link = (Boolean) state[1]; } }
开发呈现程序。呈现程序有两个作用。第一,呈现程序负责发送适当的HTML程序段,该程序段能在客户端中呈现组件。通常情况下,这个HTML程序段由一些适于呈现整个Web浏览器的HTML标签组成。此JSF生存周期称作编码阶段或呈现—响应阶段。该响应阶段还能发送增强客户端交互性的JavaScript代码。
呈现程序的第二个作用是解析来自客户端的数据,从而对服务器端的组件状态进行更新(如用户在文本字段输入的文本)。标准呈现程序软件包具有强制性,但也可以提供其他呈现程序软件包,用于提供可替换的客户端表示法或SVG之类的语言(参见参考资料)。通过检验组件的连接属性,您实现的呈现程序(参见清单2)将选择在HTML页面中发送的CSS样式。
清单2. 标准呈现程序软件包具有强制性,但是,您可以使用其他呈现程序软件包,来提供可替换的客户端表示法或语言。通过检验组件的连接属性,您实现的呈现程序将选择在HTML页面中发出的CSS样式。
import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.render.Renderer; public class CSSRenderer extends Renderer { public void encodeEnd(FacesContext context, UIComponent component) throws IOException { super.encodeEnd(context, component); if (component instanceof CSSComponent) { CSSComponent cssComponent = (CSSComponent) component; String css = (String)cssComponent.getValue(); boolean isLink = cssComponent.isLink(); if (css != null) if (isLink) context.getResponseWriter().write( "<link type='text/css' rel='stylesheet' href='" + css + "'/>"); else context.getResponseWriter().write( "<style>;/n" + css + "/n<style/>/n"); } } }
开发标签类。同样,JSF框架提供了用于扩展的基类,来编写与组件相关的标签。该标签类负责定义并呈现将在faces-config.xml文件中应用的组件样式(这种样式的描述很简短)。它还负责创建JSF组件(由JSF框架来处理),传递JSF标签中所包含的属性,该属性用于初始化组件(参见清单3)。
清单3. 该标签类定义了将在faces-config.xml文件中应用的组件的样式和组件呈现方式。
import javax.faces.webapp.UIComponentTag; public class CSSTag extends UIComponentTag { private String value; private String link; public String getComponentType() { return "faces.CSSComponent"; } public String getRendererType() { return “HTML.LinkOrInlineRenderer"; } protected void setProperties(UIComponent component) { super.setProperties(component); Application app = getFacesContext().getApplication(); if (value != null) if (isValueReference(value)) component.setValueBinding("value", app.createValueBinding(value)); else component.getAttributes().put("value", value); if (link != null) if (isValueReference(link)) component.setValueBinding("link", app.createValueBinding(link)); else component.getAttributes().put("link", new Boolean(link)); } public String getLink() { return link; } public void setLink(String link) { this.link = link; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } }
该标签提供setter和getter来管理链接和值属性。组件一旦创建,便会调用setProperties()方法,对标签属性进行初始化。每个标签属性都无外乎两种:要么是文字值,要么是bean属性的一个绑定。
编写标签库定义(TLD)。TLD是一个XML文件,它通过将标签名与相应的Java类相关联来描述标签。TLD还描述了标签所允许的属性(参见清单4)。这个TLD定义了一个名为css的标签,该标签绑定到CSSTag类。它还声明了链接和值标签属性。
清单4. TLD是一个通过将标签名与相应的Java类相关联来描述标签的XML文件。TLD定义了名为css的标签,使其与CSSTag类绑定。它还声明了链接和值标签属性。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.// DTD JSP Tag Library 1.2//EN" "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd"> <taglib> <tlib-version>1.0</tlib-version> <jsp-version>1.2</jsp-version> <short-name>custom</short-name> <uri>http://www.ilog.com/jviews/tlds/css.tld</uri> <description>This tag library contains a tag for a sample custom JSF Component.</description> <tag> <name>css</name> <tag-class>path.to.CSSTag</tag-class> <description>A component that displays the style inline or a link a to a css file</description> <attribute> <name>id</name> <required>false</required> <rtexprvalue>false</rtexprvalue> <type>java.lang.String</type> <description>The id of this component. </description> </attribute> <attribute> <name>binding</name> <required>false</required> <rtexprvalue>false</rtexprvalue> <type>java.lang.String</type> <description>The value binding expression linking this component to a property in a backing bean. If this attribute is set, the tag does not create the component itself but retrieves it from the bean property. This attribute must be a value binding. </description> </attribute> <attribute> <name>value</name> <required>true</required> <rtexprvalue>false</rtexprvalue> <type>java.lang.String</type> <description>The inline css text or the url to the css file to link.</description> </attribute> <attribute> <name>link</name> <required>false</required> <rtexprvalue>false</rtexprvalue> <type>java.lang.String</type> <description>Whether the value is a link or the inline style.</description> </attribute> </tag> </taglib>
编写JSF配置文件。为了将某个JSF组件集成到框架中,您必须提供一个名为faces-config.xml的配置文件。该文件将组件类型和呈现程序类型(用于JSP定制标签处理程序)与对应的Java类关联起来。它还能描述与每个组件一同使用的呈现程序(参见清单5)。该文件定义了faces.CSSFamily组件家族。在本例中,该家族由faces.CSSComponent这一个组件类型(该类型与CSSComponent类绑定)组成。最后,HTML.LinkOrInlineRenderer类型的呈现程序(由CSSComponent类实现)要与faces.CSSFamily家族相关联。
清单5. 该文件将组件类型和呈现程序类型与对应的Java类联系起来,并描述与每个组件一同使用的呈现程序。它还定义了faces.CSSFamily组件家族。<!DOCTYPE faces-config PUBLIC "-//Sun Microsystems, Inc.// DTD JavaServer Faces Config 1.0//EN" "http://java.sun.com/dtd/web-facesconfig_1_0.dtd"> <faces-config> <component> <component-type>faces.CSSComponent </component-type> <component-class>path.to.CSSComponent </component-class> <component-extension> <component-family>faces.CSSFamily </component-family> <renderer-type>HTML.LinkOrInlineRenderer </renderer-type> </component-extension> </component> <render-kit> <renderer> <component-family>faces.CSSFamily </component-family> <renderer-type> HTML.LinkOrInlineRenderer </renderer-type> <renderer-class>path.to.CSSRenderer </renderer-class> </renderer> /render-kit> </faces-config>
开始制图
如果您希望将自己的组件集成到JSF-enabled IDE中,您还可以提供补充说明。比如说,除提供其他的设计时信息外,还可以提供一个名为sun-faces-config.xml的XML配置文件,用于描述应在IDE中公开的组件属性。
既然已经看到如何创建一个简单的JSF组件,不妨再来看看怎样创建一个图形JSF组件。我们将遵循同样的基本步骤来设计一个高级JSF图形组件。让我们以一个图形组件(如ILOG JSF图形组件)为例,通过一组分类,该组件为数据值分布提供了可视化表示。该图形能够以条型统计图、圆形分格统计图和气泡式统计图等各种显示方法来显示数据集合。该JSF图形组件有两个初始设计限制:
我们已经拥有Java图形bean组件,它具备所有图形显示能力。该组件可以显示很多图形,而且可定制性很高。在理想情况下,我们希望利用bean组件,使用它的功能来构成我们的JSF组件的基础。
普通JSF应用程序需要重新载入整个页面以更新视图。这种方法适合基于表单的应用程序,但在很多情况下却不适用于高度图形化的用户界面。因此,我们的JSF图形组件必须能在不更新整个页面的前提下处理某些简单的导航,以提供更好的用户体验。
以下是满足这些需求的解决方案:该JSF图形组件将管理图形bean组件,包括创建图形bean、定制该bean以及使该bean可用于服务器端操作。呈现JSF组件将分为两个阶段完成。JSF呈现程序会产生一个<img>标签和一套JavaScript对象(参见图2)。客户端将请求服务器发回一张图像。这一请求由某个servlet完成,该servlet获得图形bean,并利用图形提供的方法生成一幅图像(参见图3)。任何只改变该图形外观的进一步用户交互(放大、扫视、更改样式表等)都会引起图形的一次增量更新。如果客户端不只是要求对图形图像进行更新,那么将提交该页面(参见图4)。
图2JSF图形组件管理图形bean组件,包括创建图形bean、对其进行定制,并使其可用于服务器端动作。JSF呈现程序生成一个<img>标签和一套JavaScript对象。
图3 客户机通过servlet要求服务器获得一张图像。该servlet获得图形bean,并通过由图形提供的方法生成一幅图像。
图4如果客户端不只是要求对图形外观的进行更新,那么页面将被提交。
JSF图形组件还配有一套附加的JSF组件。overview可显示该图形整体视图,显示一个代表图形视图的长方形,还应允许用户扫描可视区域。legend组件可显示数据集合的相关信息,还能自行在图形中显示,依被显示数据的样式而定。也能提供客户端的interactors如扫描和放大,这些功能可看成是客户端交互,表示与图形的交互不会像一次正常的JSF交互那样重新载入整个页面。
要想呈现图形组件,只需使用chartView标签:
<jvcf:chartView id="c" style="width:500px;height:300px" … />
该数据在HTML页面中作为图像显示。该图像由servlet创建,旨在响应一次HTTP请求(该请求包括指定结果图像、生成图像映射以及生成内联式图例等各种参数)。结果图像随之被嵌入客户端DOM,页面中只有图像自身这一部分被更新。
应用程序核心部件
让我们看看简单的定制JSF组件和高级图形组件之间的一些区别。JSF图形组件类很像一个标准组件,不过是多了一个可访问图形bean(该图形bean负责生成在HTML页面中显示的图像)的图形属性。JSF组件可以通过某个绑定值或在当前会话中对这个图形bean进行局部检索。当JSF图形组件成为某个应用程序的核心部件时,可选的JSF组件(如概览或图例)便与主图形相关联,来显示附加信息(见清单6)。
清单6. 当JSF图形组件成为某个应用程序的核心部件时,可选的JSF组件便与主图形相关联,来显示附加信息。
<jvcf:chartZoomInteractor id="chartZoomInteractor" XZoomAllowed="true" YZoomAllowed="true" /> <jvcf:chartView id="chartView" chart="#{myBean.chart}" servlet="demo.ImageMapServlet" interactorId="chartZoomInteractor" width="500" height="300" styleSheets="/data/line.css" waitingImage="data/images/wait.gif" imageFormat="PNG" /> <jvcf:chartOverview id="chartOverview" style="height:100;width:150px" viewId="chartView" lineWidth="3" lineColor="red" /> <jvcf:chartLegend id="legendView" viewId="chartView" width="400" height="180" layout="vertical" waitingImage="data/images/wait.gif" />
呈现程序是实现这个JSF的一大难点。如前所述,呈现程序并不生成简单的HTML,而是生成由HTML(<IMG> tag)和JavaScript proxy(代理程序)组成的动态HTML(DHTML)。
Proxy是一个负责管理客户机组件图像显示的JavaScript类实例。该对象是服务器端Java组件类在客户端显示;它与组件类具有相同属性。页面上的每个组件、图形及其配件都有一个proxy实例。呈现JavaScript时,在每个可视的JavaScript变量上使用facesContext.getExternalContext().encodeNamespace(name)方法是个很好的实践。这样做在今后方便地将组件集成到到JSR 168-compliant端口环境中。
为举例说明客户机上的proxy,必须在页面上导入JavaScript支持库。为保持客户端尽量瘦,需要基于JavaScript库支持的proxy类,对JavaScript库进行模块化。因此,需要给每个proxy类输入一套不同的、有可能重叠的库。图形呈现的困难部分,出现在发送这些script库的阶段。每个组件的呈现程序都要声明自己需要哪个库,以及什么时候发送引用的库,必须认清已发送的库,以避免重复。仅在页面呈现期间的存在script管理器负责这项筛选工作。每当呈现程序想要发送整套库输入时,它都会向筛选出已发送库的script管理器提供列表。
客户端proxy的目的在于允许编写脚本,并避免不必要的页面更新。一旦呈现了图形,在客户端便可使用proxy,以便动态安装interactor,并显示或隐藏图像映射。Proxy对象也可供支持JavaScript鼠标事件处理的常规JSF组件使用。
<jvcf:chartView id= "chartView" .. /> <h:selectBooleanCheckbox id= "genImageMap" οnclick= "chartView.setGenerateImageMap( this.checked ? true : false, true);" />
对组件客户端proxy进行局部修改的问题在于,其状态不再与服务器上的Java组件的状态同步。为解决这个问题,proxy使用一个隐藏的输入标签(<INPUT TYPE="HIDDEN">)来保存客户机上的新状态。当执行某个标准JSF动作并提交页面时,呈现程序将解析该隐藏状态,使客户机与服务器同步。这种行为需要呈现程序类中有专门的破解行为。标准破解方法得以改进,以便解析来自客户机的状态,并更新服务器端组件的状态。
测试实例
图形及其相关组件之间的关联由标记引用与绑定来完成。为使页面设计具有灵活性,一个组件可以在呈现之前被引用。因此,在呈现时间内,如果某个组件属性引用另一个尚未呈现的组件,那么,将延迟发送依赖于客户机进行解决的JavaScript代码,直到呈现已引用的组件。此工作可由依赖性管理器完成。
为证实这一点,不妨看一个典型实例,该实例涉及某个概览,该概览引用一张图形。
<jvcf:overview viewId= "chart" [...] /> <jvcf:chartView id= "chart" [....] />
存在两种情况。被引用图形组件已经呈现,因此不存在任何问题
JSP: <jvcf:chartView id= "chart" [....] /> <jvcf:overview viewId= "chart" id="overview" [...] /> render: [...] var chart = new IlvChartViewProxy ( .. ); [...] var overview= new IlvFacesOverviewProxy ( .. ); overview.setView(chart); [...]
已引用图形的组件在依赖的概览组件之前不会呈现。既然如此,可在依赖性管理器上注册一个组件创建监视器。已引用图形组件最终呈现时,其呈现程序会通知自己创建的依赖性管理器。此时,将发送解决依赖性所需的代码:
JSP: <jvf:overview viewId= "chart" id="overview" [...] /> <jvdf:chartView id= "chart" [....] /> render: [...] var overview = new IlvFacesOverviewProxy ( .. ); [...] var chart = new IlvChartViewProxy ( .. ); overview.setView(chart); [...]
开发JSF组件的目的之一,是能够将它们应用于任何与JSF兼容的IDE。尽管如此,JSF兼容性并不足以保证这种设计时集成将会有效。下面是在开发JSF组件过程中,为了便于在今后与IDE集成需要注意的一些简单思想:
首先,定制JSF组件应该提供一个基本HTML呈现程序。在设计时,JSF IDE不能呈现请求有效数据或app服务器连接的动态图形组件。因此,带有复杂的或非传统的(比如不是HTML)呈现程序的组件,应该使用Beans.isDesignTime()来确定是提供一个基本HTML表示法,还是提供真正的组件呈现程序。
另一个设计时问题是组件的位置和大小。不同IDE使用不同的标志和属性。能够调整大小的组件(如一幅图像)应能处理定义大小的不同方式。
最后,为了与IDE集成,组件必须提供尚未被JSF说明定义的补充信息。遗憾的是,当前每个IDE都需要特殊处理程序来集成组件,即:在一种情况就需要XML文件,而在另一种情况下需要eclipse插件,如此等等。下一个JSF JSR(2.0版)的主要目的之一,将是指定附加的元数据格式。
如您所见,编写一个简单的JSF组件并不难,因为框架已经完成了大部分工作。JSF框架管理着组件状态、呈现程序等等。在本文中,我们已经扩展了这些基本概念,来设计一个能够显示复杂元数据、提供增量更新、支持大量客户端交互并与配套组件协作的高级图形JSF组件。支持这些特点需要对基本JSF组件的结构进行许多改进。当然,增量更新的概念今后对JSF框架将是一个很好的完善,因为它只允许呈现页面已改变的部分,避免了更新整个页面。按照JSF说明书工作往往不足以确保组件完全集成到JSF IDE中;一个新JSR应能及时解决这些难题。尽管存在缺陷,JSF框架仍能极大地加快Web组件开发速度、方便的融合来自各种资源的组件,以创建完整的、复杂的Web应用程序。
参考资料
- developer.sun.com
Java 工具
Sun Java Studio Creator
Developing Web Applications with JavaServer Faces - IBM
Rational 软件
Rational Application Developer for WebSphere Software - Java Community Process
Java Specification Requests JSR 127: JavaServer Faces
JSR 168: Portlet Specification
作者简介
Marc Durocher是ILOG的一名软件架构师,ILOG是企业级软件组件和服务的主要提供商。Marc Durocher在ILOG负责开发ILOG JViews生产线上的JSF组件。可以通过mdurocher@ilog.fr联系Marc。