扩展缺省 JSF 呈现器功能
作者:Mark Basler
状态:在 Early Access 版本阶段
问题描述
在设计 JSF 组件时,开发者必须对多种不同的选择权衡利弊。其中就包括组件如何呈现其标记。如果开发的组件包含的新功能与其他现有组件并不密切相关,则通常的方法是开发新的呈现器以生成目标标记。这是一种简单直接的方法,Java EE 5 教程的 "Creating Custom UI Components"(创建定制的 UI 组件)部分详细介绍了这种方法。
有时,创建组件就是在现有组件的基础上添加功能,如添加 AJAX 功能。开发者并不希望重新实现整个组件的基本呈现行为。本文阐述了呈现组件标记的几种可行的方法,即利用现有的基本呈现功能。
解决方案
如果创建组件就是在现有组件中添加功能(如 AJAX 文件上载 JSF 组件),则可以利用现有组件的基本呈现器功能来呈现目标标记。已将文件上载组件设计为将 AJAX 功能添加到 JSF javax.faces.component.UIForm 组件中。这是因为为文件上载组件呈现的标记与具有某些缺省属性的 UIForm 相同,因此,从头开始对呈现器进行编码是无意义的。如果 Java 开发者采用面向对象的设计思想,则扩展基本功能是非常有用的,即仅添加特定于 AJAX 文件上载组件的功能。
由于此组件是与 JSP 结合使用的,因此,新组件仍需要添加一个公开该组件属性的单独 JSP 标记处理程序。同时,此标记处理程序还会将 JSF 的 component-type 和 renderer-type 链接在一起。目前还不能通过非特定于供应商的方法来扩展基本标记处理程序功能,但不久将会提供一些工具来帮助自动创建这些文件。有关如何创建定制 JSP 标记库描述符和定制标记处理程序的详细信息,将不在本文论述的范畴之内,如需了解这些任务的详情,请参见Java EE 5 教程的 "Creating the Component Tag Handler"(创建组件标记处理程序)部分。
下面,我们将讨论使用基本呈现器功能呈现组件标记的各种不同的方法。
定制呈现器调用缺省呈现器以获得基本功能
让定制呈现器调用缺省组件呈现器可以简化添加功能的过程,条件是这些功能不改变基本组件的呈现方式。AJAX 文件上载组件的定制标记添加和删除缺省 JSF 表单标记中提供的某些属性。删除的属性在文件上载组件的定制呈现器中被设置为必需的值。添加的属性被设置为在提交表单时发送的隐藏字段。在 HtmlForm 组件中填充相应的值后,将在 FacesContext 中检索缺省呈现器。调用缺省呈现器的 encodeBegin 和 encodeEnd 方法,传入 HtmlForm 组件(此时它已经被更新以保存文件上载组件的特定值)。缺省呈现器就像未使用定制标记/呈现器时一样呈现组件。
下面是一个用于说明此方法的代码片段。我们对编码示例进行了简单修改以使其更加简单明了。
文件上载定制标记工件:
文件上载标记的标记处理程序将基本的 JSF 组件 ("javax.faces.HtmlForm") 与定制呈现器 ("FileUploadForm") 链接在一起,其中的定制呈现器是在文件上载组件的 faces-config 文件中使用 component-family 定义的。
public class FileUploadTag extends UIComponentELTag {
...
public String getComponentType() {
return ("javax.faces.HtmlForm");
}
public String getRendererType() {
return ("FileUploadForm");
}
...
}
faces-config.xml
<render-kit>
<renderer>
<description>
Renderer for ajax fileupload component
</description>
<component-family>javax.faces.Form</component-family>
<renderer-type>FileUploadForm</renderer-type>
<renderer-class>com.sun.javaee.blueprints.components.ui.fileupload.FileUploadRenderer</renderer-class>
</renderer>
</render-kit>
FileUploadRenderer:
FileUploadRenderer.encodeBegin 方法会设置 HtmlForm 缺省属性,查找 "javax.faces.Form"component-family 和 renderer-type 的缺省呈现器,然后使用修改的组件将呈现委托给基本呈现器的 encodeBegin 方法。文件上载组件的子组件的呈现被发送到自身呈现器,因此,不需要在 FileUploadRenderer 中执行任何操作。FileUploadRenderer.encodeEnd 方法将隐藏字段输出到表单的主体中,然后使用修改的组件将呈现委托给基本呈现器的 encodeEnd 方法。
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
if ((context == null) || (component == null)) {
throw new NullPointerException();
}
HtmlForm outComp = (HtmlForm)component;
// custom setup of component
outComp.setEnctype("multipart/form-data");
....
// let default renderer for form render all the basic form attributes
Renderer baseRenderer=context.getRenderKit().getRenderer("javax.faces.Form", "javax.faces.Form");
baseRenderer.encodeBegin(context, outComp);
}
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
ResponseWriter writer = context.getResponseWriter();
// get properties from UIOutput that were set by the specific taghandler
String serverLocationDir=(String)component.getAttributes().get("serverLocationDir");
// add hidden field to represent location of directory on server
// if doesn't exist, defaulted in FileUploadHandler
if(serverLocationDir != null) {
writer.startElement("input", component);
writer.writeAttribute("type", "hidden", null);
writer.writeAttribute("name", id + "_" + FileUploadUtil.SERVER_LOCATION_DIR, null);
writer.writeAttribute("value", serverLocationDir, null);
writer.endElement("input");
writer.write("\n");
}
// let default renderer for form render all the basic form attributes
Renderer baseRenderer=context.getRenderKit().getRenderer("javax.faces.Form", "javax.faces.Form");
baseRenderer.encodeEnd(context, component);
}
正如您所看到的那样,这是一种定制现有组件功能的简单直接的方法,您无需重新实现缺省呈现器的功能。由于文件上载组件是一个 AJAX 组件,因此不会使用呈现器的解码方法,这是因为表单将不通过传统的方法来提交。将使用 Dojo 并通过 Shale-Remoting 进行异步提交,并由 FileUploadHandler.handleFileUpload 方法来使用。如果定制组件将在“应用请求值”阶段对表单提交进行解码,可应用相同的方法来查找缺省呈现器并将调用委托给缺省呈现器的解码方法。
定制呈现器使用隐藏子组件并将呈现委托给它
这种方法也使用定制标记处理程序/呈现器来修饰基本 JSF 组件,然后利用缺省呈现器来呈现标记。这种方法可用于将单个父组件映射到单个子组件,但它的真正优势在于,将单个父组件映射到多个子组件。在执行属性/值表达式到相应子组件的映射后,将按所需的顺序调用每个单独子组件的 encodeBegin/endcodeEnd 方法。子组件存储在父组件的 Facet 映射中,以便在将来的调用中对其检索以进行重用。
这种方法要求由父组件的标记定义来收集创建子组件所需的所有信息。标记的属性数据存储在基本 JSF 组件内部,这是使用组件从 UIComponent 继承的属性映射和值表达式映射实现的。通过直接使用父组件的映射,可以避开通常在定制的 JSF 组件中做为惯例定义的简便存取方法和增变方法。当组件开发者不希望或无法在一个定制的 JSF 组件上为额外的属性或值表达式创建简便方法时,则可以对组件使用这种方法。下面介绍了一种标记处理程序,它通过这种方法(以蓝色表示)使用 UIComponent 映射来存储属性和值表达式。
当前文件上载组件不使用隐藏子组件呈现方法,但下面的相关代码片段说明了其实现方式:
FileUploadTag 代码片段:
public class FileUploadTag extends javax.faces.webapp.UIComponentELTag {
private javax.el.ValueExpression serverLocationDir=null;
// handle unique properties for FileUpload form
public void setServerLocationDir(ValueExpression dir) {
serverLocationDir=dir;
}
protected void setProperties(UIComponent component) {
super.setProperties(component);
UIForm outComp outComp=(HtmlForm)component;
// pull out serverLocationDir attribute
if (serverLocationDir != null) {
if (!serverLocationDir.isLiteralText()) {
outComp.setValueExpression("serverLocationDir", serverLocationDir);
} else {
outComp.getAttributes().put("serverLocationDir", serverLocationDir.getExpressionString());
}
}
}
}
FileUploadRenderer:
在父组件的定制呈现器 encodeBegin 方法中,将执行检查以了解是否已创建了子组件。如果组件不存在,则会创建具有相应类型的子组件,将其存储在父组件的 Facet 映射中,并使用相应的属性/值表达式进行更新。获得子组件后,系统将呈现委托给子组件的 encodeBegin 方法。
在调用父组件的定制呈现器 encodeEnd 方法后,将从父组件的 Facet 映射中检索子组件,并将呈现委托给子组件的 encodeEnd 方法。
FileUploadRenderer 代码片段:
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
HtmlForm childComp=(HtmlForm)component.getFacet(CHILD_COMPONENT_ID);
if(childComp == null) {
// create new component and store it for later retrieval
childComp=new HtmlForm();
component.getFacets().put(CHILD_COMPONENT_ID, childComp);
// loop through and copy attributes from main component to child component
Object attr=null;
for(int ii=0; ii < formAttributes.length; ii++) {
attr=component.getAttributes().get(formAttributes[ii]);
if(attr != null) {
childComp.getAttributes().put(formAttributes[ii], attr);
}
}
// copy named ValueExpression(s), if necessary
childComp
.setValueExpression("serverLocationDir",
component
.getValueExpression(
"serverLocationDir"));
// CUSTOM default the HTML enctype so the fileupload will always work properly
childComp.getAttributes().put("enctype", "multipart/form-data");
...
}
...
// Have the child component render itself
childComp.encodeBegin(context);
}
public void encodeEnd(FacesContext context, UIComponent component) throws IOException
HtmlForm childComp=(HtmlForm)component.getFacet(CHILD_COMPONENT_ID);
if(childComp != null) {
// Have the child component render its end
childComp.encodeEnd(context);
}
}
private static String formAttributes[]={"id", "prependId","rendered","accept",
"acceptcharset","dir","enctype",lang","onclick",ondblclick","onkeydown","onkeypress",
"onkeyup","onmousedown","onmousemove","onmouseout","onmouseover","onmouseup","onreset",
"onsubmit","style","styleClass","target","title","binding"};
private static final String CHILD_COMPONENT_ID="bpui_fileupload_childcomponent";
正如您所看到的那样,虽然此方法很复杂,但是功能却更为强大。您可以利用任意数量的 JSF 组件功能,将其包装在父组件中以供其他人使用。由于此文件上载组件是以 AJAX 组件为模型的,因此,不会使用呈现器的解码方法,这是因为表单不是通过传统方法提交的。表单是使用 Dojo 并通过 Shale-Remoting 异步提交的,并由 FileUploadHandler.handleFileUpload 方法使用。如果定制组件将在“应用请求值”阶段对表单提交进行解码,可应用相同的方法从父组件的 Facet 映射中检索子组件并将调用委托给子组件的解码方法。
一般来说,如果子组件的解码方法正确地将模型数据传输到父组件,JSF 功能的其余部分将保持不变,这与此隐藏子组件方法无关。如果父组件尝试在子组件(例如,转换器、验证器和事件)中实现复杂的行为,则会出现一些警告,我们将在本文的下一版本中对其进行介绍。
© Sun Microsystems 2006。Java BluePrints Solutions Catalog 中的所有内容受版权保护,未经 Sun Microsystems 的明确书面许可,不得在其他产品中发布。