在当今——Web 2.0概念铺天盖地的Internet环境下,简易的AJAX集成对于一个成功的WEB框架来说是不可或缺的。因此,Struts 2其中的一个重要的功能(Feature)就是“First-class AJAX support - Add interactivity and flexibility with AJAX tags that look and feel just like standard Struts tags(大意:一流的AJAX支持——通过AJAX标志增加互动性和灵活性,而且使用这些AJAX标志与普通的Struts标志同样简单)”。
实现原理
基于不重新发明轮子的原则,Struts 2并没有开发新的AJAX框架,而是使用时下Java EE平台中比较流行的AJAX框架——Dojo和DWR。
最近在Musachy Barroso等同志的无私奉献下,开发了Struts 2的JSON插件(Plugin),极大地方便了我们输出JSON结果(Result)。
JSON插件(Plugin)
在Struts 2的showcase中的AJAX部分,JSON的结果输出是通过Freemaker模板实现。这种方法在简易性和灵活性上都比不上JSON插件,所以JSON插件值得向大家五星推荐。
下面让我们看一个JSON插件的例子。
首先到以下网址http://code.google.com/p/jsonplugin/downloads/list下载JSON插件的JAR包,并将其加入你的WebContent/WEB-INF/lib下。
接下是本例子的Action代码:
import java.util.ArrayList;
import java.util.List;
import com.googlecode.jsonplugin.annotations.JSON;
import com.opensymphony.xwork2.ActionSupport;
public class JsonPluginAction extends ActionSupport {
private static final long serialVersionUID = -6784977600668791997L;
private int bookId;
private String title;
private double price;
private List<String> comments;
private transient String secret1;
private String secret2;
@JSON(name="ISBN")
public int getBookId() {
return bookId;
}
public void setBookId(int bookId) {
this.bookId = bookId;
}
public List<String> getComments() {
return comments;
}
public void setComments(List<String> comments) {
this.comments = comments;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public String execute() {
bookId = 15645912;
title = "Max On Java";
price = 0.9999d;
comments = new ArrayList<String>(3);
comments.add("It's no bad!");
comments.add("WOW!");
comments.add("No comment!");
secret1 = "You can't see me!";
secret2 = "I am invisible!";
return SUCCESS;
}
}
以上代码值得注意的是,通过@JSON的JAVA注释(Annotation),我们可以改变JSON结果的属性名称,另外带有transient修饰符与没有Getter方法的字段(field)都不会被串行化为JSON。
然后,我们来配置一下此Action,代码如下:
<! DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd" >
< struts >
< package name ="Struts2_AJAX_DEMO" extends ="json-default" >
< action name ="JsonPlugin" class ="tutorial.JsonPluginAction" >
< result type ="json" />
</ action >
</ package >
</ struts >
上面配置文件的“package”元素和以往不同的是,它扩展了“json-default”而不是“struts-default”。“json-default”是在jsonplugin-0.11.jar包里的struts-plugin.xml中定义的。该文件同时定义了“json”的结果类型,有兴趣的朋友可以打开此文件看看。
发布运行应用程序,在浏览器中键入:http://localhost:8080/Struts2_Ajax/JsonPlugin.action,出现下载文件对话框,原因是JSON插件将HTTP响应(Response)的MIME类型设为“application/json”。把文件下载下来,用记事本打开,内容如下:
当然这还不是一个完整的AJAX的例子,下面让我们写一个HTML文件将其完成,HTML代码如下:
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
< title > JSON Plugin </ title >
< script type ="text/javascript" >
var bXmlHttpSupport = ( typeof XMLHttpRequest != " undefined " || window.ActiveXObject);
if ( typeof XMLHttpRequest == " undefined " && window.ActiveXObject) {
function XMLHttpRequest() {
var arrSignatures = [ " MSXML2.XMLHTTP.5.0 " , " MSXML2.XMLHTTP.4.0 " ,
" MSXML2.XMLHTTP.3.0 " , " MSXML2.XMLHTTP " ,
" Microsoft.XMLHTTP " ];
for ( var i = 0 ; i < arrSignatures.length; i ++ ) {
try {
var oRequest = new ActiveXObject(arrSignatures[i]);
return oRequest;
} catch (oError) { /* ignore */ }
}
throw new Error( " MSXML is not installed on your system. " );
}
}
function retrieveBook() {
if (bXmlHttpSupport) {
var sUrl = 'JsonPlugin.action';
var oRequest = new XMLHttpRequest();
oRequest.onreadystatechange = function () {
if (oRequest.readyState == 4 ) {
var oBook = eval('(' + oRequest.responseText + ')');
var bookHolder = document.getElementById('bookHolder');
var sBook = ' < p >< b > ISBN: </ b > ' + oBook.ISBN + ' </ p > ';
sBook += (' < p >< b > Title: </ b > ' + oBook.title + ' </ p > ');
sBook += (' < p >< b > Price: </ b > $' + oBook.price + ' </ p > ');
sBook += (' < b >< i > Comments: </ i ></ b >< hr /> ');
for (i = 0 ; i < oBook.comments.length; i ++ ) {
sBook += (' < p >< b > #' + (i + 1 ) + ' </ b > ' + oBook.comments[i] + ' </ p > ');
}
bookHolder.innerHTML = sBook;
}
};
oRequest.open('POST', sUrl);
oRequest.send( null );
}
}
</ script >
</ head >
< body >
< input type ="button" value ="Retrieve Book" onclick ="retrieveBook()" />
< div id ="bookHolder" ></ div >
</ body >
</ html >
以上代码中,我没有使用任何的AJAX的Javascript包,而是参考《Professional Javascript For Web Developer》手工创建XHR(XMLHttpRequest),并在XHR完成后使用eval()方法将JSON字符串变为JSON对象。需要注意的是,要调用eval函数时,必须使用“(”和“)”将JSON字符串括起来,否则会出错的。
打开http://localhost:8080/Struts2_Ajax/JsonPlugin.html,点击“Retrieve Book”按钮,页面如下图所示:
图1 JsonPlugin.html页面输出
Struts 2与Dojo
Dojo是开源Javascript工具包,它引了Widget的概念,方便了Javascript面向对象编程(OOP),改进Javascript的事件模型。在此我不打算对此进行深入的讲解,有兴趣的朋友的可以找网上找一些关于Dojo的资料学习。
Struts 2基于Dojo编写一些AJAX标志(在Dojo中称为Widget),要使用这些标志的AJAX功能,需要将标志的“theme”属性设为“ajax”。同时,亦需要将加入在<head>与</head>之间加入<s:head theme="ajax" />。当使用这些标志的AJAX功能,有些属性可能会经常用到,所以我会对这些属性稍作解释。
名称 | 描述 |
href | XHR(XMLHttpRequest)请求的地址 |
listenTopics | 监听的Dojo话题(Topic)以触发自身,如可以在可以通过发布(Publish)相应的话题,通知<s:autocompleter />重新加载其备选项(Options) |
notifyTopics | 完成远程调用后,发出通知,触发相应的Javascript函数或Dojo Widget |
formId | 需要提交到服务器的表单的ID |
formFilter | 过滤表单字段的Javascript函数名称 |
indicator | 在XHR处理过程中,包含用户提示的信息的HTML元素的ID,如图片或DIV等 |
这些标志包括:<s:a />、<s: submit />、<s:autocompleter />和<s:tree />等,下面我将分别讲解。
1、<s:a />和<s:submit />
这两个标志方便了我们的调用XHR实现AJAX,所以上面的HTML如果使用了这两标志将会变得更简单,因为我们不用再去理会繁锁的XHR创建和设定的工作。下面是示例代码:
pageEncoding = " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
< title > JSON Plugin </ title >
< s:head theme ="ajax" />
< script type ="text/javascript" >
dojo.addOnLoad( function () {
dojo.event.topic.subscribe('retrieveBook', this , function (data, type, e){
if (type == 'load') {
showBook(data);
} else if (type == 'error') {
alert('Can not retrieve the book');
}
});
});
function showBook(strBook) {
var oBook = eval('(' + strBook + ')');
var bookHolder = document.getElementById('bookHolder');
var sBook = ' < p >< b > ISBN: </ b > ' + oBook.ISBN + ' </ p > ';
sBook += (' < p >< b > Title: </ b > ' + oBook.title + ' </ p > ');
sBook += (' < p >< b > Price: </ b > $' + oBook.price + ' </ p > ');
sBook += (' < b >< i > Comments: </ i ></ b >< hr /> ');
for (i = 0 ; i < oBook.comments.length; i ++ ) {
sBook += (' < p >< b > #' + (i + 1 ) + ' </ b > ' + oBook.comments[i] + ' </ p > ');
}
bookHolder.innerHTML = sBook;
}
</ script >
</ head >
< body >
< s:url id ="bookUrl" value ="/JsonPlugin.action" />
< s:submit href ="%{bookUrl}" theme ="ajax" indicator ="indicator"
value ="Retrieve Book" align ="left" notifyTopics ="retrieveBook" />
< s:a theme ="ajax" href ="%{bookUrl}" indicator ="indicator"
notifyTopics ="retrieveBook" > Retrieve Book </ s:a >
< img id ="indicator"
src ="${pageContext.request.contextPath}/images/indicator.gif"
alt ="Loading" style ="display:none" />
< div id ="bookHolder" ></ div >
</ body >
</ html >
可能上述代码还不够简洁,因为我将HTML格式化的工作都放在Javascript中完成。但如果你的XHR返回的是HTML片段,你可以简单地将<s:a />或<s:submit />的“targets”属性设为“bookHolder”即可,详情大家可以参考Struts 2 Showcase。至于返回HTML片段,可以通过Action + Freemaker完成。
2、<s:autocompleter />
Autocomplete是比较经典的AJAX应用,虽然谷歌已经停止使用这一功能,但就Autocompleter自身而言的确是很酷的。下面是一个<s:autocompleter />的例子。
首先,我要伪造一些字符串数据,代码如下:
import java.util.ArrayList;
import java.util.List;
public final class Datas {
public static final List<String> NAMES;
static {
NAMES = new ArrayList<String>();
NAMES.add("Alabama");
NAMES.add("Alaska");
NAMES.add("American Samoa");
NAMES.add("Arizona");
NAMES.add("Arkansas");
NAMES.add("Armed Forces Europe");
NAMES.add("Armed Forces Pacific");
NAMES.add("Armed Forces the Americas");
NAMES.add("California");
NAMES.add("Colorado");
NAMES.add("Connecticut");
NAMES.add("Delaware");
NAMES.add("District of Columbia");
NAMES.add("Federated States of Micronesia");
NAMES.add("Florida");
NAMES.add("Georgia");
NAMES.add("Guam");
NAMES.add("Hawaii");
NAMES.add("Idaho");
NAMES.add("Illinois");
NAMES.add("Indiana");
NAMES.add("Iowa");
NAMES.add("Kansas");
NAMES.add("Kentucky");
NAMES.add("Louisiana");
NAMES.add("Maine");
NAMES.add("Marshall Islands");
NAMES.add("Maryland");
NAMES.add("Massachusetts");
NAMES.add("Michigan");
NAMES.add("Minnesota");
NAMES.add("Mississippi");
NAMES.add("Missouri");
NAMES.add("Montana");
NAMES.add("Nebraska");
NAMES.add("Nevada");
NAMES.add("New Hampshire");
NAMES.add("New Jersey");
NAMES.add("New Mexico");
NAMES.add("New York");
NAMES.add("North Carolina");
NAMES.add("North Dakota");
NAMES.add("Northern Mariana Islands");
NAMES.add("Ohio");
NAMES.add("Oklahoma");
NAMES.add("Oregon");
NAMES.add("Pennsylvania");
NAMES.add("Puerto Rico");
NAMES.add("Rhode Island");
NAMES.add("South Carolina");
NAMES.add("South Dakota");
NAMES.add("Tennessee");
NAMES.add("Texas");
NAMES.add("Utah");
NAMES.add("Vermont");
NAMES.add("Virgin Islands, U.S.");
NAMES.add("Virginia");
NAMES.add("Washington");
NAMES.add("West Virginia");
NAMES.add("Wisconsin");
NAMES.add("Wyoming");
}
}
然后是用于获取和过滤数据的Action,代码如下:
import java.util.ArrayList;
import java.util.List;
import com.opensymphony.xwork2.ActionSupport;
public class AutocompleterAction extends ActionSupport {
private static final long serialVersionUID = -8201401726773589361L;
private List<String[]> names;
private String start;
public void setStart(String start) {
this.start = start;
}
public List<String[]> getNames() {
return names;
}
@Override
public String execute() {
names = new ArrayList<String[]>();
if(start == null || "".equals(start.trim())) {
start = "a";
}
for(String s : Datas.NAMES) {
if(s.toLowerCase().startsWith(start.toLowerCase())) {
names.add(new String[]{ s, s });
}
}
return SUCCESS;
}
}
上述Action会以JSON的形式返回以start开头的Datas.NAMES的中字符串,以下是此Action的配置:
< result type ="json" >
< param name ="root" > names </ param >
</ result >
</ action >
在JSON类型结果的参数中加入“root”参数可以设定输出JSON结果的根,以上述情况为例,如果没有“root”参数,输出将为“{ "names": [ ["xxx", "xxx"]...] }”,加了之后变就会成“[ ["xxx", "xxx"]...] ”。接下来,让我们看看页面的代码:
pageEncoding = " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
< title > Struts 2 AJAX - Autocompleter </ title >
< s:head theme ="ajax" />
</ head >
< body >
< h2 >
Autocompleter
</ h2 >
< s:form action ="autocompleterForm" >
< s:textfield label ="abc" name ="abc" />
< tr >
< td class ="tdLabel" >
< label class ="label" >
No AJAX Autocompleter:
</ label >
</ td >
< td >
< s:autocompleter theme ="simple" name ="user"
list ="@tutorial.Datas@NAMES" />
</ td >
</ tr >
< tr >
< td class ="tdLabel" >
< label class ="label" >
AJAX Autocompleter:
</ label >
</ td >
< td >
< s:url id ="dataUrl" value ="/Autocompleter.action" />
< s:autocompleter theme ="ajax" name ="start" href ="%{dataUrl}"
loadOnTextChange ="true" loadMinimumCount ="1" indicator ="indicator"
autoComplete ="false" showDownArrow ="false" />
< img id ="indicator"
src ="${pageContext.request.contextPath}/images/indicator.gif"
alt ="Loading" style ="display:none" />
</ td >
</ tr >
</ s:form >
</ body >
</ html >
上述页面包含两个<s:autocompleter />标志,前者使用“simple”模板,所以不具有AJAX功能,它的数据将以HTML方式输出到最终页面里;而后者则使用了“ajax”模板,每当输入框的值发生改变时,它都向URL“/Autocompleter.action”发送请求,Action根据请求中的start参数的值,返回相当的JSON,在请求完成后页面通过回调函数改变输入框的下拉提示,效果如下图所示:
图2 Autocompleter.jsp页面输出
3、<s:tree />
树是是比较常用的数据结构,因为它可以很好地体现真实世界中对象之间的关系。<s:tree />的使用也相对简单,但需要说明的是——Struts 2.0.6 GA版本的<s:tree />是有BUG的,大家可以点击这个链接https://issues.apache.org/struts/browse/WW-1813了解详细的情况。这个BUG主要是在<s:tree />的通过“treeCollapsedTopic”、“treeExpandedTopic”和“treeSelectedTopic”设定的话题(Topic)都没有起作用,上述链接相应给出了解决方法,但我认为该方法太麻烦(需要自己重新编译和打包Struts 2),所以下面的例子,我将另辟徯径,请参考以下代码。
pageEncoding = " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
< title > Struts 2 AJAX - Tree </ title >
< s:head theme ="ajax" debug ="true" />
< script type ="text/javascript" >
function treeNodeSelected(arg) {
alert(arg.source.title + ' selected');
}
dojo.addOnLoad( function () {
var s = dojo.widget.byId('parentId').selector;
dojo.event.connect(s, 'select', 'treeNodeSelected');
});
</ script >
</ head >
< body >
< h2 >
Tree
</ h2 >
< div style ="float:left; margin-right: 50px;" >
< s:tree label ="parent" id ="parentId" theme ="ajax"
templateCssPath ="/struts/tree.css" showRootGrid ="true"
showGrid ="true" >
< s:treenode theme ="ajax" label ="child1" id ="child1Id" >
< s:treenode theme ="ajax" label ="grandchild1" id ="grandchild1Id" />
< s:treenode theme ="ajax" label ="grandchild2" id ="grandchild2Id" />
< s:treenode theme ="ajax" label ="grandchild3" id ="grandchild3Id" />
</ s:treenode >
< s:treenode theme ="ajax" label ="child2" id ="child2Id" />
< s:treenode theme ="ajax" label ="child3" id ="child3Id" />
< s:treenode theme ="ajax" label ="child4" id ="child4Id" />
< s:treenode theme ="ajax" label ="child5" id ="child5Id" >
< s:treenode theme ="ajax" label ="gChild1" id ="gChild1Id" />
< s:treenode theme ="ajax" label ="gChild2" id ="gChild2Id" />
</ s:treenode >
</ s:tree >
</ div >
</ body >
</ html >
因为Dojo的树控件,即使在没有设定“selector”情况下,也会自动生成一个默认的Selector,所以只要将其事件绑定到特定的事件处理函数即可。
打开http://localhost:8080/Struts2_Ajax/Tree.jsp,点击任一树节点,页面如下图所示:
图3 Tree.jsp页面输出
总结
我原本打算用一篇文章写完这个“Struts 2与AJAX”。不过在写的过程中,发现内容越来越多。如果勉强写成一篇,朋友们读起来也会很麻烦,所以我决定分开几部分,本文为第一部分。
接下来我将继续深入讲解<s:tree />的使用和通过DWR实现AJAX校验。
更多<s:tree />
在Struts 2的showcase中有两个<s:tree />的例子,分别是静态树与动态树。所谓的静态树即是在编写JSP代码时通过<s:treenode />生成树节点。我的上一篇文章的例子就是一个典型的静态树。而动态树则是在程序运行期间,Struts 2 运行时(Runtime)根据程序中的数据动态创建树节点。虽然在两个例子中<s:tree />的theme属性都为“ajax”,但是从严格意义上来说,这两种树都不属于AJAX树,因为它们都是在输出页面时将全部节点加载到其中,而不是在父节点展开时通过XHR(XMLHttpRequest)获取节点数据。
动态树
下面我们先看一下动态树的例子,接着再一步步地将其改造为名副其实的AJAX 树。下例将会把WEB应用程序的目录树展现在JSP页面中。因此,我需要先包装一下java.io.File 类,代码如下:
import java.io.File;
public class FileWrapper {
private File file;
public FileWrapper(String path) {
file = new File(path);
}
public FileWrapper(File file) {
this.file = file;
}
public String getId() {
return "file_" + file.hashCode();
}
public String getName() {
return file.getName();
}
public String getAbsolutePath() {
return file.getAbsolutePath();
}
public FileWrapper[] getChildren() {
File[] files = file.listFiles();
if(files != null && files.length > 0) {
int length = files.length;
FileWrapper[] wrappers = new FileWrapper[length];
for(int i = 0; i < length; ++i) {
wrappers[i] = new FileWrapper(files[i]);
}
return wrappers;
}
return new FileWrapper[0];
}
}
清单1 src/tutorial/FileWrapper.java
之所以需要对File类进行如此包装,是因为<s:tree />用于动态树时,rootNode、nodeIdProperty、nodeTitleProperty 和 childCollectionProperty等属性都必填的。
然后是Action类的代码如下:
import javax.servlet.http.HttpServletRequest;
import org.apache.struts2.interceptor.ServletRequestAware;
import com.opensymphony.xwork2.ActionSupport;
public class DynamicTreeAction extends ActionSupport implements ServletRequestAware {
private static final long serialVersionUID = 1128593047269036737L;
private HttpServletRequest request;
private FileWrapper root;
public void setServletRequest(HttpServletRequest request) {
this.request = request;
}
public FileWrapper getRoot() {
return root;
}
@Override
public String execute() {
root = new FileWrapper(request.getSession().getServletContext().getRealPath("/"));
return SUCCESS;
}
}
清单2 src/tutorial/DynamicTreeAction.java
上述代码取得WEB应用程序的根目录的绝对路径后,初始化FileWrapper对象root。该对象将为JSP页面的<s:tree />的根节点。如下代码所示:
pageEncoding = " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
< title > Struts 2 AJAX - More Tree </ title >
< s:head theme ="ajax" debug ="true" />
< script type ="text/javascript" >
/* <![CDATA[ */
function treeNodeSelected(arg) {
alert(arg.source.title + ' selected');
}
function treeNodeExpanded(arg) {
alert(arg.source.title + ' expanded');
}
function treeNodeCollapsed(arg) {
alert(arg.source.title + ' collapsed');
}
dojo.addOnLoad( function () {
var t = dojo.widget.byId('appFiles');
dojo.event.topic.subscribe(t.eventNames.expand, treeNodeExpanded);
dojo.event.topic.subscribe(t.eventNames.collapse, treeNodeCollapsed);
var s = t.selector;
dojo.event.connect(s, 'select', 'treeNodeSelected');
});
/* ]]> */
</ script >
</ head >
< body >
< h2 >
Dynamic Tree Example
</ h2 >
< div style ="float:left; margin-right: 50px;" >
< s:tree id ="appFiles" theme ="ajax" rootNode ="root"
nodeTitleProperty ="name" nodeIdProperty ="id"
childCollectionProperty ="children" />
</ div >
</ body >
</ html >
清单3 WebContent/Tree.jsp
因为<s:tree />的treeCollapsedTopic和treeExpandedTopic属性都没有起作用,所以如果我们想要监听这两个事件,就必须使用上述代码的方法。
最后是struts.xml配置文件:
<! DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd" >
< struts >
< package name ="Struts2_AJAX_DEMO" extends ="struts-default" >
< action name ="DynamicTree" class ="tutorial.DynamicTreeAction" >
< result > Tree.jsp </ result >
</ action >
</ package >
</ struts >
清单4 src/struts.xml
发布运行应用程序,在浏览器地址栏中键入http://localhost:8080/Struts2_Ajax2/DynamicTree.action,有如下图所示页面:
图1 动态树示例
AJAX 树
正如我在文章开头所说,Struts 2所提供的静态树和动态树都不是严格意义上的AJAX树。下面就让我们来实现一个如假包换的AJAX树。首先要说明的是,Struts 2的<s:tree />默认是不支持这种按需加载数据的AJAX树。不过因为它是基于Dojo的树控件(Widget)所以要扩展也很方便。
Dojo 通过名为“TreeRPCController”的控件实现 AJAX 树,它会监听被控制树的事件。当发生展开节点的事件时,TreeRPCController就会向URL发送XHR请求,该URL由TreeRPCController的RPCUrl 属性定义。XHR请求格式类似如下格式:
清单5 XHR样本
显而易见,请求中包含三个参数,分别是action为“getChildren”(固定值),data一个包含当前节点与树信息的JSON串和dojo.preventCache随机串,用于缓存不同节点的请求响应(父节点只会在第一次被展开时到服务器端加载数据,之后都是从浏览器的缓存中读取数据,可以提高应用程序性能)。
首先我要先写一个加载树节点数据的Action类,代码如下:
import java.util.Map;
import com.googlecode.jsonplugin.JSONExeption;
import com.googlecode.jsonplugin.JSONUtil;
public class AjaxTreeAction extends DynamicTreeAction {
private static final long serialVersionUID = 3970019751740942311L;
private String action;
private String data;
private FileWrapper[] wrappers;
public void setAction(String action) {
this.action = action;
}
public void setData(String data) {
this.data = data;
}
public FileWrapper[] getWrappers() {
return wrappers;
}
@Override
public String execute() {
if("getChildren".equals(action)) {
try {
Object o = JSONUtil.deserialize(data);
String path = ((Map) ((Map) o).get("node")).get("objectId").toString();
wrappers = new FileWrapper(path).getChildren();
} catch (JSONExeption e) {
e.printStackTrace();
}
return "ajax";
}
return super.execute();
}
}
清单6 src/tutorial/AjaxTreeAction.java
上述代码可能需要解释一下:
- action属性对应于XHR中的action,如果它为“getChildren”时,则需要进行加载子节点操作。否则,会读取树的根节点,并返回JSP页面;
- 通过上面XHR的分析,大家可以知道data是代表树和当前节点的JSON串,故应将其反串行化为Map对象,并将其 objectId属性取出。通常情况下,Dojo树的objectId属性代表服务器端的对象的标识,在本例中为文件夹的绝对路径;
- wrappers属性表示当前文件夹下的文件数组,它被传送到Freemarker页面,翻译为Dojo树节点数组的JSON串。
下面是Freemarker页面的代码:
< #list wrappers as r >
{ "title": "${r.name}", "isFolder": < #if r.children?size gt 0 > true < #else > false </ #if > , "id": "${r.id}", "objectId": "${r.absolutePath?js_string}" } < #if r_has_next > , </ #if >
</ #list >
]
清单7 WebContent/AjaxTree.ftl
以上代码中<#list></#lsit>的写法是Freemarker中遍历集合的写法;而<#if r.children?size gt 0>判断“r”对象的children属性是否为空;r.absolutePath?js_string 就是将“r”的absolutePath属性的值输出为Javascript 的字串符形式;<#if r_has_next></#if>判断集合是否有下一项数据。如果希望更详细地了解Freemarker的使用,请参考该手册。
接下来,让我们看看Action的配置代码片段:
< result > AjaxTree.jsp </ result >
< result name ="ajax" type ="freemarker" > AjaxTree.ftl </ result >
</ action >
清单8 src/struts.xml配置片段
最后是JSP页面代码:
pageEncoding = " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
< title > Struts 2 AJAX - More Tree </ title >
< s:head theme ="ajax" debug ="true" />
< script type ="text/javascript" >
/* <![CDATA[ */
function treeNodeSelected(arg) {
alert(arg.source.title + ' selected');
}
dojo.addOnLoad( function () {
var t = dojo.widget.byId('appFiles');
var s = t.selector;
dojo.event.connect(s, 'select', 'treeNodeSelected');
});
/* ]]> */
</ script >
</ head >
< body >
< h2 >
AJAX Tree Example
</ h2 >
< div style ="float:left; margin-right: 50px;" >
< script type ="text/javascript" >
/* <![CDATA[ */
dojo.require( " dojo.lang.* " );
dojo.require( " dojo.widget.* " );
dojo.require( " dojo.widget.Tree " );
dojo.require( " dojo.widget.TreeRPCController " );
/* ]]> */
</ script >
< div dojoType ="TreeRPCController" widgetId ="treeController"
DNDcontroller ="create" RPCUrl ="<s:url />" ></ div >
< div dojoType ="Tree" widgetId ="appFiles" toggle ="fade" controller ="treeController" >
< div dojoType ="TreeNode" title ='<s:property value ="root.name" /> '
widgetId=' < s:property value ="root.id" /> '
isFolder=' < s:property value ="root.children.length > 0" /> '
objectId=' < s:property value ="root.absolutePath" /> '>
</ div >
</ div >
</ div >
</ body >
</ html >
清单9 WebContent/AjaxTree.jsp
由于上面所提及的原因,我在上述的代码中并没有使用<s:tree />标志,而是使用了Dojo的写法——创建 widgetId 为“treeController”的 TreeRPCController 并将设为树的控制器。
发布运行应用程序,在浏览器地址栏中键入http://localhost:8080/Struts2_Ajax2/AjaxTree.action,点开某个节点,在节点加载的过程中,加号图标变成时钟状图标,如下图所示页面:
图2 AJAX树示例
自定义<s:tree />的AJAX的主题(theme)
Struts 2的标志过人之外在于它允许开发人员自定义标志的页面输出。要做到这一点,你所需要做的只是创建一个自定义的theme并将其应用到相应标志。下面就让我自定义一个真正的AJAX的<s:tree/>的theme。
首先,你的源文件的根目录下新建包“template.realajax”。
然后,在上一步所建的包中新建“tree.ftl”文件,内容如下:
/* <![CDATA[ */
dojo.require( " dojo.lang.* " );
dojo.require( " dojo.widget.* " );
dojo.require( " dojo.widget.Tree " );
dojo.require( " dojo.widget.TreeRPCController " ); < # -- Added by Max -->
/* ]]> */
</ script >
< #-- Added by Max -- >
< div dojoType ="TreeRPCController"
widgetId ="${parameters.id?html}_controller"
DNDcontroller ="create"
RPCUrl ="<@s.url />" >
</ div >
< #-- End -- >
< div dojoType ="Tree"
<#if parameters.blankIconSrc?exists >
gridIconSrcT=" < @s .url value ='${parameters.blankIconSrc}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.gridIconSrcL?exists >
gridIconSrcL=" < @s .url value ='${parameters.gridIconSrcL}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.gridIconSrcV?exists >
gridIconSrcV=" < @s .url value ='${parameters.gridIconSrcV}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.gridIconSrcP?exists >
gridIconSrcP=" < @s .url value ='${parameters.gridIconSrcP}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.gridIconSrcC?exists >
gridIconSrcC=" < @s .url value ='${parameters.gridIconSrcC}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.gridIconSrcX?exists >
gridIconSrcX=" < @s .url value ='${parameters.gridIconSrcX}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.gridIconSrcY?exists >
gridIconSrcY=" < @s .url value ='${parameters.gridIconSrcY}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.gridIconSrcZ?exists >
gridIconSrcZ=" < @s .url value ='${parameters.gridIconSrcZ}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.expandIconSrcPlus?exists >
expandIconSrcPlus=" < @s .url value ='${parameters.expandIconSrcPlus}' includeParams ='none' /> "
</ #if >
< #if parameters.expandIconSrcMinus?exists >
expandIconSrcMinus=" < @s .url value ='${parameters.expandIconSrcMinus?html}' includeParams ='none' /> "
</ #if >
< #if parameters.iconWidth?exists >
iconWidth=" < @s .url value ='${parameters.iconWidth?html}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.iconHeight?exists >
iconHeight=" < @s .url value ='${parameters.iconHeight?html}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.toggleDuration?exists >
toggleDuration=${parameters.toggleDuration?c}
</ #if >
< #if parameters.templateCssPath?exists >
templateCssPath=" < @s .url value ='${parameters.templateCssPath}' encode ="false" includeParams ='none' /> "
</ #if >
< #if parameters.showGrid?exists >
showGrid="${parameters.showGrid?default(true)?string}"
</ #if >
< #if parameters.showRootGrid?exists >
showRootGrid="${parameters.showRootGrid?default(true)?string}"
</ #if >
< #if parameters.id?exists >
id="${parameters.id?html}"
</ #if >
< #if parameters.treeSelectedTopic?exists >
publishSelectionTopic="${parameters.treeSelectedTopic?html}"
</ #if >
< #if parameters.treeExpandedTopic?exists >
publishExpandedTopic="${parameters.treeExpandedTopic?html}"
</ #if >
< #if parameters.treeCollapsedTopic?exists >
publishCollapsedTopic="${parameters.treeCollapsedTopic?html}"
</ #if >
< #if parameters.toggle?exists >
toggle="${parameters.toggle?html}"
</ #if >
controller="${parameters.id?html}_controller" < #-- Added by Max -- >
>
< #if parameters.label?exists >
< div dojoType ="TreeNode" title ="${parameters.label?html}"
<#if parameters.nodeIdProperty?exists >
id="${stack.findValue(parameters.nodeIdProperty)}"
< #else >
id="${parameters.id}_root"
</ #if >
>
< #elseif parameters.rootNode?exists >
${stack.push(parameters.rootNode)}
< #-- Edited by Max -- >
< div dojoType ="TreeNode"
title ="${stack.findValue(parameters.nodeTitleProperty)}"
widgetId ="${stack.findValue(parameters.nodeIdProperty)}"
isFolder ="<#if stack.findValue(parameters.childCollectionProperty)?size gt 0>true<#else>false</#if>"
objectId ="${stack.findValue(parameters.nameValue)}" >
</ div >
< #-- End -- >
< #assign oldNode = stack.pop() /> < #-- pop the node off of the stack, but don't show it -- >
</ #if >
清单10 src/template/realajax/tree.ftl
对上述稍作解释,上述代码主要在原版的src/template/ajax/tree.ftl的基础上添加了TreeRPCController的控件,并只输出根节点。由于<s:tree />没有类似nodeObjectIdProperty的属性,所以我用了value属性表示objectId对应的属性名称。
接着新建tree-close.ftl文件,内容和原版的一样,如下所示:
清单11 src/template/realajax/tree-close.ftl
再下来就应该是将theme应用到<s:tree />,如下代码所示:
pageEncoding = " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
< title > Struts 2 AJAX - More Tree </ title >
< s:head theme ="ajax" debug ="true" />
< script type ="text/javascript" >
/* <![CDATA[ */
function treeNodeSelected(arg) {
alert(arg.source.title + ' selected');
}
dojo.addOnLoad( function () {
var t = dojo.widget.byId('appFiles');
var s = t.selector;
dojo.event.connect(s, 'select', 'treeNodeSelected');
});
/* ]]> */
</ script >
</ head >
< body >
< h2 >
AJAX Tree Example
</ h2 >
< div style ="float:left; margin-right: 50px;" >
< s:tree id ="appFiles" theme ="realajax" rootNode ="root"
nodeTitleProperty ="name" nodeIdProperty ="id"
childCollectionProperty ="children" value ="absolutePath" />
</ div >
</ body >
</ html >
清单12 WebContent/AjaxTreeTheme.jsp
上述代码中<s:tree />的用法,除了theme改为“realajax”和多了value="absolutePath"外,几乎和静态树中的一样。
为了不影响前一个例子,我们为该JSP文件配置类型相同的Action,如下代码所示:
< result > AjaxTreeTheme.jsp </ result >
< result name ="ajax" type ="freemarker" > AjaxTree.ftl </ result >
</ action >
清单13 src/struts.xml配置片段
发布运行应用程序,在浏览器地址栏中键入http://localhost:8080/Struts2_Ajax2/AjaxTreeTheme.action,结果如图2所示。
总结
通过上述例子,大家知道Struts 2 的AJAX 标志是基于Dojo控件开发的,所以如果大家希望熟练地使用这些标志,最好去了解一下Dojo。
很久没有更新BLOG了,前一段时间公司的项目比较忙,另外我还和一位出版社的朋友谈写书的事情,所以一直没有时间,完成《Struts 2与AJAX》。后来写书的事情吹了,趁今天有点空闲就把它完成。
在大家看这部分文章之前,我想对于写书的事情说两句,或者应该叫发牢骚才对。通过这次写书失败的经历,我明白为什么国内的IT书籍多数是滥于充数、粗制滥造、缺乏经典。其实说白了就是一个“钱”字作怪。为了市场,很多编辑可能会“建议”你去“抄考”一些国内相对畅销的同类书籍,例如写Struts就一定要按所谓的MVC进行目录分类,美其名曰“容易入门”。我认为“MVC”的概念虽然重要,但对初学者而言,需要对编程有一定的了解才容易明白此概念。另外,为了“实用”,不惜使用相同的技术重复编写不同的范例。可能是我不太了解读者的心理吧。
言归正传,在上两部分的《Struts 2与AJAX》中我介绍了Struts 2与DOJO结合实现AJAX的知识,本文将介绍在Struts 2中使用DWR实现AJAX表单校验。
什么是DWR
DWR(Direct Web Remoting)是在Java EE中较流行的AJAX框架,它的最大优势就是可以像使用本地的Javascript函数一样,调用服务器上的Java方法。如下图所示:
图1 DWR工作原理
其实DWR原理也不复杂,它先在web.xml中配置一个Servlet,映射到特定的路径(通常是%CONTEXT_PATH%/dwr/*)。这个Servlet的作用就是初始化要暴露给Javascript调用的Java类(通过dwr.xml进行配置),并生成相应的代理的Javascript类代码。在XHR请求到来的时候,Servlet负责将请求的参数变成对应的Java对象,并以其为参数调用目标Java方法,并将返回值转化为Javascript代码。详情请参考:http://getahead.ltd.uk/dwr/
Struts 2与DWR
在Struts 2.0.x中使用DWR实现AJAX表单校验。在大家掌握了DWR的原理后,下面我想详细介绍一下实现的步骤。
首先,到以下站点https://dwr.dev.java.net/files/documents/2427/47455/dwr.jar下载DWR的1.1.4版本的JAR包。需要注意的是,DWR虽然已经发布2.0版本,但它与1.1.4有很大的区别,所以请大家不要使用2.0版本,否则会出现异常的;
接着,新建WEB工程,将下图所示的JAR包加入到工程的“Build Path”中;
图2 依赖的JAR包
接下来,配置web.xml文件,内容如下:
< web-app id ="WebApp_9" version ="2.4"
xmlns ="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation ="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" >
< display-name > Struts 2 AJAX Part 3 </ display-name >
< filter >
< filter-name > struts-cleanup </ filter-name >
< filter-class >
org.apache.struts2.dispatcher.ActionContextCleanUp
</ filter-class >
</ filter >
< filter-mapping >
< filter-name > struts-cleanup </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
< filter >
< filter-name > struts2 </ filter-name >
< filter-class >
org.apache.struts2.dispatcher.FilterDispatcher
</ filter-class >
</ filter >
< filter-mapping >
< filter-name > struts2 </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >
<!-- 开始DWR配置 -->
< servlet >
< servlet-name > dwr </ servlet-name >
< servlet-class > uk.ltd.getahead.dwr.DWRServlet </ servlet-class >
< init-param >
< param-name > debug </ param-name >
< param-value > true </ param-value >
</ init-param >
</ servlet >
< servlet-mapping >
< servlet-name > dwr </ servlet-name >
< url-pattern > /dwr/* </ url-pattern >
</ servlet-mapping >
<!-- 结束DWR配置 -->
< welcome-file-list >
< welcome-file > index.html </ welcome-file >
</ welcome-file-list >
</ web-app >
清单1 WebContent/WEB-INF/web.xml
然后是DWR的配置文件:
<!-- START SNIPPET: dwr -->
<! DOCTYPE dwr PUBLIC
"-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN"
"http://www.getahead.ltd.uk/dwr/dwr10.dtd" >
< dwr >
< allow >
< create creator ="new" javascript ="validator" >
< param name ="class" value ="org.apache.struts2.validators.DWRValidator" />
</ create >
< convert converter ="bean" match ="com.opensymphony.xwork2.ValidationAwareSupport" />
</ allow >
< signatures >
<![CDATA[
import java.util.Map;
import org.apache.struts2.validators.DWRValidator;
DWRValidator.doPost(String, String, Map<String, String>);
]]>
</ signatures >
</ dwr >
<!-- END SNIPPET: dwr -->
清单2 WebContent/WEB-INF/dwr.xml
通过以上配置,我们可以将DWRValidator中的方法暴露为Javascript可以调用的远程接口。
在正确完成以上步骤之后,我们发布运行一下应用程序,在浏览器地址栏中输入http://localhost:8080/Struts2_Ajax3/dwr/,应该会出现如下页面:
图3 DWR Servlet默认输出页面
接下来,我们要开始编写Action类了,代码如下:
import com.opensymphony.xwork2.ActionSupport;
public class AjaxValidation extends ActionSupport {
private static final long serialVersionUID = -7901311649275887920L;
private String name;
private String password;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String execute() {
return SUCCESS;
}
}
清单3 src/tutorial/AjaxValidation.java
上述代码一目了然,相信大家已经很熟悉了。下面,我们再来看看表单校验的配置代码:
< validators >
< validator type ="regex" >
< param name ="fieldName" > password </ param >
< param name ="expression" >
<![CDATA[ (?!^[0-9]*$)(?!^[a-zA-Z]*$)^([a-zA-Z0-9]{8,10})$ ]]>
</ param >
< message > Password must be between 8 and 10 characters, contain at least one digit and one alphabetic character, and must not contain special characters </ message >
</ validator >
< field name ="name" >
< field-validator type ="requiredstring" >
< message > You must enter a name </ message >
</ field-validator >
</ field >
< field name ="age" >
< field-validator type ="int" >
< param name ="min" > 18 </ param >
< param name ="max" > 127 </ param >
< message > Age must be between 18 and 127 </ message >
</ field-validator >
</ field >
</ validators >
清单4 src/tutorial/AjaxValidation-validation.xml
对于AjaxValidation类的name、password和age三个字段,我分别用了非空、正规表达式和范围验证。正规表达式(?!^[0-9]*$)(?!^[a-zA-Z]*$)^([a-zA-Z0-9]{8,10})$的作用是保证密码由至少包括一个数字和一个字母,且不能含有符号的长度为8到10的字符串组成。它也是所谓强密码(Strong Password)的普通实现。
接下来的是JSP的代码,内容如下:
pageEncoding = " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>
<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
< title > Struts 2 AJAX - Validation </ title >
< s:head theme ="ajax" />
</ head >
< body >
< h2 >
AJAX Validation Using DWR
</ h2 >
< s:form method ="post" validate ="true" theme ="ajax" >
< s:textfield label ="Name" name ="name" />
< s:password label ="Password" name ="password" />
< s:textfield label ="Age" name ="age" />
< s:submit />
</ s:form >
</ body >
</ html >
清单5 WebContent/AjaxValidation.jsp
以上代码也不复杂,不过需要的是注意的是除了要加入<s:head theme="ajax" />外,<s:form />也必须加入validate="true" theme="ajax"的属性。
最后是Struts 2的配置文件,内容如下所示:
<! DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd" >
< struts >
< package name ="Struts2_AJAX_DEMO" extends ="struts-default" >
< action name ="AjaxValidation" class ="tutorial.AjaxValidation" >
< result name ="input" > AjaxValidation.jsp </ result >
< result > AjaxValidation.jsp </ result >
</ action >
</ package >
</ struts >
清单6 src/struts.xml
最后发布运应用程序,在浏览器地址栏中输入http://localhost:8080/Struts2_Ajax3/AjaxValidation!input.action出现如下图所示页面:
图4 AjaxValidation页面输出
在文本框中输入错误的值使页面出现错误提示信息,如下图所示:
图5 AjaxValidation页面错误提示
可能有朋友会问怎么知道这是通过AJAX进行校验的呢?在这里我向大家推荐一个AJAX开发必备的工具——Firebug。Firebug是Firefox的一个功能强大的插件,它可以准确地输出和定位Javascript的错误、通过直观的方式查看HTML文档的DOM及其样式、所见即所得的编辑方式,更值得一赞的是它可以方便地对Javascript进行跟踪和调试,如果你希望这进一步了解这个工具,请安装Firefox 2.0以上版本,并使用它浏览以下网址http://www.getfirebug.com。
在安装完成Firebug之后,在Firefox中打开http://localhost:8080/Struts2_Ajax3/AjaxValidation!input.action,按“F12”键找开Firebug窗口,如果你第一次使用Firebug,请点击其窗口中的链接“Enable Firebug”激活插件。之后,点击“Net”,并在出现的菜单中点击选中“XHR”。然后将光标移入文本框,再将光标移出使文本框失去焦点,你可以看到Firebug窗口会多出一项记录,如下图所示:
图6 Firebug中查看XHR请求
这就证明你在文本框失去焦出时,Struts 2会发送XHR请求到服务器以对该文本框值进行校验。有兴趣的朋友可以通过Firebug,研究XHR的请求与响应,这样可以加深对DWR工作原理的理解。
何时使用AJAX表单校验
虽然在Struts 2实现AJAX表单校验是一件非常简单的事,但我建议大家不要在所有的场合都使用这个功能,原因可以分为以下几个方面:
- AJAX校验在服务器上进行数据校验,可能会比较耗时;
- AJAX校验可能会过于频繁,加重服务器的负载;
- 一些普通的校验,只需要使用纯Javascript便可以实现。
读到这里,有的朋友可能会问:“那么什么时候才应该使用AJAX表单校验呢?”答案其实很简单,当我们的校验在页面加载时还不能够确定的情况下,就应该使用这个功能。例如,注册用户时,校验用户名是否已经存在;或者校验涉及过多的页务逻辑等。
现在让我们来改造一下上述例子,对于name我们可以使用AJAX校验,但对于其它的字段应该使用纯Javascript的校验。
在tutorial.AjaxValidation类加入如下方法:
public void validate() {
Set<String> users = new HashSet<String>();
users.add("max");
users.add("scott");
if(users.contains(name)) {
addFieldError("name", "The user name has been used!");
}
}
清单7 src/tutorial/AjaxValidation.java代码片段
用于模拟用户注册的场境,当然在真实情况应该在数据库中检查用户是否存在。
接下来再修改JSP文件,将<s:form />里面的内容改为如下所示代码:
< s:textfield label ="Name" name ="name" theme ="ajax" />
< s:password label ="Password" name ="password" theme ="xhtml" />
< s:textfield label ="Age" name ="age" theme ="xhtml" />
< s:submit theme ="xhtml" />
</ s:form >
清单8 WebContent/AjaxValidation.jsp代码片段
对比早前的JSP代码,大家可以看出我将<s:form />的theme改成了“ajax_xhtml”,这个theme不是Struts 2自带,需要自定义。另外,除了Name使用了ajax的theme之外,其它的表单标签的theme都为xhtml,如此一来便可以实现只有当Name文本框失去焦点时才发生AJAX表单校验。
接下来,应该是我们的自定义ajax_xhtml的theme了。在源代码文件夹下新建包“template.ajax_xhtml”,然后在其中加入form.ftl和form-close.ftl文件,内容分别如下:
< script type ="text/javascript" src ="${base}/struts/validationClient.js" ></ script >
< script type ="text/javascript" src ="${base}/dwr/interface/validator.js" ></ script >
< script type ="text/javascript" src ="${base}/dwr/engine.js" ></ script >
< script type ="text/javascript" src ="${base}/struts/ajax/validation.js" ></ script >
</ #if >
< #include "/${parameters.templateDir}/xhtml/form-validate.ftl" />
< #include "/${parameters.templateDir}/simple/form.ftl" />
< #include "/${parameters.templateDir}/xhtml/control.ftl" />
清单9 src/template/ajax_xhtml/form.ftl
上述的文件与xhtml theme中的form.ftl文件相似,我只是加入了AJAX表单校验所用的Javascript库,以便theme为ajax的表单标签使用。
< #include "/${parameters.templateDir}/simple/form-close.ftl" />
< #include "/${parameters.templateDir}/xhtml/form-close-validate.ftl" />
清单10 src/template/ajax_xhtml/form-close.ftl
这个文件与xhtml theme中的form-close.ftl文件相同。
最后发布运行应用程序,大家可以发现在Password与Age的校验,只有在表单提交时才发生,而且是纯Javascript的校验。不过,以上代码还不是很完善,在行为上有些BUG。
总结
Struts 2相比一些其它的框架,在实现AJAX方面的确简单很多。更激动人心的是Struts 2的标签库支持基于模板的输出,使得开发者可以跟据自身的需要方便地改变标签的行为。