实现图形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/