WebForm是事件驱动的,控件状态可以在http请求之间自动保持,并且使用后置代码很好地实现了页面外观与页面逻辑控制的分离,一改以往html,服务器段代码、javaScript混杂在一起的web开发方式。stucts提供了大量的定制标签,由tag、form、bean、action及配置文件构建了一个优秀的MVC模式的web开发方式。但相比较其WebForm来,窃以为stucts更为复杂,需要协同工作的元素较多,解决问题的效果不如WebForm显著(仅是个人看法)。 在现实开发中,常常需要在某个页面中处理很多Form控件,且要处理这个页面可能引发的多个事件,在事件触发后,又请求同一个页面,又需要在请求之间保持状态,在页面中处理所有这些,真实不胜其烦。受到WebForm启发,我在用JSP进行开发时,借鉴了了其一些思想。本质上我们就是想让页面显示代码与页面控制代码分离,要作到这一点并不困难,有很多办法。 可以为页面定义一个“页面处理器(PageHandler)”,它类似WebForm的后置代码,它的接口基本是下面这个样子: public class PageHandler { protected HttpServletRequest request; protected HttpServletResponse response; protected JspWriter out; protected PageContext pageContext; protected HttpSession session = null; protected ServletContext application = null; protected ServletConfig config = null; protected String event_action = null; //页面事件 protected String event_params = null; //页面参数 //取得操作页面的基本组件 public PageHandler(PageContext page) { this.pageContext = page; this.request = (HttpServletRequest) pageContext.getRequest(); this.response = (HttpServletResponse) pageContext.getResponse(); this.pageContext = page; out = pageContext.getOut(); application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); try{ request.setCharacterEncoding("gb2312");//设定页面编码 } catch(Exception e) { e.printStackTrace(); } } //初始化页面的参数,具体的页面处理器类可以重写这 //个方法进行页面初始化 protected void onLoad() throws Exception { } //根据页面指定的事件进行处理 private final void eventBind() throws Exception { //event_action从从页面的名为event_action的hidden字段取得,它意为事件的称, //当此事件触发时,他会寻找在"页面处理器类中"与event_action同名的方法加 // 以调用。 if (event_action != null && !event_action.equals(Format.Empty)) { event_params = request.getParameter("parameters"); //事件参数参数,从页面 //的名为parameters的hidden字段取得 if (paramTypes[0] == null) { paramTypes[0] = Class.forName("java.lang.String"); } Object paramValues[] = new Object[1]; paramValues[0] = event_params; Method method = null; try { method = this.getClass().getDeclaredMethod(event_action, paramTypes); method.setAccessible(true); } catch (Exception e) { throw new UserException("系统缺少对您的请求的处理机制: + event_action); } if (method != null) { method.invoke(this, paramValues); //调用web时间 } } } //处理页面 public void process() throws Exception { try { event_action = request.getParameter("action"); //得页面事件 onLoad();//页面加载时的初始化 eventBind();//处理事件 } catch (Exception e) { e.printStackTrace(); /////////////// Format.alert(out, "发生了未知错误:" + Format.getString(e.getMessage())); } } } 当然,实用的 PageHandler应提供更为复杂的功能。 具体的页面处理器类从此类继承下来,现在,我们用一个简单的例子说明页面处理器的用法:假设有这样一个页面,有一个文本框要求用户输入一个数字,有两个按钮,点击一个要求计算出用户输入数字的2倍,点击另外一个按钮要求计算出用户输入数字的10倍。再假设此页面的页面处理器类为JspTest. //test.jsp <%@ page contentType="text/html; charset=GB2312" %> <%@ page import="youpackage.JspTest" %> <% JspTest handler=new JspTest(pageContext); handler.process();//调用页面处理器 String formAction=request.getRequestURI()+"?"+request.getQueryString(); %> <html> <head> <title>测试页面处理器</title> <script language="javascript"> function on_event(action,params) { window.form1.action.value=action; window.form1.parameters.value=params; window.form1.submit(); } </script> </head> <body bgcolor="#ffffff"> <form name="form1" method="post" action="<%=formAction%>"> 请输入数字:<input type="text" name="t_value" value="<%=handler.t_value%>"> <br><br> <font color="red"><%=handler.result%></font> <br><br> <input type="button" name="b1" value="2倍" onclick="on_event('onTwo','')"> <input type="button" name="b2" value="10倍" onclick="on_event('onTen','')"> <input type="hidden" name="action" value=""/> <input type="hidden" name="parameters" value=""/> </form> </body> </html> 则,我们为以上页面定义其页面处理器:JspTest //JspTest.java public class JspTest extends PageHandler { //定义页面变量 public int t_value;//用户输入的整数 public String result;//存储计算结果 public JspTest(PageContext page) { super(page); } protected void onLoad() throws Exception { t_value=0; result=""; //在实际应用中,这里应作许多的初始化工作(如,得到页面参数) } //双倍 private void onTwo(String str_params) throws Exception { try { t_value=Integer.parseInt(request.getParameter("t_value")); } catch(Exception e) { out.println("<script language='javaScript'>alert('您输入的不是有效的整数.');</script>"); } int i=2*t_value; result="计算结果为:"+i; } //10倍 private void onTen(String str_params) throws Exception { try { t_value=Integer.parseInt(request.getParameter("t_value")); } catch(Exception e) { out.println("<script language='javaScript'>alert('您输入的不是有效的整数.');</script>"); } int i=10*t_value; result="计算结果为:"+i; } } WebForm的基本思想也就在于此,当然,WebForm中的服务器端控件的状态可以自动保持(而我们的实现为保持状态还需作一些工作),WebForm的控件属性可以在后置代码中进行操作,服务器端事件可以在后置代码中进行邦定,服务器端控件支持数据邦定等等,我们的实现还无法做到。 如果能在Jsp中定义类似服务器端控件的东东,以上的功能在Jsp中可以得以实现。 用“页面处理器”的方式组织页面代码,起到了将页面显示元素与服务器端控制代码分离的目的,使得我们的代码更为清晰。在页面上,需要例行公事地调用页面相应的处理器(多个页面可以具有相同的处理器),声明一个类似on_event的javaScript函数,并在需要进行“回调”(提交本页面,并重新请求本页面)的Form控件的事件中调用on_event(同时指定事件的名称和参数),还需要指定Form的Action指向本页面,并在form中放置两个隐藏字段,分别持有页面发生的事件名称和需要向服务器传递的参数。 是的,需要例行公事作这么多事情,拷贝和粘贴可以完成这些工作,但如果自定义一个标签,则可以将这些类似要做的工作自动完成。我们先定义一个标签(PageTag),向其指定页面的处理器类名,由其负责调用页面处理器: public class PageTag extends BodyTagSupport { protected String pageHandlerClass = null; PageHandler handler = null; final public String getPageHandler() { return (this.pageHandlerClass); } //为标签定义PageHandler属性 final public void setPageHandler(String pageHandler) { this.pageHandlerClass = pageHandler; } //当标签开始执行时,调用页面处理器 final public int doStartTag() throws JspException { //生成PageHandler的实例 try { Class p = Class.forName(pageHandler); //在这里我们需要修改一下前面定义 //的PageHandler类,为其定义无参的构造函数,取消先前的构造 handler = (PageHandler) p.newInstance(); //改变process的定义,为其传入一个PageContext,并实现由原先 //的构造所完成的功能(取得操作页面的基本组件)。 handler.process(this.pageContext);//执行页面处理器 } catch (ClassNotFoundException e) { throw new JspException(noSuchHandler); } catch (InstantiationException e1) { throw new JspException(InstantiationErr); } catch (IllegalAccessException e2) { throw new JspException(accessErr); } return (EVAL_BODY_BUFFERED); } //我们规定页面的其他界面元素均在PageTag标签的范围内 //定义,则当PageTag.doEndTag调用时,也意味着页面 //执行完毕 final public int doEndTag() throws JspException { //为PageHandler增加一个可重写的函数onUnload //子类可以重写此函数完成页面执行完之后的工作 handler.onUnload(); return SKIP_PAGE; } } 我们还需要定义一个FormTag标签以取代html的Form,在她的实现中,要求能输出javaScript函数on_event的类似功能实现,自动将form的action指向本页面,此外,还要自动输出两个隐藏字段,分别用来持有页面发生的事件名称和需要向服务器传递的参数。 这样,我们具体的页面定义看起来象下面的样子: <%@ page contentType="text/html; charset=GB2312" %> <%@ taglib uri="/WEB-INF/myjsp-html.tld" prefix="myjsp" %> <!--pageTag标签, 属性pageHandler指定了本页面的处理器--> <myjsp:page pageHandler="myjsp.test.JspTest"> <html> <head><title>测试页面处理器</title></head> <body bgcolor="#ffffff"> <h1>测试页面处理器</h1> <myjsp:form method="post" id="myForm"><!--FormTag标签,它帮我们自动生成许多代码--> 请输入数字:<input type="text" name="t_value" value="<%=handler.t_value%>"> <br><br> <font color="red"><%=handler.result%></font> <br><br> <input type="button" name="b1" value="2倍" onclick="on_event('onTwo','')"> <input type="button" name="b2" value="10倍" onclick="on_event('onTen','')"> </myjsp:form> </body> </html> </myjsp:page> 至此,我们简化了 PageHandler使用。这样的模式使得页面具有了明显的生命周期(体现在PageHandler中): onLoad():PageHandler的子类可在此方法中进行页面的初始化,获取页面参数并作初步处理。 服务器端事件:如JspTest中定义的onTwo和onTen,如果在客户端触发了相应的事件的话,它们将在onLoad之后执行。 onUnload():页面执行完毕后被调用,可以作页面持有资源的清理工作。 在客户端的每一次请求中(包括回调),页面处理器都会按照onLoad、服务器端事件、onUnload的流程顺序运作。把握了这一点,就能很好地组织自己的代码,写出功能复杂的页面来。 这类似WebForm的运作方式,同样,要很好运用asp.net,也必须注意到WebForm的运作流程。很多习惯于开发桌面应用的人在开始用WebForm时,会随意地说:太简单了,和我以前开发 CS没什么分别,孰不知当用户点击了WebForm中的一个按钮而引起回送时,不但事件函数会执行,onLoad和onUnload也将一前一后地又被调用(为什么说又,因为在回调之前页面初次被请求时,也肯定已经调 用了onLoad和onUnload)。而桌面应用中的窗体,在其显现时调用一次onLoad,关闭时调用onUnload,中间用户触发了事件只会调用对应的事件处理函数。道理虽简单,不知道的话却要吃大亏。 如果在这样的模型基础上,定义一套自定义标签集合,则能在Jsp中实现类似WebForm的功能。当然,我们的前提是不能改变现有jsp,servlet容器的运行方式。 至此,我们还面临另外几个问题:在Jsp中实现类似WebForm中的服务器端控件、自动状态保持、数据绑定的功能,附带也可以实现类似ViewState的功能。虽然前面提出的页面控制器大大改善了传统jsp页面的组织方式,有了明显的生命周期函数,但如果没有服务器端控件、自动状态保持和数据绑定,则在实际的项目开发中我们还必须作大量繁重、重复的工作来实现类似的功能。 要想能够自动保持状态、进行数据绑定,前提是实现服务器端控件功能。我们首先会想到JSP中的自定义标记,实际也只有自定义标记才能实现类似服务器端控件的功能。 先说状态保持。例如,form中的text控件,用户更改了其value值,value在form被提交后会被送达服务器,但服务器如果重新将本页面送回客户端时,不将这个value值放在http文本流中的适当位置,那么,当客户端再次看到这个页面的时候,将看不到先前他所输入的值。也就是说,状态丢失了。而应用程序之所以成为应用程序,就要保持应用的状态,web应用程序也不例外。 我们可以自定义标签来其自身来管理其自身的状态。当标签在执行其doStartTag或doEndTag时,应通过request对象获取其自身的值,并将这个值写回客户端。则这个标签就是一个可自动保持状态的标签。 WebForm的状态保持更进一步。如WebForm中的DataGrid控件,第一次加载页面的时候,我们从数据库中读取数据并绑定到DataGrid上,则页面传送到客户端,用户作些处理后重新请求服务器(再次请求本页面,连带DataGrid中的数据也会传送回服务器),服务器作完处理后需要将页面再次传回客户端,但此次我们不需要再次请求数据库了,DataGrid自动保持状态,因为它可以得到从客户端传回的自身的数据。但request对象仅能得到form控件的某些属性的值,而且是简单类型的值,DataGrid如何通过request得到从客户端传回的自身的值?! 答案是用隐藏字段,绑定到DataGrid上的复杂数据(通常是个二维表)会被WebForm经过一些处理而赋给隐藏字段,隐藏字段的值当然会被request所得到,这样,DataGrid再将从隐藏字段得到的数据进行与刚才相反的逆向处理而被还原为DataGrid所能接受的数据。WebForm的ViewState功能同样是这样的办法实现的。 所以我们称webForm中的服务器端事件触发为回调,或回送,部分原因是客户端请求的是同一个页面,部分原因则是需要自动保持状态的值也随着客户的请求经过网络再次到达服务器啦。 可见,自动状态保持和ViewState是牺牲了网络流量的。有些程序员会不满意,你怎么将DataGrid中那么一大堆东西又通过宝贵的网络给我送回来了!我再一次请求一次数据库不就行了?解决办法是将DataGrid的enableViewState属性置为false,这样,webForm就不会偷偷将DataGrid的一大堆东西藏在Hidden中了。滥用自动状态保持会造成性能下降,你只要查看asp.net页面的源代码,其中有个_VIEWSTATE的隐藏字段,如果它的值是好长好长一串,那就应该考虑将某些控件的enableViewState属性置为false了。 jsp中我们想要实现自动状态保持和ViewState,当然也采用这样的办法。这些工作在PageHandle基类和自定义标签中进行处理,我们在使用时就会省心多了。 还有就是数据绑定。实现这个应该不是难事,应该有相应的自定义标签来处理这个事情,在此不多说。 但是,服务器端控件有那些特征是自定义标记不具备的呢?最重要的一条就是:服务器端控件可以在后置代码中被访问,并可通过代码操作控件的属性。 这一点真的那么重要吗?相信各位都有各位的看法。但这并不影响实现自动保持状态和数据绑定。 实际上我也相信,不能在后置代码(在我们的实现中应是PageHandler及其子类)中访问自定义标签也没什么大不了。PageHandler专著于为自定义标签准备数据,而自定义标签则负责把这些数据显示出来,这已经足够了,而且事件处理PageHandler也已经包办了。 但如果我们需要在运行时决定一个按钮上面应该显示什么文本,甚至需要在运行时动态创建一个自定义标签控件,那我们已有的机制虽可以实现,但却得增加应用本身的复杂度。因此我决定继续讨论用自定义标签来实现WebForm中的服务器端控件功能,而且能让开发者在PageHandler及其子类中可以访问在页面中声明的标签,并通过代码运行时改变这些控件的行为。 WebForm这样的机制使得web开发如此地接近于桌面应用的开发,使开发变得更简单。而且我相信,asp.net这样作并没有什么性能上的损失,我们可以将asp.net生成的webForm的cs源文件和Jsp容器生成的jsp文件的java源文件,不难得到这一结论。 可惜,如果我们在现有Jsp容器规范的约束下实现我们的想法,则不免要损失一些性能。前面我们在实现PageHandler的事件机制时,已经运用了java的反射机制,下面要实现服务器端控件,恐怕不免又要作一些额外的工作。 观察由tomcat生成的带有tag的jsp页面的java源文件,会发现每一个tag都会根据其在jsp文件中出现的位置而在java源文件中的对应位置被实例化(resin生成的源码还与tomcat不一样,tomcat会对每一个出现的标签实例化,而resin则对同类的tag只实例化一次),犹如如下的格式: ButtonTag _button1 = new ButtonTag(); _button1.setPageContext(pageContext); _button1.setParent(null); _button1.setCaption("我的按钮"); _button1.setId("button1"); _button1.setOnclick("clickme()"); try { int ret=_button1.doStartTag(); if (ret!=Tag.SKIP_BODY) { try { if (ret!=Tag.EVAL_BODY_INCLUDE) { out = pageContext.pushBody(); _button1.setBodyContent((BodyContent) out); _button1.doInitBody(); } do { ...处理tag的body }while(_button1.doAfterBody()==BodyTag.EVAL_BODY_AGAIN) } finally { if (ret!=Tag.EVAL_BODY_INCLUDE) { out = pageContext.popBody(); } } } if (_button1.doEndTag()==Tag.SKIP_PAGE) return; } finally { _button1.release(); } 当然,随着tag是BodyTagSupport还是TagSupport,生成的代码会有些不同。 假设以上调用页面上所有tag的代码称为A,则我们希望最终生成的代码有类似如下的结构: 1:PageHandler handler=new MyPageHandler(); 2:handler.prepareProcess(pageContext);//作处理前的初始化工作 3:handler.onInit(); 4:A;//在A 执行过程中,我们希望能将tag的实例按顺 //序添加到PageHandler的一个集合中 5:handler.onLoad();//此时,所有控件已经初始化完毕 6:handler.onEvent();//如果有请求的事件,则处理 7:handler.render();//向页面输出html代码,pageHandler会调用每个控件 //的写出html的方法,同时,摧毁每个控件 8:handler.onUnload(); 在以上步骤中,1、2、6、7由PageHandler基类完成,6会自动调用MyPageHandler的适当方法。4由PageHandler和各个tag协同完成,主要是按照tag的出现顺序将其添加入MyPageHandler中,甚至能同MyPageHandler中声明的同名控件对象引用向联系。3、6、8由MyPageHandler负责重写,MyPageHandler还要负责事件处理的逻辑实现。 这样,在第四步以后,MyPageHandler都可访问到页面中的tag实例,并可在4、5步骤中控制这些tag,在7中,tag会依据其自身最新的状态向客户端写出html文本。 现在,在第4步还存在困扰我们的问题。 在第4步还存在困扰我们的问题。按照JSP规范对Tag的定义,tag的doStartTag和doEndTag应该向JspWrite对象实例out输出html文本,如果我们真的这么作了,5、6步骤对Tag进行操作还有何意义?tag已经输出到out对象中,生米成了熟饭,即使你在5、6两步中调用了_button.setCaption("不是我的按钮"),客户端也不会看到了。更重要的是4中调用了tag的release方法,tag的属性在5、6两步中已经面目全非了。 办法只有一个,那就是在doStartTag、和doEndTag中不输出html文本,在release也不 释放tag的资源,而在另外的方法中实现本应这3个方法实现的功能,并在第7步中被pageHandler所调用。 还有另外一个问题,jsp页面中存在大量非动态的文本,它们在生成的java文件中是被out.print输出的,如 <br>静态文本 <br><myjsp:button id="button1" caption="我的按钮"> <br>按钮之后 如果按照以上的想法,则最终输出的会是: <br>静态文本 <br>按钮之后 <br><myjsp:button id="button1" caption="我的按钮"> 这是因为我们并没如期在doStartTag和doEndTag中输出html,而在页面的最后才输出,因此,所有tag的输出就会落在所有静态内容之后。那岂不是乱了套。 还记得我们先前写的PageTag吗(见本文二)?他的父类是BodyTagSupport,这样,对他的执行也会如对_button1(见本文四)那般。如果PageTag.doStartTag的返回值不是Tag.EVAL_BODY_INCLUDE,也不是Tag.SKIP_BODY,则会执行如下代码: out = pageContext.pushBody(); _PageTag.setBodyContent((BodyContent) out); _PageTag.doInitBody(); 这样,实际上就是把jsp的输出重定向了。pushBody的输出为BodyContent类的实类,同时也是JspWriter的子类,此后,out.print的任何输出都不会真正被写出,而被缓存在BodyContent中。想办法取出BodyContent中的这些字符串,让它们与tag依照jsp页面上一样的顺序出现在pageHandler的render方法中,我们就会得到正确的页面输出结果。 我们前面说过,页面中的任何内容都置于<myjsp:page pageHandler="MyPageHandler"> 和</myjsp:page>之间,这样,页面中的所有内容都不会在PageTag.doEndTag之前被输出。 我们写一个继承自BodyTagSupport的类WebControl,让其他的自定义标签均继承自这个类。则WebControl的实现大致如下: public class WebControl extends BodyTagSupport { //阻止子类重写此方法 final public int doStartTag() throws JspException { if(本控件有父控件,且父控件不是PageTag也不是FormTag) { //从BodyContent中取出本tag开始之前的静态html文本, //..置于pageHandler的render序列中 //将自身添加到pageHandler的控件集合中 } else { //从BodyContent中取出本tag开始之前的静态html文本, //..置于父控件的render序列中 //将自身添加到父控件的控件集合中 } //清空BodyContent中的内容 } //阻止子类重写此方法 final public int doEndTag() throws JspException { if(本控件有父控件,且父控件不是PageTag也不是FormTag) { //从BodyContent中取出本tag结束之前的静态html文本, //..置于pageHandler的render序列中 } else { //从BodyContent中取出本tag结束之前的静态html文本, //..置于父控件的render序列中 } //清空BodyContent中的内容 } final void render() throws JspException { startRender(); //调用本控件子控件的render()方法(按render序列执行) endRender(); dispose(); } //以下3个方法应由子类重写 //输出控件的开始标记(原本应由doStartTag做的事情) protected void startRender() throws JspException { } //输出控件的闭合标记(原本应由doEndTag做的事情) protected void endRender() throws JspException { } //释放控件的资源(原本应由release做的事情) protected void dispose() { } } 至此,按照上面的思路,在Jsp中模拟WebForm中可以实现的。在resin中运行还有些问题,因为resin为同类的tag只实例化一次,因此要在resin中运行必须修改WebControl的doStartTag方法,在此方法中,将this实例复制一分,再添加入pageHandler或其父控件中去。 |
|小黑屋|最新主题|手机版|微赢网络技术论坛 ( 苏ICP备08020429号 )
GMT+8, 2024-9-30 07:29 , Processed in 0.262544 second(s), 12 queries , Gzip On, MemCache On.
Powered by Discuz! X3.5
© 2001-2023 Discuz! Team.