要看懂/userguide/index.zul是如何实现导航的,这里有一些知识必须能够首先要理解和掌握才行。
一、ZK指令
XML处理指令描述了如何处理ZUML页面。包括page、init、component和taglib等等,这里我们了解最常见的几种处理指令就可以了。
page指令描述了页面的属性,可以将page
指令放置在XML文档的任何地方,但是language
属性只有当指令位于最高层次时才是有意义的,也就是说,处于根组件的层次。
component指令为某一页面定义新的组件。使用此指令定义的组件,仅对于使用该指令的页面是可见的。为了定义在所有组件中可以使用的组件,可是使用附加语言插件(language addon),即一个XML文件(/WEB-INF/zk.xml),用来定义在Web应用程序中所有页面都可使用的组件。
component指令有两种方式:通过宏和通过类(by-macro and by-class)。其中通过宏来定义组件又分为两种,内联宏和常规宏,它们的区别在于:内联宏的行为就像内联扩展(inline-expansion),若遇到了内联宏,则zk不会创建宏组件,相反,它会内联扩展定义在URI内的组件,换言之就像直接把内联组件的内容插入到目标页面。而对常规宏,zk将会创建一个真实的组件(称为宏组件)来显示常规宏中定义的组件,即宏组件将会作为定义在常规宏内组件的父组件被创建。常规宏允许开发人员提供额外的API并且对组件的用户隐藏实现细节,每个常规宏组件均是一个ID空间所有者,所以不必担心常规宏组件会和其它的组件名字冲突。下面是内联宏的一个例子:
username.zul
<!--MacroDef username.zul--> <row> Username<textbox id="${arg.id}" value="${arg.name}"/> </row>
demo.zul
<?component name="username" inline="true" macro-uri="username.zul"?> <grid> <rows> <username id="ua" name="John"/> </rows> </grid>
demo.zul的等价页面
<!--等价页面--> <grid> <rows> <row> UserName:<textbox id="ua" value="Jhon"/> </row> </rows> </grid>
init指令有两种格式。第一种格式是指定一个类(要实现org.zkoss.zk.ui.util.Initator接口)用于处理具体应用(application-specific)的初始化,一旦指定,在页面被赋值(evaluated)前,此类的实例会被创建,并且其doInit
方法会被调用,另外,页面被赋值(evaluated)完毕后,doFinally
方法会被调用。当例外发生时,doCatch
方法会被调用。因此,init
指令并不限于初始化,你可以将其用于清理及错误处理。第二种格式是指定一个zscript文件用于处理具体应用(application-specific)的初始化。
variable-resolver指令,为zscript解释器指定一个变量分解器(variable resolver )用以分解未知变量。被指定的类必须实现 org.zkoss.xel.VariableResolver
接口。你可以使用多个variable-resolver指令以指定多个变量分解器。声明靠后的分解器有更高的优先级。下面是一个ZK结合Spring框架的例子,它分解了在Spring框架中声明的Java Beans,这样我们可以直接访问到它们。
<?variable-resolver class="org.zkoss.zkplus.spring.DelegatingVariableResolver"?>
别的指令在这里不再赘述。
二、事件监听器(EventListener)
org.zkoss.ui.event.EventListener描述了一个事件监听器,如果一个事件监听器被注册到组件中(Component.addEventListener(String evname,EventListener listener),那么当在这个组件上有一个事件发生时,此监听器将被通知到。如下是此接口的定义,它是相当简单的:
public interface EventListener { public void onEvent(Event event) throws Exception; }
如果一个事件监听器同时实现了Deferrable接口且isDeferrable()返回true,那么此监听器被认为是一个延时事件监听器,如果一个事件监听器实现了Express接口,那么此监听器在所有其它的监听器被通知之前被通知,甚至在zuml页面中用onXxx属性声明的监听器也要在它的后面被通知到,也就是说此监听器的优先级被提高了。
GenericEventListener是EventListener的默认实现,它的子类可以更加直观地用onXxx的形式表示事件处理方法,而GenericEventListener中onEvent方法的实现会自动地把事件导航到相应的方法(主要是依据名称),另外GenericEventListener还提供了一个非常便利的方法,bindComponent(Component c)以便于把一个目标事件组件绑定到事件监听器上,这样在这个组件上发生的事件就会被事件监听器监听到。
三、Composer(设计者)与ComposerExt
Composer用于初始化组件,而ComposerExt在Composer的基础上增加了更多的控制,需要指出的是实现了ComposerExt接口的组件必须也同时实现Composer,反之不成立。与Composer接口相关的ZK属性是apply。如下是Composer的定义:
public interface Composer { public void doAfterCompose(Component comp) throws Exception; }
为了说明Composer的用途必须首先了解一下ZK Loader加载组件的顺序:
1、如果Composer也同时实现了ComposerExt,调用ComposerExt的doBeforeCompose(Page p,Component c,ComponentInfo ci)方法。
2、创建组件(调用UiFactory.newComponent(Page p,Component c,ComponentInfo ci))
3、如果Composer也同时实现了ComposerExt,调用ComposerExt的doBeforeComposeChildren(Component comp)。
4、创建组件的所有孩子。
5、在组件的所有孩子(即子组件)被创建完成后,调用doAfterCompose(Component c)
6、如果需要的话发送onCreate事件。
AfterComposer与Composer的区别:前者只能被组件本身实现,而Composer是用于初始化组件(可以实现AfterComposer也可以不实现)的控制器。
GenericComposer是Composer的默认实现,它继承自GenericEventListener,可以看出它同时又是一个事件监听器,comp上产生的事件它都会监听到。
abstract public class GenericComposer extends GenericEventListener implements Composer{ public void doAfterCompose(Component comp) throws Exception { //bind this GenericEventListener to the supervised component bindComponent(comp); } }
GenericAutowireComposer扩展了GenericComposer,它实际上是定义了若干隐含对象,例如protected Desktop desktop,即组件对应的桌面。这些隐含对象对于组件来说是肯定可以访问的,比如前面定义的桌面对应着Component.getDesktop(),问题是现在需要把这些隐含对象对应到GenericAutowireComposer,使这些隐含对象对于它以及它的子类均是可见的,其实这在GenericAutowireComposer中只用一行代码就做到了:
public void doAfterCompose(Component comp) throws Exception { super.doAfterCompose(comp); //wire variables to reference fields (include implicit objects) Components.wireVariables(comp, this); }
注意:上面代码的注释非常精炼,说明了Components.wireVariables(comp, this)的目的,我个人的理解是(不一定十分准确):只要某一变量对于comp是可见的,并且在GenericAutowireComposer或其子类中有相应名称的字段(或称域、成员变量),那么上述方法调用就会把comp中可见的变量的值赋给GenericAutowireComposer或它的子类的同名字段,当然毫无疑问这肯定是通过反射机制来实现的。
啰里啰唆说了这么多,其实GenericAutowireComposer的职责就是使得对组件可见的隐含对象以及变量能够被GenericAutowireComposer及其子类的同名属性引用到。
注意:1、由于GenericAutowireComposer持有组件的引用,所以单例的Composer不能被多个组件共享。2、应该把GenericAutowireComposer的子类定义在单独的Java文件中,而不是定义在zuml页面的zscript元素中,看来BSHInterceptor的工作原理和Java编译器与虚拟机还是有差别的。
GenericForwardComposer是GenericAutowireComposer的子类,它主要完成一些导航的功能,通过继承此类,我们就可以书写类似onXxx$compId的事件处理方法以监听标识符(id)为compId的组件产生的onXxx事件,也就是说,继承自此类的监听器不仅可以监听它所在的组件上产生的事件,还可以监听它所在的组件的子组件的事件,当然方法的命名需要遵循特定的规则。下面是一个例子:
package cn.asix.zkdemo; import org.zkoss.zk.ui.Page; import org.zkoss.zk.ui.event.Event; import org.zkoss.zk.ui.util.GenericForwardComposer; import org.zkoss.zul.Label; import org.zkoss.zul.Textbox; import org.zkoss.zul.Window; public class MyForwardComposer extends GenericForwardComposer { private Textbox mytextbox; private Window self; //embeded object, the supervised window "mywin" private Page page; //the ZK zuml page private Label mylabel; public void onChange$mytextbox(Event event) { mylabel.setValue("You just entered: "+ mytextbox.getValue()); } }
ZUML页面:
<?page title="GenericForwardComposerDemo" contentType="text/html;charset=UTF-8"?> <zk> <window title="GenericForwardComposerDemo" border="normal" apply="cn.asix.zkdemo.MyForwardComposer"> <textbox id="mytextbox"/> <label id="mylabel"/> </window> </zk>
onChange$mytextbox可以监听id标识为mytextbox的textbox上产生的onChange事件。
四、关于变量与表达式
就像在JSP中一样,可以在zuml页面的任何部分使用EL表达式,但除了属性的名字、元素以及处理指令。语法:${exp}。
EL表达式中的变量对于当前的组件必须是可见的,即Component.containsVariable(String name,boolean local)返回true。例如:
package cn.asix.zkdemo; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.Page; import org.zkoss.zk.ui.metainfo.ComponentInfo; import org.zkoss.zk.ui.util.Composer; import org.zkoss.zk.ui.util.ComposerExt; public class SimpleComposer implements Composer,ComposerExt { public void doAfterCompose(Component comp) throws Exception { } public ComponentInfo doBeforeCompose(Page arg0, Component arg1, ComponentInfo arg2) { return arg2; } public void doBeforeComposeChildren(Component comp) throws Exception { comp.setVariable("person", new Person(), false); } public boolean doCatch(Throwable arg0) throws Exception { arg0.printStackTrace(); return false; } public void doFinally() throws Exception {} public class Person{ private String name; private int age; public static final int DEFAULT_AGE=20; public Person(){ this.name="defaultName"; this.age=DEFAULT_AGE; } public String getName() { return name; } public int getAge() { return age; } } }
ZUML代码:
<?page title="GenericForwardComposerDemo" contentType="text/html;charset=UTF-8"?> <zk> <window title="SimpleComposerDemo" border="normal" apply="cn.asix.zkdemo.SimpleComposer"> <button label="${person.name}" onClick="alert(person.age);"/> </window> </zk>
在上面的例子中,由于window是一个ID空间,而SimpleComposer在doBeforeComposeChildren方法中将变量person添加到了window所对应的命名空间中:comp.setVariable("person",new Person(),false),因此这个变量对于在window所在的ID空间中的子组件button也是可见的,所以label="${person.name}"等价于label="defaultName",而onClick="alert(person.name)"等价于onClick="alert(20)",这里有一点不一样,label中的内容属于EL表达式,而onClick中的内容属于脚本代码,不过定义在命名空间中的变量对于属于同一个命名空间的脚本代码及EL表达式都是可见的。关于组件的命名空间有这样的解释:1、每一个ID空间都有一个确切的命名空间与之对应;2、定义在命名空间中的变量对于属于同一个命名空间的脚本代码及EL表达式都是可见的。
The End.