实现图形JSF组件
很简单地构建一个纯HTML无法轻松实现的图形Web应用程序组件
作者:Marc Durocher
http://dev2dev.bea.com.cn/techdoc/wlworkshop/2005070704.html
开发人员认为,如果有合适的工具来创建交互式Web界面,他们就能将时间集中在核心需求和定制上,并在规定时间内及时得交付应用程序。与其他技术如JavaServer Pages或Apache Struts 相比,JavaServer Faces (JSF)技术为创建交互式Web应用程序带来了很多便利。JSF在程序逻辑和GUI表示之间划出一条清晰的界限,提高了对Web程序的维护能力,并为Web用户界面组件的开发和重用提供了一个框架。

  如今,许多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" onclick=
	"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应用程序。

参考资料

作者简介

  Marc Durocher是ILOG的一名软件架构师,ILOG是企业级软件组件和服务的主要提供商。Marc Durocher在ILOG负责开发ILOG JViews生产线上的JSF组件。可以通过mdurocher@ilog.fr联系Marc。

原文出处

http://www.ftponline.com/weblogicpro/2005_03/magazine/features/mdurocher/