Java Enterprise Edition 5 平台包括 JavaServer Faces (JSF) 技术,该技术提供了成熟的可扩展的用户界面组件模型。通过使用此模型的设计,应用程序开发者可通过扩展 JSF 中包含的标准组件来方便地创建定制组件,并在应用程序中重用这些组件。结合使用 AJAX 和 JSF 的策略有很多种。本文主要侧重于如何使用 PhaseListener 方法在 JSF 应用程序中加入 AJAX 功能。有关访问静态和动态资源的各种方法的详细信息,请参见“访问静态和动态资源”。有关其他以及相关的解决方案,请参见“结合使用 JSF 和 AJAX”。
如果开发者要在其 JSF 组件中加入 AJAX 支持,他们可以从多个策略中进行选择。一种解决此问题的方法是,使用 PhaseListener 截取并实现定制组件的 AJAX 请求,并且(可选)提供组件资源。
我们将讨论使用 PhaseListener 将 AJAX 支持集成到 JSF 应用程序的策略。这需要创建一个定制组件以生成在客户端上执行 AJAX 交互所需的 Javascript,并且需要使用 PhaseListener 形式的另一个服务器端组件来处理 AJAX 请求。
这种方法可用于以下情况:
这种方法自身也存在一些问题,如下所示:
由于这些原因,我们目前建议的方法是使用第三方库(“访问静态和动态资源”文章中详细介绍了这种方法)。我们仍在研究一些新出现的技术,当我们的建议发生变化时,将会对这些文章进行更新。
图 1. 提交 AJAX 请求时执行的步骤
1) mouseover 事件处理程序触发用于填充和发送 XMLHttpRequest 的 Javascript 函数。URL 包含上下文根目录 (bp-jsf-example)、FacesServlet 映射 (faces)、用于通知 CompBPhaseListener 处理请求的名称空间敏感标志 (bpui_compb_action)、在 faces-config.xml 中注册的受管 Bean 名称 (CompBBean)、要调用的受管 Bean 的方法 (processMethod) 以及填充的与请求关联的查询字符串。例如:
/bp-jsf-example/faces/bpui_compb_action/CompBBean/processMethod?itemId=test1A
2) 根据请求调用 CompBPhaseListener(已经在 faces-config.xml 文件中进行了注册),然后在根目录 ID(URL 中 "/faces" Servlet 映射后面的部分)中搜索标志 "bpui_compb_action"。如果找到了该标志,则会将根目录 ID 解析为其管理 Bean 名称和方法名称,并将其修改为 javax.el.MethodExpression 形式,然后调用受管 Bean 的方法。例如:
#{CompBBean.processMethod}
3) 由于受管 Bean 在 faces-config.xml 中设置了一个受管属性 (itemId),因此,将在来自请求的管理 Bean 实例中自动设置它的值。然后,管理 Bean 从 FacesContext 中检索 HttpServletResponse,并为与请求一起发送来的 itemId 构造相应的响应。
4) 将 XML 响应发回到浏览器,并调用回调函数。
5) 分析该响应,填充弹出式组件的值并显示弹出式组件。
由于我们的组件是用于显示详细信息的弹出式组件,因此,我们需要呈现某个标记,以用于区分出实际所显示的部分。可以使用多种方法来完成此操作,一种方法是使用模板文件,读入该文件并将标记替换为维护名称空间约定所需的值。此处,将模板文本的 "%%%ID%%%" 标记替换为组件 ID 以防止组件的多个实例发生冲突。组件开发者也可以使用 ResponseWriter 的 startElement 和 endElement 方法,直接在呈现器中对模板进行编码。如果要使用工具管理组件(以便在 IDE 中以可视方式使用),则这可能是最佳的方法,因为每个 startElement 调用都与组件相关联。这样,工具便可以更好地跟踪属于各个组件的标记。Map requestMap=context.getExternalContext().getRequestMap();
Boolean scriptRendered=(Boolean)requestMap.get(RENDERED_SCRIPT_KEY);
// Check to see if resource already rendered
if (scriptRendered != null && scriptRendered.equals(Boolean.TRUE)) {
return;
}
// put flag in requestMap to indicate already rendered
requestMap.put(RENDERED_SCRIPT_KEY, Boolean.TRUE);
String contextRoot=context.getExternalContext().getRequestContextPath();
// Render markup tag for the javascript file
writer.startElement("script", component);
writer.writeAttribute("type", "text/javascript", null);
writer.writeAttribute("src", contextRoot + "/faces/jsf-example/compB/compB.js", null);
writer.endElement("script");
writer.write("\n");
<div class="bpui_compB_popTop">
<div class="bpui_compB_cornerTL"><div class="bpui_compB_cornerTR">
<center><font color="white"><b><span id="%%%ID%%%_title">title</span></b></font></center>
</div></div>
</div>
<div class="bpui_compB_popMid">
<table border="0" style="width: 100%">
<tr>
<td>
<table border="0" bgcolor="#ffffff" cellpadding="5" cellspacing="5">
<tr>
<td><span id="%%%ID%%%_message">Value</span></td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<div class="bpui_compB_popBot">
<div class="bpui_compB_cornerBL"><div class="bpui_compB_cornerBR">
</div></div>
</div>
public void afterPhase(PhaseEvent event) {
FacesContext context=event.getFacesContext();
String rootId=context.getViewRoot().getViewId();
int iPos=rootId.indexOf(APP_KEY);
int iPosx=rootId.indexOf(ACTION_KEY);
// see what suffix is used for mapping to content type
if (rootId.endsWith(SCRIPT_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "text/javascript");
} else if (rootId.endsWith(CSS_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "text/css");
} else if (rootId.endsWith(GIF_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "image/gif");
} else if (rootId.endsWith(JPG_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "image/jpeg");
} else if (rootId.endsWith(PNG_SUFFIX) && iPos > -1) {
handleResourceRequest(event, PATH_PREFIX + rootId, "image/x-png");
} else if (iPosx > -1) {
// action to invoke through a deferred method expression
String methodx="#{" + rootId.substring(iPos + ACTION_KEY.length() + 1) + "}";
methodx=methodx.replace('/','.');
try {
Class[] argTypes = { PhaseEvent.class};
// create method expression
MethodExpression mex=context.getApplication().getExpressionFactory().createMethodExpression(context.getELContext(),
methodx, null, argTypes);
Object[] args = { event };
// invoke method of managed bean
mex.invoke(context.getELContext(), args);
} catch (Exception e) {
// Just log exception
e.printStackTrace();
}
}
}
JSF 受管 Bean (com.sun.javaee.blueprints.components.ui.example.CompBBean) public void processMethod(PhaseEvent event) {
HttpServletResponse response=null;
try {
FacesContext context=event.getFacesContext();
response=(HttpServletResponse)context.getExternalContext().getResponse();
StringBuffer sb=new StringBuffer();
response.setContentType("text/xml;charset=UTF-8");
response.setStatus(200);
// need to set no cache or IE will not make future requests when same URL used.
response.setHeader("Pragma", "No-Cache");
response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
response.setDateHeader("Expires", 1);
sb.append("<response>");
// custom response code goes here
...
sb.append("</response>");
response.getWriter().write(sb.toString());
} catch (IOException iox) {
iox.printStackTrace();
} finally {
try {
response.getWriter().flush();
} catch (Exception ee) {}
// set response complete so JSF lifecycle is not continued
event.getFacesContext().responseComplete();
}
}
<!-- register the custom renderer for the standard component family -->
<render-kit>
<renderer>
<component-family>javax.faces.Output</component-family>
<renderer-type>CompB</renderer-type>
<renderer-class>com.sun.javaee.blueprints.components.ui.example.CompBRenderer</renderer-class>
</renderer>
</render-kit><!-- register components custom phaselistener -->
<lifecycle>
<phase-listener>com.sun.javaee.blueprints.components.ui.example.CompBPhaseListener</phase-listener>
</lifecycle><!-- register the custom managed bean, with a managed property (the requests itemId parameter -->
<managed-bean>
<managed-bean-name>CompBBean</managed-bean-name>
<managed-bean-class>com.sun.javaee.blueprints.components.ui.example.CompBBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>itemId</property-name>
<value>#{param.itemId}</value>
</managed-property>
</managed-bean>
<ui:compB id="pop1" url="faces/bpui_compb_action/CompBBean/processMethod?itemId="/>
<a href="#" onmouseover="bpui.compB.showPopup('pop1', event, 'test1A')"
onmouseout="bpui.compB.hidePopup('pop1')" style="cursor: pointer"><b>Mouse over link to see popup (test1A)</b></a><br/>
<small><i>(With a JSF Managed Bean fulfilling the AJAX Request)</i></small><br/><br/>
<div id="pop1" class="bpui_compB_popup">
<div class="bpui_compB_popTop">
<div class="bpui_compB_cornerTL"><div class="bpui_compB_cornerTR">
<center><font color="white"><b><span id="pop1_title">title</span></b></font></center>
</div></div>
</div>
<div class="bpui_compB_popMid">
<table border="0" style="width: 100%">
<tr>
<td>
<table border="0" bgcolor="#ffffff" cellpadding="5" cellspacing="5">
<tr>
<td><span id="pop1_message">Value</span></td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<div class="bpui_compB_popBot">
<div class="bpui_compB_cornerBL"><div class="bpui_compB_cornerBR">
</div></div>
</div>
</div>
<script type="text/javascript">
bpui.compB['pop1']=new bpui.compB.createPopup('pop1', '/bp-jsf-example/faces/bpui_compb_action/CompBBean/processMethod?itemId=');
</script>
请注意,Javascript 代码创建新的 bpui.compB.createPopup 对象并将结果存储在 bpui.compB 名称空间的 "pop1" 属性下面。此对象是一个 Javascript 关闭,通过 bpui.compB.showPopup 函数触发弹出式组件时将检索此对象(如下所述)。
// create a closure to maintain the component's id and AJAX url with callback function
bpui.compB.createPopup=function(componentId, urlx) {
this.componentId=componentId;
this.urlx=urlx;
this.ajaxReturnFunction=function() {
// make sure response is ready
if (bpui.compB.req.readyState == 4) {
// make sure it is a valid response
if (bpui.compB.req.status == 200) {
// populate the popup with the info from the response
var resultx=bpui.compB.req.responseXML.getElementsByTagName("response")[0];
document.getElementById(componentId + "_title").innerHTML=resultx.getElementsByTagName("title")[0].childNodes[0].nodeValue;
document.getElementById(componentId + "_message").innerHTML=resultx.getElementsByTagName("message")[0].childNodes[0].nodeValue;;
// show popup with the newly populated information
document.getElementById(componentId).style.visibility='visible';
} else if (bpui.compB.req.status == 204){
// error, just show alert
alert("204 returned from AJAX call");
}
}
}
}
bpui.compB.showPopup=function(popupx, eventx, itemId) {
// Position popup base on event and set timeout so popup isn't flashing all the time
var xx=0;
var yy=0;
if (!eventx) var eventx=window.event;
if (eventx.pageX || eventx.pageY){
xx=eventx.pageX;
yy=eventx.pageY;
} else if (eventx.clientX || eventx.clientY) {
xx=eventx.clientX + document.body.scrollLeft;
yy=eventx.clientY + document.body.scrollTop;
}
document.getElementById(popupx).style.left= (xx + 3) + "px";
document.getElementById(popupx).style.top=yy + "px";
// make sure the popup doesn't show all the time, need to mouseover for at least 1 second.
bpui.compB.timeout=setTimeout("bpui.compB.showPopupInternal('" + popupx + "', '" + itemId + "')", 1000);
}
bpui.compB.showPopupInternal=function(popupx, itemId) {
// initialize the AJAX request
bpui.compB.req=bpui.compB.initRequest();
// retrieve the correct popup object that is being shown
popObject=bpui.compB[popupx];
// concatenate the itemId value to the URI
url=popObject.urlx + escape(itemId);
// set the correct popup's callback function
bpui.compB.req.onreadystatechange = popObject.ajaxReturnFunction;
bpui.compB.req.open("GET", url, true);
// send the request
bpui.compB.req.send(null);
}