xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.zkoss.org/2005/zul
http://www.zkoss.org/2005/zul/zul.xsd">
4 ZK常用基本组件
4.1 输入框组件汇总
在ZK中,输入框被细分成了很多种:textbox、intbox、decimalbox、doublebox、datebox、timebox、spinner、combobox、bandbox
组件的常用属性有(属性是可选的):constraint、disabled、maxlength、readonly
组件名
用途
textbox
用于输入文本。
例1:"Jerry" width="150px" />
效果图:
例2:"password" value="foo" width="150px" />
效果图:
intbox
用于输入整数,其他的按键都会被屏蔽。比如输入字母时不会有任何反应
例:"no negative,no zero" width="150px" value="12345678" />
效果图:
decimalbox
用于输入小数,其他的按键都会被屏蔽。
例:"###.##" value="154.326" width="150px" />
效果图:
doublebox
用于输入双精度数,其他的按键都会被屏蔽
datebox
用于输入日期,它自带一个日历控件,且可以对日期进行格式化。
例:"db" width="150px" format="yyyy/MM/dd" />
效果图:
未点击时:
点击后:
timebox
用于输入日期,注意:它的value是Date类型的。
例:
Date now = new Date();
"${now}"/>
效果图:
spinner
用于输入整数,其他的按键都会被屏蔽,组件上的箭头用于增加或减小输入框中的值。它的value只能是int类型的整数。step属性用于控制增长的歩长
例:"123345" step="2" />
效果图:
combobox
下拉框。默认情况下是可以输入值的,设置了disabled=”true”之后,就只能通过鼠标选择而不能输入值。
例:
效果图:
未点击时:
点击后:
bandbox
一般用作搜索框。可以使用buttonVisible="false"将组件上的按钮隐藏
例:"123" />
效果图:
4.1.1 对输入框输入内容的约束和验证
输入框通常都有一个constraint属性,它是用来约束用户输入的,使用constraint属性可以控制输入控件接受什么值。
当失去光标的焦点时,如果不符合约束条件,就会弹出出错信息。
在ZK中提供了两种约束用户输入的方式:第一是使用ZK自定义的约束条件;第二是使用正则表达式。
constraint属性可以使用的值有:no positive、no negative、 no zero、no empty、no future、no past、no today 和一个正则表达式的集合。
no positive、no negative、 no zero适用于intbox。
no future、no past和no today 约束仅适用于datebox。
no empty适用于任何组件。
正则表达式约束仅适用于字符串类型组件,例如textbox、combobox和bandbox。
① ZK自定义的约束条件
constraint中可以使用的约束条件表
约束条件
解析
no empty
输入不能为空
no future
不能是以后的时间。用于datebox
no negtive
不能输入负数。用于intbox
no past
不能是过去的时间。用于datebox
no positive
不能是正数。用于intbox
no today
不能是今天。用于datebox
no zero
不能是0。用于intbox
between yyyyMMdd and yyyyMMdd
控制时间在一个范围内。
例:"between 20071225 and 20071203"/>
after yyyyMMdd
例:"after 20071225"/>
before yyyyMMdd
end_before
end_after
after_start
after_end
指定弹出的错误提示框的位置。
例:"no empty, start_before"/>
效果图:
② 使用正则表达式
例:"/.+@.+\.[a-z]+/:邮箱地址不正确" />
效果图:
也可以不指定出错信息,这时会使用默认的出错信息。
例:"/.+@.+\.[a-z]+/" />
效果图:
如果是在java代码中,我们可以这样指定控件的constraint:
new Textbox().setContraint("/.+@.+\\.[a-z]+/");
4.2 容器组件
容器组件可以用来包含其他组件。
4.2.1 Window组件
Window组件是我们在ZK中使用最多的一个组件。
一个Window组件,就像HTML中的div标签一样,用来将组件组合在一起。
和其他组件不同,Window组件具有如下特点:
◆ Window组件是ID空间宿主。Window组件下的任何组件(包括它自身)都可以通过这个Window组件的id来进行检索。使用:id.getFellow(java.lang.String)
◆ Window组件可以设置成overlapped(重叠)、popup(弹出)、embeded(嵌入)
◆ Window可以作为一个模态窗口(modal dialog)
① Window组件的模式(mode)
zul页面中,使用mode 属性 来设置Window的模式。
Window组件有5种不同的模式:overlapped、popup、modal、hightlighted、embeded
默认的是embeded模式。
我们可以通过Window.setMode(java.lang.String)来改变Window的模式。
我们也可以通过调用Window.doOverlapped()、Window.doPopup()、Window.doModal()、Window.doHightlighted()、Window.doEmbeded()来改变Window的模式。
例:
"win" title="Hi!" border="normal" width="200px" >
"Help" />
"Hello, Wolrd!" />
"CLOSE" onClick='win.setVisible(false)' />
"Overlap" onClick="win.doOverlapped();" />
"Popup" onClick="win.doPopup();" />
"Modal" onClick="win.doModal();" />
"Embed" onClick="win.doEmbedded();" />
"Highlighted" onClick="win.doHighlighted();" />
"true"/>
"SHOW" onClick='win.setVisible(true)' />
效果图:
embeded:这是默认的模式。这种模式下,我们不能改变Window的位置。
overlapped:overlapped模式下的Window与其他的组件是层叠的。用户可以使用鼠标拖拽它。开发人员可以通过Window.setLeft(java.lang.String)和Window.setTop(java.lang.String)来设置它的位置。
popup:popup模式和overlapped模式相似。它们之间不同是:在popup模式下,我们只要点击其他组件的话,这个弹出的Window就会自动消失。
modal:modal与hightlighted模式基本上是相同的。modal模式下,Window之外的组件是不能够操作的(如下图)。
hightlighted:
4.1.2 Window组件的常用属性
window属性表
属性名
说明
border
取值有:normal、none(默认为none)
例:
"border为none" border="none" width="200px" >
border为none的Window
"border为none" border="normal" width="200px" >
border为normal的Window
closable
一旦我们关闭了这个Window,那么这个页面上将不会有这个Window存在,也不能对它进行引用,否则会报错,这是也隐藏最大的不同。我们也可以通过Window.detach()来实现相同的效果。
Window关闭的时候会触发onClose事件。
我们可以重写onClose方法来实现我们自己想要的操作。比如,我们想在点击关闭按钮的时候将Window隐藏,而不是detach
例:
"myWin" closable="true" title="Demo" border="normal" width="200px"
onClose="self.visible = false; event.stopPropagation();" >
点击关闭按钮,Window将会被隐藏,而非detach
"SHOW" onClick='myWin.setVisible(true)' />
效果图:
contentStyle
通过这个属性,我们可以为Window定制样式。
例1:
"My Window" border="normal" width="200px" contentStyle="background:yellow" >
Hello, World!
效果图:
例2:
"win" title="Hi" width="150px" height="100px" contentStyle="overflow:auto" border="normal" >
当我们设置了overflow:auto之后,当内容超过了可显示的区域之后,就会出现滚动条
效果图:
position
我们可以为mode为overlapped、pupup和modal的Window设置位置。
如:"300px" mode="overlapped" position="right,bottom" >
可供选择的position有:center、left、right、top、bottom
sizable
用于设置Window是否可以改变大小。
当用户改变Window的大小时会触发onSize事件
title AND caption
用来设置标题。(注:caption是一个组件,而非属性)
例:
"My Window" border="normal" width="200px" >
"/imgs/sml4.gif" label="Hi there!" /> "Hello, World!"/>
效果图:
4.2 Grid组件
Grid组件是处理与展现大量数据的组件。与它功能类似的组件有:Grid、Listbox、Tree。
4.2.1 Grid、Listbox、Tree的比较
◆ Grid和Listbox都是用来展现列表数据的。而Tree主要用来展示分层数据。
◆ Listbox和Tree允许用户选中它所展现的一条或多条数据(可以通过ZK提供的方法来获取选中的数据),而Grid则不行。
◆ Grid、Listbox和Tree都支持分页,具体请查阅Paging组件。
4.2.2 Grid概观
相关子组件:grid、columns、colum、rows、row
组件名
说明
grid
使用它来定义一个grid
columns
用来设置grid的标题头。
column
位于columns里面,用来设置列的相关属性
rows
用来展现数据
row
位于rows里面,用来展现一行数据
例1:
"500px">
"Column 1"/>
"Column 2"/>
"Column 3"/>
|
"Item A.1" />
"Item A.2" />
"Item A.3" />
|
"Item B.1" />
"Item B.2" />
"Item B.3" />
|
"Item C.1" />
"Item C.2" />
"Item C.3" />
效果图:
4.2.3 Grid展现大批量数据
在grid中,我们有两种方式来展现大批量数据:一是使用滚动条;二是使用分页。
① 使用滚动条
只需要为grid的hight属性设置一个值,当数据展现超过这个hight时,滚动条会自动出现。
例:
我们在上面的例子中的grid标签上加个属性height=”100px”,即"500px" height="100px" >。得到的效果图如下:
② 使用分页
在grid中添加属性:mold=”paging” pageSize=”n”即可。当展现的数据量超过n条时,分页控件会自动出现。
(注意:这里的分页只是页面上的分页,而不是后台的分页。有人叫这种分页为假分页)
例:
我们将上面代码中的height去掉,加上属性:mold="paging" pageSize="3"
效果图:
pageSize的默认值为:20
除此指定mold=”paging”之外,我们还可以使用Paging组件来实现分页。详情请参照后续Paging分页的介绍。
4.2.4 使用Paging组件来为Grid分页
使用Paging组件,我们可以自定义的放置paging组件的位置。还可以使用一个Paging组件来控制多个grid,这时需要指定Grid组件的paginal属性为同一个Paging。
例:
"pg" pageSize="3" />
"300px" mold="paging" paginal="${pg}" >
"Left" />
"Right" />
|
"Item 1.1" />
"Item 1.2" />
|
"Item 2.1" />
"Item 2.2" />
|
"Item 3.1" />
"Item 3.2" />
|
"Item 4.1" />
"Item 4.2" />
"300px" mold="paging" paginal="${pg}" >
"Left" />
"Right" />
|
"Item A.1" />
"Item A.2" />
|
"Item B.1" />
"Item B.2" />
|
"Item C.1" />
"Item C.2" />
|
"Item D.1" />
"Item D.2" />
|
"Item E.1" />
"Item E.2" />
效果图:
4.2.5 Paging组件的onPaging事件
当用户点击paging组件的时候,会触发onPaging事件。如果想实现后台自动分页,我们就可以通过这个事件来实现。
例:
"win" title="分页" border="normal" width="400px" >
"paging" onCreate="init()" >
"编号" />
"名字" />
"rowsId">
"myPaging" pageSize="3" onPaging="nextPage(event)" />
import org.zkoss.zul.event.PagingEvent;
class Student{
private int id;
private String name;
public Student(){}
public Student(int id, String name){
this .id = id;
this .name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this .id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this .name = name;
}
}
List stuList = new ArrayList();
Student stu1 = new Student(1, "张三");
Student stu2 = new Student(2, "李四");
Student stu3 = new Student(3, "王五");
Student stu4 = new Student(4, "朱红");
stuList.add(stu1);
stuList.add(stu2);
stuList.add(stu3);
stuList.add(stu4);
final int PAGE_SIZE = myPaging.getPageSize();
public void nextPage(Event event){
PagingEvent pe = (PagingEvent) event;
int pgno = pe.getActivePage();//页数(从零计算)
int start = pgno * PAGE_SIZE;
int end = start + PAGE_SIZE;
end = end>stuList.size()?stuList.size():end;
System.out.println(start+","+end);
rowsId.getChildren().clear();//清空数据
createData2View(start, end);
}
private void createData2View(int start, int end){//显示数据
for (int i=start; i Student stu = (Student)stuList.get(i);
Row row = new Row();
Label lab1 = new Label();
lab1.setValue(stu.getId()+"");
Label lab2 = new Label();
lab2.setValue(stu.getName());
lab1.setParent(row);
lab2.setParent(row);
rowsId.appendChild(row);
}
}
public void init(){
createData2View(0, 3);
myPaging.setTotalSize(stuList.size());//总数据量
}
]]>
效果图:
除了可以在onPaging事件上写函数之外,我们还可以使用下面的代码来为Paging组件添加事件监听器:
pagingId.addEventListener("onPaging", new EventListener() {
public void onEvent(Event event) {
……
……
……
}
};
4.2.6 Grid的排序功能
通过在column上设置属性sort,即可实现排序功能。详情请参见Listbox的排序。
4.2.7 使用grid的Model来动态展现数据
例:
"Live Grid" border="normal" width="150px" >
String[] data = new String[10];
for (int j = 0; j < data.length; ++j) {
data[j] = "option " + j;
}
ListModel strset = new SimpleListModel(data);
]]>
"100px" model="${strset}" >
"options" />
效果图:
4.2.8 Grid的辅助标题(Auxiliary Headers)
除了使用columns来添加表头以外,我们还可以使用辅助表头auxhead来添加表头。
auxhead支持rowspan和colspan属性,这是column所不支持的。
columns和column只能用在grid组件里面,而auxheader、auxhead可以用在Listbox、Grid和Tree组件里面。
例:
"H1'07" colspan="6" />
"H2'07" colspan="6" />
"Q1" colspan="3" />
"Q2" colspan="3" />
"Q3" colspan="3" />
"Q4" colspan="3" />
"Jan" />
"Feb" />
"Mar" />
"Apr" />
"May" />
"Jun" />
"Jul" />
"Aug" />
"Sep" />
"Oct" />
"Nov" />
"Dec" />
|
"1,000" />
"1,100" />
"1,200" />
"1,300" />
"1,400" />
"1,500" />
"1,600" />
"1,700" />
"1,800" />
"1,900" />
"2,000" />
"2,100" />
效果图:
4.2.9 使用span属性来合并单元格
例:
"500px">
"Left" align="left" width="150px" />
"Center" align="center" width="150px" />
"Right" align="right" />
"Column 4" />
|
"Item A.1" />
"Item A.2" />
"Item A.3" />
"Item A.4" />
"1,2,1">
"Item B.1" />
"Item B.2" />
"Item B.4" />
"3">
"Item C.1" />
"Item C.4" />
",,2">
"Item D.1" />
"Item D.2" />
"Item D.4" />
|
"Item A.1" />
"BUTTON" />
[微软用户1]
"Item A.2" />
"BUTTON" />
"Item A.3" />
"Item A.4" />
效果图:
4.2.10 Grid组件的常用属性
属性名
说明
sizedByContent
根据内容改变大小。取值:true、false
一般与span=”true”连用。例如:
"true" span="true" width="800px" >
span
当设置了sizeByContent后,每一列都只会占用它需要的宽度。如果我们想每一列都看起来宽松一点的话,就可以设置span=”true”
emptyMessage
当展现的内容为空时,就会显示我们设置的emptyMessage中的内容。
例如:"test1" emptyMessage="没有相关数据" >
sizable
columns组件的属性,设置sizable=”true”后,用户就可以使用鼠标来更改列宽。例如:"true">
4.2.11 Grid表头上的弹出菜单:Menuepopup
默认情况下menuepopup被设置成none,我们可以在columns组件上设置menuepopup=”auto”来显示一个默认的弹出菜单。你也可以为组件设置自己定义的弹出菜单。
① 默认的Menuepopup:
例:
"300px">
"auto">
"第一列" />
"第二列" />
|
"Item 1.1" />
"Item 1.2" />
|
"Item 2.1" />
"Item 2.2" />
|
"Item 3.1" />
"Item 3.2" />
效果图:
鼠标移上点击→
(勾选的列即为显示出来的列)
② 自定义的Menuepopup
例:
"editPopup">
"Group" image="/imgs/sml1.gif" />
"Sort Ascending" image="/imgs/sml2.gif" />
"Sort Descending" image="/imgs/sml3.gif" />
"300px">
"editPopup">
"第一列" />
"第二列" />
|
"Item 1.1" />
"Item 1.2" />
|
"Item 2.1" />
"Item 2.2" />
|
"Item 3.1" />
"Item 3.2" />
效果图:
4.2.12 Grid中Detail组件的使用
detail组件是用来展示详情部分,主行和详情部分在同一行上。
例:
"min" />
"productName"/>
"Price"/>
"Item Location"/>
|
"true">
"myimg" width="100px" height="100px"
src="/imgs/tea1.gif" />
这里面可以有其他的组件来展示详细信息。也可以直接使用文字来描述……
"商品1"/>
"$250"/>
"美国"/>
效果图:
展开时:
收拢后:
4.2.13 foot组件
用来显示grid的foot。
例:
"300px">
"Type" width="50px" />
"Content" />
|
"File:" />
"99%" />
|
"Type:" />
"1" mold="select" >
"Java Files,(*.java)" />
"All Files,(*.*)" />
"Browse..." />
效果图:
4.2.14 Grid中使用Group组件分组
支持groupfoot。
例:
"800px">
"true">
"Brand" />
" Processor Type " width="150px" />
"Memory (RAM)" width="120px" />
"Price" width="100px" />
"Hard Drive Capacity" width="150px" />
"Dell" />
|
"padding-left:15px" value="Dell E4500 2.2GHz" />
"Intel Core 2 Duo" />
"2GB RAM" />
"$261.00" style="color:green" />
"500GB" />
|
"padding-left:15px" value="XP-Pro Slim Dell-Inspiron" />
"Intel Core 2 Duo " />
"2GB RAM" />
"$498.93" style="color:green" />
"500GB" />
|
"padding-left:15px" value="Dell P4 3.2 GHz" />
"Intel Pentium 4" />
"4GB RAM" />
"$377.99" style="color:green" />
"500GB" />
"Compaq" />
|
"padding-left:15px" value="Compaq SR5113WM" />
"Intel Core Duo" />
"1GB RAM" />
"$279.00" style="color:green" />
"160GB" />
|
"padding-left:15px" value="Compaq HP XW4200" />
"Intel Pentium 4" />
"4GB RAM" />
"$980" style="color:green" />
"500GB" />
"5">
"This a summary about Compaq Desktop PCs" />
效果图:
收起来:
4.3 List组件
相关子组件:listbox、listitem、listcell、listhead、listheader
listbox组件可以用来展示列表数据。用户可以选中列表数据中的某一条记录。
listbox的mold:default、select、paging。
如果我们使用mold=”select”,那么它将被解析成HTML的select标签。
4.3.1 List组件概观
例1:
"listbox demo" border="normal" width="250px" >
"box">
"true">
"name" sort="auto" />
"gender" sort="auto" />
"Mary" />
"女" />
"John" />
"男" />
"Jane" />
"女" />
"This is footer1" />
"This is footer2" />
效果图:
4.3.2 mold为select的lisbox
当listbox的mold属性为select时,可以作为一个下拉框来使用。
例:
"select" rows="1" width="70px" >
"Car" />
"Taxi" />
"Bus" selected="true" />
"Train" />
效果图: →
4.3.3 listbox的常用属性
属性名
说明
sizedByContent
根据内容改变大小。取值:true、false
一般与span=”true”连用。例如:
"true" span="true" width="800px" >
span
当设置了sizeByContent后,每一列都只会占用它需要的宽度。如果我们想每一列都看起来宽松一点的话,就可以设置span=”true”
emptyMessage
当展现的内容为空时,就会显示我们设置的emptyMessage中的内容。
如:"test1" emptyMessage="没有相关数据" >
sizable
listhead组件的属性,设置sizable=”true”后,用户就可以使用鼠标来更改列宽。例如:"true">
4.3.4 listbox的分页
同grid组件类似,只要在listbox组件上加上属性mold="paging" pageSize="n" 即可。如果不指定pageSize,则会使用默认值20。
4.3.5 listbox的自动排序功能
我们通过listheader上的sort属性来实现listbox的自动排序。默认情况下的设置为sort=”auto”。它能对这一列所展示的字符串进行自动排序。如果我们想自定义的控制,根据指定的字段来排序,可以使用sort="auto(field1[,field2...])"的格式来指定。
① 默认的排序功能
例:
"400px" mold="paging" pageSize="2" >
"true">
"序号" sort="auto" hflex="min" />
"名字" sort="auto" />
"性别" sort="auto" />
"1" />
"Mary" />
"女" />
"2" />
"John" />
"男" />
"3" />
"Jane" />
"女" />
效果:
② 根据指定的字段来排序,使用sort="auto(field1[,field2...])"的格式
例:
"Test auto(FIELD_NAME1,FIELD_NAME2)" border="normal" width="300px" >
class Person {
private String firstName;
private String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this .firstName = firstName;
this .lastName = lastName;
this .age = age;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getFullName() {
return firstName + " " + lastName;
}
public int getAge() {
return age;
}
}
java.util.List persons = new java.util.ArrayList(8);
persons.add(new Person("Tom", "Yeh", 43));
persons.add(new Person("Henri", "Chen", 43));
persons.add(new Person("Jim", "Yeh", 39));
]]>
"@{persons}">
"Full Name"
sort="auto(lastName, firstName)" />
"Age" sort="auto(age)" />
"@{each=person}">
"@{person.fullName}" />
"@{person.age}" />
效果图:
→
4.4 Tree组件
tree组件可以实现与listbox相似的功能。但是我们通常都是用tree组件来做树状的菜单。这里不做过多介绍。
请看下面的例子:
"main" width="100%" height="100%" border="none" >
"200px" splittable="true" flex="true" collapsible="true" title="功能菜单" >
"tree" zclass="z-dottree" width="150px" rows="100" >
"/imgs/menu.png"/>信息汇总
"/imgs/menu.png"/>基本信息
"/imgs/menuitem.png"/>班级信息
"/imgs/menuitem.png"/>学生信息
"/imgs/menuitem.png"/>我的信息
效果图:
4.5 开发中的ZK常用组件
4.5.1 A组件
例1:
效果图:
a组件中的路径都是相对路径。如果路径是以斜杠(/)开头,那么则表示是当前的context path(上下文路径)。其他的路径都是相对于文件本身的路径而言的。
a组件可以包含其他的组件,但是它的子组件也要能处理鼠标点击事件。
4.5.2 Button组件
按钮组件有两种类型,button和toolbarbutton,它们的主要功能是类似的,只是其外观显示不同。
在解析时,button组件使用html中的button标记,而toolbarbutton则使用的是html中的a标记。
① button组件的属性
属性名
说明
label
指定按钮上的文字
image
为按钮指定图像
dir
用于控制图像和文字哪个显示在前面,默认是图像显示在前面。
若dir="reverse"则文字显示在前面
orient
控制布局是横向还是纵向,默认是横向布局。
orient="vertical"则为纵向布局
onClick
按钮被点击后进行的响应
href
按钮被点击后跳转的页面
mold
取值有:default、trendy、os
autodisable
通过设置这个属性,可以防止重复提交。
例:
"Btn" autodisable="+self" onClick='text.value="点击了Btn"' />
"ok" label="确定" autodisable="ok,cancel" onClick='text.value="点击了确定"' />
"cancel" label="取消" autodisable="+ok,+cancel" onClick='text.value="点击了取消"' />
"text"/>
效果图:
如果在按钮的id前面加上加号(+),则点击一次后被指定的按钮将变成disabled,需要我们手动变回enabled;如果不加+,则点击时的这次request完成以后,按钮将自动从disabled变成enabled
② 配置button默认的属性
在zk.xml中做如下设置,则可以使按钮的默认mold变成trendy:
org.zkoss.zul.Button.mold
trendy
在zk.xml中做如下设置,则可以使按钮的autodisable指向它自身:
button
button
autodisable
self
③ button上面使用图片
例1:
"按钮应用" border="normal" width="40%" >
"文字left" image="/imgs/sml1.gif" dir="reverse" onClick="view.value=self.label" href="/index.zul" />
"文字right" image="/imgs/sml2.gif" onClick="view.value=self.label" />
"文字below" image="/imgs/sml3.gif" orient="vertical" onClick="view.value=self.label" />
"文字above" image="/imgs/sml4.gif" orient="vertical" dir="reverse" onClick="view.value=self.label" />
"view"/>
效果图:
④ 按钮的onClick与href属性
我们可以通过两种方式为一个按钮指定动作:一是通过onClick来监听;二是使用href来指定一个路径。这两种方式中,href的优先级要高于onClick。如果同时指定两者,则onClick不会被发送。
⑤ 使用html中的button标签
通过指定xml命名空间为native,则ZK解析器会按照html的格式来解析这个命名空间下的组件。(很少用)
例:
(实际应用中,我们可以使用下面的代码跳转到一个jsp页面,或者一个actoin中,等等)
4.5.3 验证码Captcha
例:
验证码:"tb" value="请输入" onFocus='self.setValue("")' onBlur="check()" />
"cpa" length="5" width="200px" height="50px" />
void check(){
if (!tb.getValue().toLowerCase().equals(cpa.getValue().toLowerCase())){
Messagebox.show("请输入正确的验证码");
tb.setValue("请输入");
}
}
]]>
效果图:
4.5.4 combobutton组件
combobutton是一种特殊的按钮,它里面可以包含一个popup或者menuepopup做为child
例:
"popup" image="/imgs/sml3.gif" autodrop="true" >
Search
"200px">
"Name" />
"Description" />
"John" />
"CEO" />
"Joe" />
"Engineer" />
"menu popup" image="/imgs/sml2.gif" >
"Index" />
"Menu">
"Color Picker" content="#color=#029BCB" />
autodrop属性用于设置鼠标滑过时是否自动打开弹出框。
效果图:
→ →
4.5.5 Fisheye组件
例:
"450px">
"Attach icon edge at bottom"
onCheck='fsb.attachEdge=self.checked?"bottom":"top"' />
"Vertical orient"
onCheck='fsb.orient=self.checked?"vertical":"horizontal"' />
"true" />
"fsb"
style="position:absolute;margin:80px 150px;" attachEdge="top"
itemWidth="80" itemHeight="80" itemMaxHeight="160"
itemMaxWidth="160" >
image="/imgs/button04.jpg"
label="folder" onClick="alert(self.label)" />
image="/imgs/button05.jpg"
label="search" onClick="alert(self.label)" />
image="/imgs/button06.jpg"
label="project" onClick="alert(self.label)" />
image="/imgs/button08.jpg"
label="Email" onClick="alert(self.label)" />
image="/imgs/button21.gif" label="Globe"
onClick="alert(self.label)" />
image="/imgs/button23.gif"
label="telescope" onClick="alert(self.label)" />
效果图: →
→
→
4.5.6 HTML组件
如果我们要在页面上直接使用html标签而不让zk的解析器来解析,那么我们就可以使用html组件。配合一起使用。
注意:html组件中的内容不是这个组件的child。我们可以在里面使用EL表达式
例:
"win" title="Html Demo" border="normal" >
Hi, ${win.title}
It is the content of the html component.
]]>
效果图:
4.5.7 Iframe组件
iframe组件和HTML中的ifram标签的功能相同。
iframe组件和include组件很相似,但是它们有很大的不同:
◆ include包含进来的子页面是属于当前的页面的,也就是属性当前desktop的一部分。我们可以直接访问include包含进来的组件。包含的动作是在server端,browser对此一无所知。
◆ iframe包含进来的子页面是作为一个单独的页面来加载的。包含的动作发生在browser端。
例:
"win" title="This is an Iframe Demo!" >
4.5.8 Include组件
include组件是一个ID空间宿主。所以不用担心两个include组件中的id会重复。
src属性可以指定为一个servlet、jsp、zul等等。
Include可以包含ZUML页面、静态页面、JSP页面或者一个servlet。
当我们使用Include包含的是一个非ZUML的页面时,那么那个页面中的内容将会被作为Include组件的内容。
当我们使用Include包含一个ZUML页面时,那个被包含的ZUML页面中的组件将会作为Include组件的子组件
例:
"another.zul"/>
"another.jsp"/>
简单的include用法我们就不再多言。我们重点讲一下include关于url的参数传递。
① include中通过路径来传递参数
include.zul页面:
"include demo" border="normal" width="300px" >
class User{
private int id;
private String name;
private String sex;
public User() {
}
public User(int id, String name, String sex) {
this .id = id;
this .name = name;
this .sex = sex;
}
public int getId() {
return id;
}
public void setId(int id) {
this .id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this .name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this .sex = sex;
}
}
User user = new User(1, "小张", "女");
]]>
Hello, World!
"temp.zul?text=5" />
"temp.zul" some="something" another="${user}" />
使用include时,两种传值的方式
temp.zul页面:
"temp页面" border="normal" width="280px" >
"${param.text ne null}" value="1.param.text:${param.text}" />
"true"/>
"${requestScope.some ne null}" value="2.equestScope.some:${requestScope.some}" />
"true"/>
"${requestScope.another ne null}" value="3.requestScope.another:${requestScope.another}、${requestScope.another.id}、${requestScope.another.name}、${requestScope.another.sex}" />
"true" />
效果图:
通过代码的话,我们可以使用:Include.setDynamicProperty(java.lang.String, java.lang.Object)来传递参数。
接收传递过来的参数时,我们可以使用:Execution.getParameter(java.lang.String)或者使用javax.servlet.ServletRequest的接口,也就是说我们可以通过EL表达式来得到传递的参数值。
通过url,我们只能传递string类型的参数。传递复杂数据类型的参数时请参照上面的例子。
② 刷新include包含进来的页面
例:
"include demo" border="normal" width="300px" >
"color:red">Hello, World!刷新include进来的页面
"inner" src="/components/_essentialComponent/003captcha/captcha.zul?text=5" />
"Load" onClick='inner.src = "/components/_essentialComponent/003captcha/captcha.zul?text=10"' />
"reload1" label="Reload1" onClick="String tmp=inner.src; inner.src=null; inner.src=tmp;" />
"reload2" label="Reload2" onClick="inner.invalidate();" />
解析:
1、Load按钮所设置的的路径是恒定不变的。当第二次再点击时,就不会进行刷新了。
2、通过先设置inner.src=null后,再设置它的值,这样就可以完成刷新同一个页面了。例如:Reload1按钮
3、通过inner.invalidate()也可以实现相同的效果
4.5.9 Image组件
例:
Location: "updateMap(self.value)"/>
Map: "image"/>
void updateMap(String location) throws Exception{
if (location.length() > 0) {
org.zkoss.image.AImage img = new org.zkoss.image.AImage(location);
image.setContent(img);
}
}
]]>
"/imgs/folder.gif" />
效果图:
Image组件还支持javax.awt.image.RenderedImage产生的图片:
例:
"Test of Live Image">
"img"/>
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;
int x = 10, y = 10;
void draw(int x1, int y1, int x2, int y2) {
BufferedImage bi = new BufferedImage(400, 300, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = bi.createGraphics();
Line2D line = new Line2D.Double(x1, y1, x2, y2);
g2d.setColor(Color.blue);
g2d.setStroke(new BasicStroke(3));
g2d.draw(line);
img.setContent(bi);
}
draw(x, y, x += 10, y += 10);
]]>
"change" onClick="draw(x, y, x += 10, y += 10)" />
4.5.10 Imagemap组件
例:
"Test of Imagemap">
点击图片显示点击的坐标
"/imgs/map.gif" onClick='alert(event.x + "," + event.y )' />
点击我们设置的区域,显示对应的区域id
"/imgs/map.gif" onClick="alert(event.area )" >
"First" coords="0, 0, 100, 100" />
"Second" shape="circle" coords="200, 200, 100" />
第一个imagemap的效果图:
4.5.11 Label组件
Label组件是一个特别常用的组件。用法也很简单。我们这里主要讲一下它的3个属性:pre、multiline和maxlength
属性名
说明
pre
用来屏蔽多余的空白字符。多余即首尾的空白字符(处理后被去掉),和中间多个空白字符(处理后变成一个)。
multiline
取值:true、false。是否显示多行
maxlength
最多显示几个字符
例:
"win" title="demo" border="normal" >
"result">
"lbl1" pre="true" >
"true" />
"lbl2" multiline="false" />
"true" />
"lbl3" maxlength="10" />
lbl1.value = " this thing has spaces.\nnext line.";
lbl2.value = " this thing no space.\nnext line.";
lbl3.value = " this is more than 10 chars.";
]]>
效果图:
4.5.12 Menue组件
例:
"menubar">
"File">
"New" onClick="alert(self.label)" />
"Open" onClick="alert(self.label)" />
"Save" onClick="alert(self.label)" />
"Exit" onClick="alert(self.label)" />
"Help">
"Index" onClick="alert(self.label)" />
"About">
"About ZK" onClick="alert(self.label)" />
"About Potix" onClick="alert(self.label)" />
效果图:
→ →
5 根据需求和条件手动控制页面的加载和组件的创建
5.1 根据条件来控制组件的创建
ZK中提供了很多根据条件来创建组件的方式。注意他们和visible的区别。visible是控制组件的显示和隐藏,而这里要介绍的是控制组件是否创建。
5.1.1 if和unless属性(适用于所有组件)
基本上所有的zk组件都有if和unless属性,他们用于控制组件是否被创建。
例1:当param.readonly为true时,我们使用label组件,否则使用textbox组件
"${customer.label}" if="${param.readonly == 'true'}" />
"${customer.value}" unless="${param.readonly == 'true'}" />
例2:只有当a等于1,b不等于2的时候,这个window才会被创建
"${a==1}" unless="${b==2}" >
...
5.1.2 switch和case(适用于zk标签)
我们一般用switch case组合来控制ZUML页面的创建。第一个匹配成功的条目才会被创建,只要一个匹配成功,就不再做匹配。没有case属性的zk标签是默认的匹配项。
例:
"${fruit}">
"apple">
当${fruit}值为apple时被创建
"${special}">
当${fruit}值为${special}时被创建
当上面没有一个匹配成功时被创建
我们也可以为case指定多个值,只要其中的一个能够匹配成功,那么整个表达式就匹配成功,该zk标签中的内容将被创建。
例:
"${fruit}">
"apple, ${special}">
当${fruit}的值为apple 或 ${special} 时被创建
我们还可以在case中使用正则表达式:
例:
"${fruit}">
"/ap*.e/">
Evaluate if the regular expression, ap *.e"., matches the switch
condition.
5.1.3 choose和when(适用于zk标签)
choose when的功能与switch case差不多,这里不再过多介绍。
例:
"">
"${fruit == 'apple'}">
Evaluated if the when condition is true.
Evaluated if none of above cases matches.
注:你不需要为choose属性指定值,它的作用仅仅是为了标识choose的结束,即:通过它可以找到choose结束的地方
5.2 根据需求加载页面或组件
一个ZUML文件可以被划分成多个页面,一般情况下,这些页面都是同时被加载进来的。这样,在页面首次被加载的时候将会消耗很多带宽,为了提高性能,我们可以通过fulfill属性来延迟页面的加载,也就是我们说的根据需求来加载。
不仅是页面可以延迟加载,组件也是可以延迟加载的。例如下拉框,我们在点击它的时候才加载里面的数据的话,我们也可以为其设置fulfill属性
例1:当combobox收到了onOpen的事件后再加载里面的数据
"onOpen">
"First Option"/>
例2:通过事件触发和组件的id来指定组件的加载
"btn" label="show" onClick="content.visible = true" />
"content" fulfill=
"btn.onClick" >
Any content created automatically when btn is clicked
不属于同一个id空间的两个组件,我们可以通过路径来指定:
"btn" label="show" onClick="content.visible = true" />
"content" fulfill="../btn.onClick" >
Any content created automatically when btn is clicked
按需加载一个页面:
我们也可以为fulfill属性指定一个页面。这个延迟加载的页面将会作为相应的组件的children。
"btn" label="Click to Load" />
"btn.onClick=another.zul"/>
6 迭代赋值
默认情况下,ZK加载器为zuml页面中的每个XML元素实例化一个组件。如果我们想生成一个组件集的话,我们可以通过forEach来实现。
例:
"${each}" forEach="Apple, Orange, Strawberry" />
它等同于
"Apple"/>
"Orange"/>
"Strawberry"/>
也等同于
String[] grades = new String[] {"Best", "Better", "Good"};
]]>
"${each}" forEach="${grades}" />
当ZK加载器通过集合中的项目来迭代的时候,它会更新两个隐含对象:each和forEachStatus。
这种迭代方式依赖于forEach属性的值的类型。
forEach属性的值必须是一个集合类型。大致有以下几种情况:
① java.util.Collection:迭代Collection集合中的每个元素
② java.util.Map:迭代Map中的每一个Map.Entry
③ java.util.Iterator:迭代Iterator中的每个元素
④ java.util.Enumeration:迭代Enumeration中的每个元素
⑤ Object[]、int[]、short[]、byte[]、char[]、float[]、double[]:迭代数组中的每个元素
⑥ null:不会生成任何组件,将会被忽略
看下面这个例子:
String[] classes = new String[] {"College", "Graduate"};
Object[] grades = new Object[] {
new String[] {"Best", "Better"}, new String[] {"A++", "A+", "A"}
};
"200px" forEach="${classes}" >
"${each}"/>
"${forEachStatus.previous.each}: ${each}"
forEach="${grades[forEachStatus.index]}" />
效果图:
将 去掉后的效果图:
解析:forEach就好比一个for循环,它循环的范围就是forEach所在的标签的开始标签至结束标签处。故,在上例中,listbox处的forEach循环它里面的listhead和listitem组件。我们不难发现,这是一个两重循环。listitem处还有一个forEach,也就是第二重循环。循环第一次开始时,listheader处的each被赋值为”College”。listitem处又有一个forEach,这时each从属于listitem处的forEach,这时我们要引用上一层循环中的each的话,只能通过forEachStatus.previous.each 来得到。
注:forEach好比一个for循环,each就好比一个iterator
forEach和each组合在一起可以与下面的代码类比:
while (iterator.hasNext()){//forEach开始处
each = iterator.next();//给each属性赋值
}
我们可以在zscript中直接使用each和forEachStatus:
"${each}" forEach="apple, orange" >
self.parent.appendChild(new Label("" + each));
]]>
效果图:
你不能在事件监听器中获得each和forEachStatus的值,因为当页面加载完成后,each已经不存在了,而这个时候我们再去触发事件想要得到each中的值的话,就不能够实现了。
一个简单的解决办法就是在页面加载的时候就将第一个each的值都用组件的属性存放起来,这样的话,当页面加载完成后,我们就可以通过检索预先存放的属性值来得到我们想要的值了。
例:
"${each}" forEach='"中国","美国"'
onClick='alert(self.getAttribute("country"))' >
"${each}"/>
注:用户自定义属性,属性名是随便取的,但是标签名一定是custom-attribute
迭代集合中的一部分:通过指定forEachBegin和forEachEnd
"${foos}" forEachBegin="${param.begin}" forEachEnd="${param.end}" >
${each.name} ${each.title}
7 XML命名空间简介
在ZK中标准的命名空间有3个:client、native、zul
ZK解析器在加载一个页面的时候,首先通过页面的拓展名来确定页面默认的命名空间。
在ZK工程中一般有两种页面:.zul和.zhtml。.zul的页面默认按zul的命名空间来进行解析。.zhtml的页面默认按native的命名空间来解析(即把它当成一个普通的html页面)。
如果我们想在默认的命名空间下使用不同的命名空间,让zk解析器按指定的命名空间来解析的话,我们就可以通过下面的方式来指定页面的命名空间。
① 指定由客户端(前台)来控制组件,而不是后台
"client" w:onFocus="this.open()" onChange="doOnChange()" />
② 使用html的形式来解析组件(在zul页面中)
"native">
Username
Password
"password"/>
③ 使用zul的形式来解析组件(在zhtml页面中)
8 Zscript的使用
zscript是zuml页面上的页面脚本。
zuml页面中的脚本分两种:1、客户端脚本 2、服务器端脚本
zscript是属于服务器端脚本。里面的代码是java代码,它运行在服务器端,所以它能够访问java包,在它里面你也可以定义变量、方法甚至是类,在同一个page下它对EL表达式是可见的。也就是说,EL表达式可以使用中的变量、方法、类等。
在zscript中的代码必须是一段有效的xml文本。所以,在用要特殊字符的时候我们需要转义。为了避免转义,我们通常使用CDATA来包围这段代码。
书写zscript的方式大致有三种:
1、标准格式:
2、将zscript写在事件监听器上
如下面的例子
"reload1" label="Reload1" onClick="String tmp=inner.src; inner.src=null; inner.src=tmp;" />
3、将zscript写在attribute中
如果处理事件处理器的代码比较复杂,我们可以在attribute元素中指定事件的名字,然后编写代码。上面的代码可以写为:
"reload1" label="Reload1" >
"onClick"> String tmp=inner.src;
inner.src=null;
inner.src=tmp;
]]>
zscript中的变量对EL表达式是可见的:
例:
Date now = new Date();
]]>
'${now}'/>
9 在ZUML页面上使用tld文件中指定的函数
在页面上,我们经常会对一些内容进行格式化,然后再输出。通常我们都将这些转换函数写成一个公有的函数。在ZK中我们可以通过以下步骤来实现在页面上调用指定的函数:
1、 用一CommonUtils类将所有的公共函数写在一起:
例:
public class CommonUtils {
public static final String TIME_PATTERN_DEFAULT = "yyyy-MM-dd HH:mm:ss"; privatestatic String getPattern(String pattern){
if ("".equals(pattern) || null ==pattern){
pattern = TIME_PATTERN_DEFAULT ;
}
return pattern;
}
public static String parseDateToString(Date date, String pattern){
String redate = null ;
pattern = CommonUtils.getPattern (pattern);
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
redate = sdf.format(date);
return redate;
}
}
2、 新建一个tld文件来描述CommonUtils类中的每一个公共函数
parseDateToString
com.cn.developer.common.CommonUtils
java.lang.String parseDateToString(java.util.Date, java.lang.String)
Returns a formatted date.
3、 在页面上引用我们的tld文件
"win" title="引用tld文件中的函数" border="normal" width="200px" >
Date now = new Date();
]]>
'${c:parseDateToString(now, "yyyy-MM-dd") }'/>
访问上面的页面的效果图:
10 自定义变量解析器
将EL表达式中特定的名字转换成我们指定的值。
下面举一个例子:
"${each.name}" forEach="${customers}" />
如果直接访问这个页面,肯定会报错。因为zk无法解析customers。下面的操作将会解析出这个页面。
首先,我们定义一个类,这个类要实现VariableResolver接口:
public class CustomerResolver implements VariableResolver {
@Override
public Object resolveVariable(String name) throws XelException {
if ("customers".equals(name))
return Customer.getAll ("*");
return null ; //not a recognized variable
}
}
在Customer类中加入静态的方法Customer.getAll (java.lang.String)
public class Customer {
private int id;
private String name;
private String sex;
public Customer() {
}
public Customer(int id, String name, String sex) {
this .id = id;
this .name = name;
this .sex = sex;
}
//略去getter和setter
//..........
public static List getAll(String str){
List list = new ArrayList();
list.add(new Customer(1, "张三", "男"));
list.add(new Customer(2, "李四", "女"));
return list;
}
}
resolver.zul:
"${each.name}" forEach="${customers}" />
最后得到的效果:
我们可以将这个Resolver设置成全局的,而不需要每个页面都引用,可以在zk.xml中加入下面的代码:
com.cn.developer.resolver.CustomerResolver
10 ZK-MVVM开发模式
ZK6.0之前的版本都是采用MVP(Model-View-Presenter)设计模式。从ZK6.0开始就有了很大的不同,采用MVVM(Model-View-ViewModel)设计模式。
10.1 ZK MVP开发模式
我们通过一个例子来剖析MVP的开发模式。
假如我们要实现下面的功能:Client客户端页面上点击一个按钮,然后从Server服务端返回消息”Hello World”
MVP通过下图所示来实现:
作用过程:
①用户在屏幕上点击按钮(一个动作action),一个通信事件就被触发。
②后台的事件监听器(event listener)处理这个事件(event)。
③访问数据库(access data)并返回
④用户界面元素(UI element)被改变,相应的把视觉反馈(visual feedback)呈现给用户
相关的代码如下:
10.2 MVVM开发模式
实现与2.1中相同的功能,MVVM的实现过程如下图:
作用过程:
①用户在屏幕上点击按钮(action)触发一个事件(event)
②绑定器(Binder)感知到相应的事件(corresponding event)被触发
③绑定器(Binder)在ViewModel中找到相应的动作逻辑(action logic)并调用它
④动作逻辑(action logic)从Model层访问数据库(access data),并更新ViewModel中相应的属性
⑤ViewModel通知(notify chnage)绑定器(Binder) 一些属性被改变
⑥每当有属性被改变,绑定器(Binder)就会从ViewModel中加载数据(load data)
⑦绑定器(Binder)改变相应的UI组件来给用户呈现视觉反馈(visual feedback)
很明显,UI设计者必须至少告诉绑定器(Binder)以下几点:
◆ 用到了哪些UI事件(UI event)来触发哪些动作逻辑(action logic)。(这样Binder才知道调用什么方法)
◆ 用到了哪些UI属性(UI attribute)来显示哪些数据。(这样Binder才知道加载什么数据,更新什么数据)
◆ 用到的哪些UI属性(UI attribute)是用来输入到数据库的。(这样Binder才知道要保存哪些属性)
相关代码如下:
在ZK绑定(ZK Bind)中用到了ZK注解(annotation)来完成了很多工作。
结合代码,我们再一次分析程序运行的过程如下:
① 当用户按下了”Show”按钮,onClick event就被触发到了Binder
② ZK Binder通过指定的ZK 注解 @command('showHello')在ViewModel中找到命令的名字(command name)为showHello的方法。
③ Binder在HelloViewModel.java中调用showHello()方法,并且改变message的属性值为”Hello World”。
注意:@command('showHello')是告诉Binder showHello这个方法是一个command方法,而@NotifyChange("message")则是告诉Binder 如果@command('showHello')中的showHello()方法被调用,那么HelloViewModel中的属性message将会被改变。也就是说当这个方法被调用后,页面上(客户端)可以感知到属性message的改变。
④ Binder找到HelloViewModel.java中与组件标签(component label)相关联的属性message的属性值(这是由于页面上message被指定了ZK注解@load(vm.message)的缘故)。因此它通过调用HelloWorldModel.java的message属性的getMessage()方法从vm.message中加载数据,并改变标签label的属性值。最后”Hello World!”就呈现到了客户端用户。
10.3 为什么要使用MVVM的设计模式
有时用户看了上面的页面之后,说想通过弹出窗(pop window)来显示信息message。这样的改变其实很容易,将信息放到一个模态窗口中:
MVP实现:
对于MVP设计模式而言,view层的zul页面修改了之后,Presenter层的java文件必须完全被重写,这时需要注入弹出窗口popwin、弹出窗口中的标签popwin$lbl
MVVM实现:
当客户改变View层的需求的时候,MVVM设计模式的优势就体现出来了。UI设计者可以独立的处理这些改变,而不用去改变HelloViewModel.java文件,因为客户需要的是改变message的呈现方式而不是message本身。
注意:模态窗口的显示或隐藏是通过判断message的值是否为空来控制的。(visible="@load(not empty vm.message)")
11 ZK数据绑定注解
Client端页面上的数据与Server端的ViewModel中的属性的绑定是通过注解来完成的。
如下图所示:
11.1 单向绑定与双向绑定
@save:将它绑定的标签对应的值保存到ViewModel中,
@load:将ViewModel中的对应的属性值加载到前台页面。
@bind:双向绑定,相当于是@save和@bind的一种简写方式。
双向绑定和我们通常实现的ajax功能类似。
11.2 模板绑定
我们可以将一个集合的值赋给model属性,再将model赋给template组件的name属性,这就是我们所说的模板绑定,其实就是将值绑定在template上。
通过模板绑定,我们可以迭代集合中的每一个元素,从而创建多个组件。template组件就好比一个while循环一样。
11.3 表单绑定
11.4 条件绑定
11.5 数据绑定中使用EL表达式
12 ZK开发中使用的注解
12.1 Client端ZUL页面上使用的注解
语法(syntax)
说明(explanation)
viewModel="@id(name) @init(expression)"
用于设置ViewModel。
◆ 一个拥有apply="org.zkoss.bind.BindComposer"属性的组件可以使用这个注解,如果没有这个注解,那么这个组件将会被设置到一个composer上。
◆ expression:如果它被赋予一个Class,那么这个Class将创建一个ViewModel实例。
comp-attribute="@load(expression)"
单向绑定。用来加载属性的表达式
◆ 用户可以在表达式中指定绑定条件,通过参数before和after来设定。例如@load(vm.filter, after='myCommand'),这表示Binder会在执行完command方法myCommand之后加载这个属性
comp-attribute="@save(expression)"
单向绑定。保存组件的属性值到ViewModel属性中的表达式。
◆ 用户可以在表达式中指定绑定条件,通过参数before和after来设定。例如:@save(vm.filter, before='myCommand'),这表示Binder会在执行command方法myCommand之前保存这个属性到ViewModel
comp-attribute="@bind(expression)"
双向绑定。在组件的属性和ViewModel的属性之间实现双向绑定,它不支持条件绑定,也就是说在绑定时不能使用条件。
◆ 它等同于@load(expression) @save(expression)
◆ 如果组件的属性不支持@save的话,Binder将会自动忽略它
◆ 通知改变(notify change)的例子:对于表达式vm.filter,如果任意一个通知(notification)说vm或vm.filter改变了,那么这个属性将会被重新加载
◆ 复杂一点的例子:对于表达式e.f.g.h,如果任何一个通知(notification)说e.f.g.h、e.f.g、e.f或者e被改变了,那么组件的属性将会被重新加载
@converter(expression, arg = arg-expression)
为绑定提供一个转换器conveter。
◆ 如果expression是一个ZK提供的conveter,那么它将会直接被使用
◆ 如果expression是一串字符,那么它将会从ViewModel中查找相应的getConveter方法,方法的名字为get+expression返回值为Conveter。参照上面提到conveter的代码。
◆ 在执行conveter之前,你可以传递多个参数给conveter。这些参数将会在调用conveter方法之前被赋值。
comp-event="@command(expression, arg =another-expression)"
组件的事件绑定,绑定到相应的事件command方法。
◆ expression必须是ViewModel中的一个command方法的名字(当然,这个方法必须是以@command为注解的)
◆ 事件被触发时,它将会按照ZK Bind Lifecycle来执行这个command方法
12.2 Server端ViewMole中使用的注解
语法(syntax)
说明(explanation)
@NotifyChange on setter
它是用于ViewModel类属性的setter方法上的,当调用了属性的setter方法之后,它将会通知(notify)Binder该属性的值被改变了。它是默认开启的,所以,当通知目标(notification target)和属性相同时,在属性的setter方法上你可以省略不写这个注解。
◆ 我们可以为属性properties指定name,通过@NotifyChange(expression)、@NotifyChange({expression1,expression2,……}),当指定的属性改变时调用这个setter方法
@NotifyChange on command method
它可以用于command方法上,当这个command方法被调用时,通知Binder属性值改变了
@NotifyChangeDisabled on setter
这个注解用在属性的setter方法上可以使setter方法默认开启的@NotifyChange关闭
@Command('commanName')
定义一个command方法。commandName是可选的,如果不指定的话,command方法的名字默认就是它所注解的方法的名字。
13 构建第一个MVVM页面
我将用一个查询的例子来展示你怎样利用ZK Bind在ZK 6中使用MVVM设计模式。请假设你正在创建一个查询页面,查询页面上显示的item清单是通过一个字符串条件检索出来的,选中的item会显示它的详细信息。
按照MVVM的设计理念,我们应该抛开视觉效果,首先设计ViewModel。一个ViewModel应该不依赖View层,但是要考虑到与View层交互的数据(data)和行为方法(action)。在这个例子中我们需要一个字符串作为检索条件,一个ListModelList- 来保存查询结果,一个doSearch()方法来执行查询命令(command)。此外,我们还需要一个域来保存当前选中的item。
我们可以看出,ViewModel是独立于View层的,这就意味着其他View也可以重复使用这个ViewModel。
通过在一个属性的getter方法上加上@NotifyChange,Binder设置了属性之后,Binder将会被通知属性已经被改变,被绑定属性的组件将会被重新加载。你也可以为command方法加上注解@NotifyChange,这样Binder执行了这个方法后,方法里面如果有指定属性被改变,那么与属性绑定的组件将会被重新加载。定义一个command,我们需要为方法加上Java注解@command,方法的名字就是command默认的名字。
通过为filter的setter方法setFilter()加上@NotifyChange,当Binder保存用户输入的filter值的时候,Binder将会被@NotifyChange通知来重新加载与filter属性相关的ViewModel。我们为doSearch() command方法加上了注解@NotifyChange({"items","selected"}),这是因为每次根据当前filter的值执行完查询后,我们都将创建一个新的items集合,并且需要将选中的属性item(selected)设置为空。当doSearch() command方法被调用后,Binder需要能够感知到这两个属性的改变。
注解@NotifyChange能够通知Binder什么时候自动加载View,和加载什么数据。
注意:
① @NotifyChange是默认激活的,因此你可以忽略它。然而,你想要改变通知的目标,那么你就必须要为不同的属性名加上@NotifyChange注解。
② 如果你想让一个属性默认的@NotifyChange失效,那么你就必须要为该属性的setter方法加上@NotifyChangeDisabled注解。
@id(name) @init(expression)是用于为Binder指定一个ViewModel,其中name是ViewModel的名字,expression是一个EL2.0表达式,用于指定绑定的类。
将filter值绑定到textbox上:
在上面的代码中,我们将vm.filter绑定到了textbox的value属性和button的disabled属性。而且,我们在disabled属性上使用了@load(expression),因为它只需要加载ViewModel中的filter属性的值。当我们在页面上修改textbox中的值的时候,vm.filter的值将会立即改变,相应的,按钮是否可点击状态也会立即改变,这取决于vm.filter是否为空。
将查询结果绑定到listbox上:
在ZK6中有一个新的功能叫template。当我们绑定一个集合的时候,使用它简直是绝配。
下面我们将用listbox和template来显示查询结果:
@load(expressoin)也可以被应用到listbox组件的model属性上。我们需要提供一个名为model(在这里template的name必须为model,与listbox中的model对应)的template来绑定到这个listbox组件的model上,用这个template来显示model中的每一个item。在这个template中,我们还需要给这个template的var属性设置一个值(例如示例代码中的item),这样我们就能用它来表示template中的数据元素了。
绑定当前选中的item:
当我们绑定listbox的时候,我们也将listbox中当前选中的item绑定到了ViewModel中的selected属性上。你不必担心当前选中的项目的数据类型不匹配,因为Binder会将它自动转换成Item(vm.items中存放的元素的类型就是Item,参照上面的代码)。
在上面的代码中我们将groupbox的visible属性与表达式not empty vm.selected绑定,因此,只有当用户选择了一个item之后,这个groupbox才会可见。
在这里,@converter又一次被用到了,但是和之前一次有所不同。现在这个converter来自于ViewModel。当然,我们需要在ViewModel中定义一个名为getTotalPriceConverter的方法,并且返回值的类型为Conveter。
将button的动作绑定到一个command方法:
现在我们需要在点击button按钮的时候执行一个ViewModel中的方法。为了实现这个功能,我们需要在button的onClick事件上添加一个@Command注解。
在组件的事件(event)属性上使用@command(expression)代表将这个事件绑定到ViewModel中的一个command方法上。
它的规则如下:
① 表达式expression必须是一个String字符串
② String字符串必须是ViewModel中的一个command方法的名字
③ 在ViewModel中必须有一个含有注解@Command('commandName')的可执行方法,如果不指定commandName,那么Binder就使用方法的名字作为command方法默认的名字。
实现的最终效果:
14 ZK6 MVVM结合Spring
尽管我们在ZVVM设计模式中使用Spring,但我们并没有把ViewModel设置成一个Spring的bean。主要有以下两个原因:
①在Spring中没有哪一种bean的范围(scope)是能够与ViewModel的生命周期(lifecycle)准确匹配的
②如果我们将ViewModel设置成一个Spring的bean,那么将会有两个变量引用同一个ViewModel,一个是@id(zul页面上指定的ViewModel的id),一个是Spring的bean的名字。这两个变量会很容易让开发者迷惑。
因此,我们就用最原始的方式来使用ViewModel,和激发Spring的变量解析器(variable resolver)
看下面的例子:
由于ViewModel不是Spring的bean,所以我们不能通过Spring的autowire(自动注入)来检索OrderService对象,但是,我们可以用ZK专门的注解@WireVariable来达到相同的效果。
将Conveter设计成一个单例bean:
有时我们想要用到一个Converter来完成类型或格式转换,但是在ZK自带的Conveter里面找不到合适的,这时我们通常是在相应的ViewModel中写一个getXXConveter的方法来返回一个Conveter(参见前面的例子)。 这样的Conveter没有重用性,为了提高组件的重用性,我们可以将我们需要用到的Conveter放到一个独立的类中,并把它们设置成一个bean,这样我们就可以在zul页面上直接使用这个conveter而不必在ViewModel中添加getXXConveter的方法。
代码如下:
由于Conveter不依赖于其他的bean,而且是无状态的,所以我们可以将它声明成单例bean。上面代码中的Conveter都需要接收一个format的参数作为格式化的Date的格式。
页面上使用:
同时,我们也可以将Validator设计成一个单例bean:
Validator和Conveter类似,我们可以将ViewModel中的Validator全部提取出来放到一个单独的类中。
这个验证器验证的域(shippDate)是基于另一个域(creationDate)的,并且将验证消息存入验证消息持有器中。
页面上使用:
这样设置之后,我们就不必在zul页面上使用”vm.”前缀来调用它们,因为它们可以被ZK的DelegatiingVariableResolver检索。任何一个zul页面都可以通过它们的bean name来得到相应的validator。
任何一个可以重复使用的元素都可以被分离开来,放到不同的包中。