1. 页面布局
ZK具有非常高的开发效率(以至于可以取代HTML用来完成高质量的Fast Prototyping),最主要缘自它采用的页面布局技术——ZUL;
采用XML语言以声明式的方式创建用户界面——XML UI技术并不是ZK的独创,Android UI、JavaFX、Microsoft Silverlight和Mozilla XUL等开发
框架都采用了这种技术,这是目前最先进的构造用户界面技术;
ZUL只不过是为所有ZK组件指定了一个对应的XML Element,然后通过这些Element以声明式的方式定义页面由哪些组件以什么样的方式构成。
相对于Java编程式方式,这种声明式方式的优点十分明显:
- 直观:ZUL代码结构与页面布局完全一致(而且必须一致),ZUL元素的嵌套关系就是页面组件的嵌套关系,ZUL元素的前后并列关系就
- 是页面组件的前后摆放;而Java编程方式与最终页面却没有这种一致性;
- 代码简洁:由于XML在表达页面布局时语义的先天优势(一致性),同样的页面用ZUL比Java代码量要少得多;
直观、简洁的代码意味着容易理解、容易编写、容易修改维护、不容易出错,因此带来开发效率上的巨大优势。
值得注意的是,上述ZUL相对于Java编程的优势也适用于JS,比如EXT、DOJO等JS UI框架。
1.1. 布局组件
1.1.1. 东西南北中布局Borderlayout
Borderlayout将屏幕划分成东西南北中五个区域,如下图所示,其灵活性可以实现绝大多数系统的首页整体布局。
首先纵向看,要指定N和S的高度,剩下中间部分就是W C E的高度;然后水平看,N S宽度百分百,中间部分指定W E的宽度后,
剩下的部分就是C了。
由于Center大小不是自己决定的,当里面摆放组件过多显示不全时,可以指定autoscroll="true"产生滚动条。
1.1.2. 基本布局
Borderlayout适合于实现大的页面结构布局,在页面局部最常见的需求就是如何将各种组件有序的摆放:有时需要水平摆放有时需要垂直摆放,
还要考虑居中居左居右问题、摆不下的问题(摆放不下时要有滚动条);
ZK提供了更细粒度的布局组件——hbox/vbox/hlayout/vlayout用于实现这些常见需求;
hlayout和hbox(h代表horizon水平)用于水平布局,vlayout和vbox(v代表vertical垂直)用于垂直布局,它们是最常用的的容器组件,里面可以
放任意多的组件;
hbox vbox与hlayout vlayout的区别:
-
Hbox and Vbox provide more functionalities such as splitter, align and pack.
-
However, their performance is slower,
-
so it is suggested to use Hlayout and Vlayout if you'd like to use them a lot in a UI, unless you need the features that only Hbox and Vbox support.
1.2. 各种容器组件
1.2.1. groupbox
企业应用往往需要在一个页面中显示大量信息或组件,如果随意摆放或只是简单的罗列,会让用户感觉很混乱难以使用,用户体验不好。
groupbox顾名思义就是用来分组布局的组件,它就像收纳盒一样可以把页面组件分门别类的摆放,标题栏可以清晰的标识分类名称,而且可收缩。
<groupbox width=
"50%"
hflex=
"true"
closable=
"true"
mold=
"3d"
>
<caption label=
"基本信息"
/>
<textbox id=
"userIdLongbox"
value=
"@bind(fx.id)"
visible=
"false"
/>
………………
|
1.2.2. tabbox页签
像ZK这样的RIA框架做出来的系统基本上SinglePage的(整个系统只有一个页面,其它都是组件和AJAX),
同时企业应用不同于网站,用户需要打开很多视图查看各种数据和表单,因此普遍采用“多页签布局”来保证系统的方便易用。
ZK提供了tabbox组件方便的实现多种形式的页签:
<
tabbox
id
=
"tb"
height
=
"300px"
>
<
tabs
id
=
"tabs"
>
<
tab
id
=
"A"
label
=
"Tab A"
/>
<
tab
id
=
"B"
label
=
"Tab B"
/>
</
tabs
>
<
tabpanels
>
<
tabpanel
>This is panel A</
tabpanel
>
<
tabpanel
>This is panel B</
tabpanel
>
</
tabpanels
>
</
tabbox
>
|
<
tabbox
id
=
"tb"
height
=
"300px"
orient
=
"vertical"
>
<
tabs
id
=
"tabs"
>
<
tab
id
=
"A"
label
=
"Tab A"
/>
<
tab
id
=
"B"
label
=
"Tab B"
/>
</
tabs
>
<
tabpanels
>
<
tabpanel
>This is panel A</
tabpanel
>
<
tabpanel
>This is panel B</
tabpanel
>
</
tabpanels
>
</
tabbox
>
|
最新zk-7支持下方的水平排列页签。
另外只需设置属性 mold
=
"accordion"
就可以把页签变成可纵向滑动伸缩的“抽屉”式页签:
<
tabbox
id
=
"tb"
height
=
"300px"
mold
=
"accordion"
>
<
tabs
id
=
"tabs"
>
<
tab
id
=
"A"
label
=
"Tab A"
/>
<
tab
id
=
"B"
label
=
"Tab B"
/>
</
tabs
>
<
tabpanels
>
<
tabpanel
>This is panel A</
tabpanel
>
<
tabpanel
>This is panel B</
tabpanel
>
</
tabpanels
>
</
tabbox
>
|
1.2.3. window与panel
window和panel是GUI最常见的容器形式,可以在里面放置任意多的组件;
它们不同于其它容器之处在于可以关闭、最小化、最大化、模态显示(始终显示在最前面,除非最小化或关闭)、可拖动;
但是window和panel也有两个很小的区别:
- window是一个独立的idspace,而panel不是;因此panel内部的组件与panel外部的是一样的;
- panel智能在自己的parent组件范围内移动,而window可以在整个页面移动;
在ZK中创建一个窗口并以模态显示,代码如下:
//create a window programmatically and use it as a modal dialog.
Window window = (Window)Executions.createComponents(
"/widgets/window/modal_dialog/employee_dialog.zul"
,
null
,
null
);
window.doModal();
|
一个简单的窗口页面:
<
window
id
=
"modalDialog"
title
=
"Coffee Order"
border
=
"normal"
width
=
"460px"
apply
=
"demo.window.modal_dialog.EmployeeDialogController"
position
=
"center,center"
closable
=
"true"
action
=
"show: slideDown;hide: slideUp"
>
<
vlayout
>
………………
</
vlayout
>
</
window
>
|
详见 http://www.zkoss.org/zkdemo/window/modal_dialog
1.3. Messagebox对话框
- Warning
: Messagebox.show("Warning is pressed", "Warning", Messagebox.OK, Messagebox.EXCLAMATION);
- Question:
Messagebox.show("Question is pressed. Are you sure?", "Question", Messagebox.OK | Messagebox.CANCEL, Messagebox.QUESTION);
- Information:
Messagebox.show("Information is pressed", "Information", Messagebox.OK, Messagebox.INFORMATION);
- Error:
Messagebox.show("Error is pressed", "Error", Messagebox.OK, Messagebox.ERROR);
-
Confirm Dialog
:详见http://www.zkoss.org/zkdemo/window/message_box
2. MVC
ZK虽然支持在ZUL脚本语言编程,但显然更正规也更有效的开发模式是把交互逻辑放到后台Java代码中实现,MVC模式正是这样的风格。
ZK MVC很简单:页面apply指定Controller、Controller中注入页面组件、Controller方法监听页面事件并修改操纵页面组件;
详见 http://www.zkoss.org/zkdemo/getting_started/mvc
2.1. MVC基本原理示例
1
2
3
4
5
6
7
8
9
10
11
|
public
class
SearchController
extends
SelectorComposer<Component> {
@Wire
private
Textbox keywordBox;
//注入页面组件
@Wire
private
Listbox carListbox;
//注入页面组件
@Listen
(
"onClick = #searchButton"
)
//监听页面事件
public
void
search(Event event){
Button searchButton = (Button) event.getTarget();
String keyword = keywordBox.getValue();
List<Car> result = carService.search(keyword);
carListbox.setModel(
new
ListModelList<Car>(result));
//操纵页面组件(显示数据或改变状态)
}
|
其中2、3行代码将页面中id为keywordBox和carListbox的组件注入Controller作为实例变量,后面方法中对它们进行的修改将被ZK框架自动同步到前端页面上去;
第5行代码为方法注册了页面事件监听器——页面中id为searchButton的组件的onClick事件发生时调用此方法,
组件以及事件监听的表达式详见:http://books.zkoss.org/wiki/Small_Talks/2011/January/Envisage_ZK_6:_An_Annotation_Based_Composer_For_MVC
2.2. MVC forward事件处理
当页面组件很多时,如果只用onClick等少数内建事件进行监听会显得混乱。
forward可以用来将某个组件上发生的内建事件转发到外层并取别名,示例如下:
<
window
id
=
"mywin"
>
<
button
label
=
"Save"
forward
=
"onSave"
/>
<
button
label
=
"Cancel"
forward
=
"onCancel"
/>
<
listitem
self
=
"@{each=p1}"
forward
=
"onDoubleClick=mywin.onDetail(each.prop1)"
>
</
window
>
|
1
2
3
4
5
|
@Listen
(
"onDetail= #mywin"
)
//监听mywin的onDetail事件
public
void
onDetail(ForwardEvent e) {
MouseEvent me = (MouseEvent) e.getOrigin();
//获取源事件
System.out.println(me.getData());
//获取参数
}
|
3. MVVM
3.1. MVVM Binding
3.1.1. Binding绑定概述
Binding(绑定)是Web框架最重要特性之一,Binding没有一个统一的定义,通常的Binding是指:
在 页面元素 与 后台(Controller)组件字段 之间建立起链接,使得后台数据(及其变化)可以显示(同步更新)到页面,
同时用户在页面的输入(修改)也可以传递(更新)到后台;
从这个定义可以看出,Binding是很常见的需求,如果不采用Binding技术,那么手工完成上述工作(如request.getParameter或setAttribute)
会十分的繁琐无聊,产生大量重复代码;
3.1.2. 复杂类型Binding
幸好ZK的MVVM数据绑定非常强大——支持任意复杂类型,例如枚举类型:
<
combobox
selectedItem
=
"@bind(fx.userTypeForCc)"
readonly
=
"true"
model
=
"@load(vm.userTypeForCcList)"
itemRenderer
=
"com.xxx.ctrl.renderer.ComboitemRenderer4UserTypeCc"
/>
class ComboitemRenderer4UserTypeCc implements ComboitemRenderer<
USER_TYPE_FOR_CC
> {
@Override
public void render(Comboitem item, USER_TYPE_FOR_CC data, int index) throws Exception {
item.setLabel(data.getText());
}
}
|
3.1.3. Binding标签
而且用起来很简单——只要三个标签(@load、@save、@bind)就可以实现各种类型的数据绑定:
- @load 用来从后台读数据显示在页面;
- @save 用来将页面输入的信息传递给后台绑定的组件字段;
- @bind 是@load加@save;
3.1.4. Binding表达式
标签中还可以运用复杂表达式,例如:
- 日期格式转换:<label value="@load(vm.modelA.crtDttm) @converter('formatedDate', format='yyyy-MM-dd HH:mm')" />
-
比较运算:
<listcell label="@load(item.quantity)" style="@load(item.quantity lt 3?'color:red':'')"/> -
绑定集合
:
<listbox selectedItems="@bind(vm.selected)" model="@load(vm.model)"> - 根据条件动态选择Template循环:
<
grid
model
=
"@bind(vm.orders) @template(vm.type='foo'?'template1':'template2')"
>
<
template
name
=
"template1"
>
<!-- child components -->
</
template
>
<
template
name
=
"template2"
>
<!-- child components -->
</
template
>
</
grid
>
<
grid
model
=
"@bind(vm.orders) @template(each.type='A'?'templateA':'templateB')"
>
<
template
name
=
"templateA"
>
<!-- child components -->
</
template
>
<
template
name
=
"templateB"
>
<!-- child components -->
</
template
>
</
grid
>
|
3.1.5. 表单整体Binding
对于表单提交场景,我们通常不希望表单中的各个字段单独进行Binding(那会导致每输入一个字段都会产生一次后台交互,
而且无法进行整体校验),
更好的做法是把表单所有元素要作为一个整体,在最后提交时才绑定到后台组件(的Model字段上),这样也使得架构更清晰更OO;
ZUL示例如下:
<
groupbox
width
=
"50%"
hflex
=
"true"
closable
=
"true"
mold
=
"3d"
form
=
"@id('fx') @load(vm.user) @save(vm.user, before='submit') @validator(vm.validator)"
>
<
textbox
value
=
"@bind(fx.userName)"
readonly
=
"${not empty arg.userId }"
/>
<
textbox
type
=
"password"
value
=
"@bind(fx.password)"
/>
<
textbox
type
=
"password"
value
=
"@bind(fx.confirmPassword)"
/>
………………
………………
<
button
id
=
"btn_submit"
label
=
"提交"
onClick
=
"@command('submit')"
/>
………………
|
更多参考 http://books.zkoss.org/wiki/ZK%20Developer%27s%20Reference/MVVM/Data%20Binding/Property%20Binding
3.2. MVVM前后台通信
binding只是在在前后台之间建立起了一个链接,但是还需要一个命令机制来通知框架什么时候以及如何在前后台同步状态;
3.2.1. 前台触发后台动作
页面使用@command标签调用后台组件,示例:
<
menuitem
label
=
"创建Xxx"
onClick
=
"@command('openXxxForm',id=each.id)"
/>
|
后台组件示例:
@Command
public
void
openXxxForm(
@BindingParam
(
"id"
) String roleId) {
|
3.2.2. 后台通知前台刷新
只需在后台组件方法上声明@NotifyChange({ "property1" }),页面中的@load(vm.property1)就会刷新获取最新的值;
3.2.3. MVVM跨页面调用
页面中的@command只能触发当前页面对应的后台组件的方法调用,要想通知其它页面的后台组件调用需要使用@GlobalCommand("refreshDataList");
调用也不是发生在页面,而是在后台显式调用:BindUtils.postGlobalCommand(null, null, "refreshDataList", null);
3.3. MVVM Validation
3.3.1. Validation概述
Web框架最重要的职责之一就是Validation校验——对客户端提交的数据进行合法性检查(长度、类型、取值范围等),
如果校验失败,则返回错误信息并在前端界面中友好清晰的显示错误信息。
3.3.2. MVVM Validation典型步骤
前面binding章节的表单整体绑定中已经包含了validation:
<
window
id
=
"winEditUser"
apply
=
"org.zkoss.bind.BindComposer"
viewModel
=
"@id('vm') @init('com.xxx.ctrl.UserFormDialogCtrl')"
validationMessages
=
"@id('vmsgs')"
<groupbox
width
=
"50%"
hflex
=
"true"
closable
=
"true"
mold
=
"3d"
form
=
"@id('fx') @load(vm.user) @save(vm.user, before='submit') @validator(vm.validator)"
>
<
textbox
type
=
"password"
value
=
"@bind(fx.password)"
/>
<
label
value
=
"@load(vmsgs['password'])"
sclass
=
"red"
/>
<
textbox
value
=
"@bind(fx.contactInfo.email)"
/>
<
label
value
=
"@load(vmsgs['contactInfo.email'])"
sclass
=
"red"
/>
………………
|
- validationMessages="@id('vmsgs')"为校验失败时的错误信息集合指定别名vmsgs
- 其中@validator(vm.validator)指定了表单提交后用来校验的Validator校验器;
- <label value="@load(vmsgs['contactInfo.email'])" sclass="red" /> 用来在校验失败时显示错误信息。
public
org.zkoss.bind.Validator getValidator() {
return
validator;
}
|
3.3.3. MVVM Validation集成JSR303
JSR303是专门针对JavaBean Validation的规范,hibernate-validator是它的一个实现:
<dependency org="org.hibernate" name="hibernate-validator" rev="4.3.0.Final" conf="compile;runtime" />
借助这一框架,可以在JavaBean类中添加对应的Annotation声明校验规则,非常简便而强大;
详见 http://books.zkoss.org/wiki/ZK_Developer%27s_Reference/MVVM/Data_Binding/Validator
4. 列表与分页
4.1. 列表内存分页
ZK列表listbox组件只需配置mold属性为paging即可实现分页,但这样的分页属于内存分页——数据一次性加载到服务端然后每次翻页把当前页数据显示在前台;
<
listbox
id
=
"dataListbox"
mold
=
"paging"
pageSize
=
"20"
multiple
=
"true"
checkmark
=
"false"
emptyMessage
=
"搜索结果为空"
width
=
"100%"
vflex
=
"true"
>
<
listhead
menupopup
=
"auto"
width
=
"100%"
sizable
=
"false"
>
………………
</
listhead
>
<
template
name
=
"model"
>
<
listitem
style
=
"cursor:hand;cursor:pointer;"
>
<
label
value
=
"${forEachStatus.index+1}"
/>
<
label
value
=
"${each.userName}"
/>
|
然后Controller中只要设置dataListbox的model即可显示列表数据:
List carsModel =
new
ListModelList<Car>(carService.findAll());
dataListbox.setModel(carsModel);
|
Demo见 http://www.zkoss.org/zkdemo/getting_started/listbox
4.2. 列表数据库分页
上面的内存分页无法用于真正的生产系统,因为一次加载出所有数据会耗尽服务器内存;
解决方法是数据库分页,每次请求只查询出一页数据然后显示到页面;但是这就不能简单的通过一个配置实现了。
参考ZK文档(http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVVM/Advanced/Displaying_Huge_Amount_of_Data )可自行设计分页组件,
用于封装分页逻辑:构造查询条件、获取总记录数、查询当前页数据、处理返回结果。
代码详见:
- 分页框架父类 https://svn.code.sf.net/p/ktf/code/KTF-UAAS/src/com/kjlink/uaas/base/BasePagingModel.java
- 分页示例 https://svn.code.sf.net/p/ktf/code/KTF-UAAS/src/com/kjlink/uaas/ctrl/UserPagingModel.java 和 https://svn.code.sf.net/p/ktf/code/KTF-UAAS/src/com/kjlink/uaas/ctrl/UserQueryCtrl.java
这里有个小问题——有时查询会缓存上次查询的总记录数和页号,可以显式调用防止缓存:
- dataListbox.getPaginal().setTotalSize(model.getSize());
- dataListbox.getPaginal().setActivePage(0);
5. Tree
<
hlayout
height
=
"90%"
vflex
=
"true"
>
<!-- 设置vflex="true"否则没有垂直滚动条,导致显示不全;设置hflex="true"否则会出现讨厌的水平滚动条 -->
<
tree
id
=
"orgTree"
vflex
=
"true"
hflex
=
"true"
height
=
"100%"
width
=
"100%"
model
=
"@bind(vm.organTreeModel)"
multiple
=
"false"
checkmark
=
"false"
zclass
=
"z-tree"
>
<
treecols
>
<
treecol
hflex
=
"7"
label
=
"名称"
/>
<
treecol
hflex
=
"3"
label
=
"描述"
/>
<
treecol
hflex
=
"1"
label
=
"用户数"
align
=
"center"
/>
</
treecols
>
<
template
name
=
"model"
>
<
treeitem
open
=
"@load(each.data.open)"
selected
=
"@bind(each.data.selected)"
>
<
treerow
onClick
=
"@command('selectOrganNode',organId=each.data.id)"
>
<
treecell
label
=
"${each.data.organName}"
/>
<
treecell
label
=
"${each.data.description}"
/>
<
treecell
label
=
"${each.data.userAmount}"
/>
</
treerow
>
</
treeitem
>
</
template
>
</
tree
>
</
hlayout
>
|
- 和listbox类似的,只需为tree提供model数据,然后内部通过template循环即可打印出一颗动态的树;
- 设置treeitem的属性open和selected属性即可控制树节点是否展开以及是否选中;
6. 右键菜单
首先在页面中隐藏一个menupopup如下:
<
menupopup
id
=
"treeMenupopup"
>
<
menuitem
label
=
"展开"
onClick
=
"@command('expandDir')"
/>
<
menuitem
label
=
"创建子目录"
onClick
=
"@command('newDir')"
/>
<
menuitem
label
=
"修改名称"
onClick
=
"@command('editDir')"
/>
<
menuitem
label
=
"删除"
onClick
=
"@command('deleteDir')"
/>
</
menupopup
>
|
然后为需要右键弹出菜单的组件注册事件:
<
treerow
onRightClick
=
"@command('openTreeMenu', paramEvent=event, reportId=each.data.id, image=each.data.image)"
>
|
处理右键事件:
1
2
3
4
5
6
7
8
|
@Command
public
void
openTreeMenu(
@BindingParam
(
"paramEvent"
) Event paramEvent,
@BindingParam
(
"reportId"
) String reportId,
@BindingParam
(
"image"
) String image) {
if
(StringUtils.isEmpty(image)) {
treeMenupopup.open(paramEvent.getTarget(),
"after_end"
);
//在鼠标光标所在的组件后面弹出菜单
reportIdForRightClick = reportId;
}
}
|
然后就是处理菜单的点击事件,没什么特别的了。
7. Spring集成
ZK与Spring集成非常简单,只需在MVC的Controller或MVVM的ViewModel类上面声明@VariableResolver(DelegatingVariableResolver.class)即可,
然后就可以通过Annotation声明实例变量注入Spring Bean,代码如下:
@VariableResolver
(DelegatingVariableResolver.
class
)
public
class
AbcCtrl
extends
SelectorComposer<Window> {
@WireVariable
private
AbcService abcService;
|
8. SpringSecurity集成
8.1. ZK集成SpringSecurity原理
SpringSecurity是最主流的Web安全框架,框架中封装了一个Web应用通用的典型认证与授权流程,以及安全上下文、session管理、cookie管理等服务;
同时框架为那些不通用的部分留下了扩展点和配置点,例如用户信息获取、权限数据获取、登录页面、登录后跳转、出错页面等;
ZK应用也是Web应用,因此可以直接置于SpringSecurity的保护之下。
但ZK应用又有特殊之处:大量采用AJAX交互并且请求URL不规则,因此为了对ZK应用进行细粒度的权限控制需要借助zkspring-security这个库的帮助;
8.2. ZK集成SpringSecurity配置步骤
依赖的第三方lib:zkspring-security
<
dependency
org
=
"org.zkoss.zk"
name
=
"zkspring-security"
rev
=
"3.1.1"
conf
=
"compile;runtime"
transitive
=
"false"
/>
<
dependency
org
=
"org.springframework.security"
name
=
"spring-security-core"
rev
=
"3.1.4.RELEASE"
conf
=
"compile;runtime"
/>
<
dependency
org
=
"org.springframework.security"
name
=
"spring-security-acl"
rev
=
"3.1.4.RELEASE"
conf
=
"compile;runtime"
/>
<
dependency
org
=
"org.springframework.security"
name
=
"spring-security-taglibs"
rev
=
"3.1.4.RELEASE"
conf
=
"compile;runtime"
/>
<
dependency
org
=
"org.springframework.security"
name
=
"spring-security-config"
rev
=
"3.1.4.RELEASE"
conf
=
"compile;runtime"
/>
<
dependency
org
=
"org.springframework.security"
name
=
"spring-security-web"
rev
=
"3.1.4.RELEASE"
conf
=
"compile;runtime"
/>
|
web.xml配置:
<
listener
>
<
listener-class
>org.springframework.security.web.session.HttpSessionEventPublisher</
listener-class
>
</
listener
>
……………………
<
filter
>
<
filter-name
>springSecurityFilterChain</
filter-name
>
<
filter-class
>org.springframework.web.filter.DelegatingFilterProxy</
filter-class
>
</
filter
>
<
filter-mapping
>
<
filter-name
>springSecurityFilterChain</
filter-name
>
<
url-pattern
>/*</
url-pattern
>
</
filter-mapping
>
|
SpringSecurity配置:
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<
beans:beans
xmlns
=
"http://www.springframework.org/schema/security"
xmlns:beans
=
"http://www.springframework.org/schema/beans"
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns:zksp
=
"http://www.zkoss.org/2008/zkspring/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/security
http://www.zkoss.org/2008/zkspring/security
>
<
http
auto-config
=
'true'
access-denied-page
=
"/error.html"
>
<
intercept-url
pattern
=
"/images/**"
access
=
"IS_AUTHENTICATED_ANONYMOUSLY"
/>
<
intercept-url
pattern
=
"/login.html*"
access
=
"IS_AUTHENTICATED_ANONYMOUSLY"
/>
<
intercept-url
pattern
=
"/pages/admin/**"
access
=
"ROLE_ADMIN"
/>
<
intercept-url
pattern
=
"/pages/**"
access
=
"IS_AUTHENTICATED_ANONYMOUSLY"
/>
<
intercept-url
pattern
=
"/**"
access
=
"IS_AUTHENTICATED_ANONYMOUSLY"
/>
<
form-login
login-page
=
"/login.html"
authentication-failure-url
=
"/login.html?login_error=1"
default-target-url
=
"/main.html"
always-use-default-target
=
"true"
/>
<!-- Following is list of ZK Spring Security custom filters. They needs to be exactly in the same order as shown below
in order to work. -->
<
custom-filter
ref
=
"zkDesktopReuseFilter"
position
=
"FIRST"
/>
<
custom-filter
ref
=
"zkDisableSessionInvalidateFilter"
before
=
"FORM_LOGIN_FILTER"
/>
<
custom-filter
ref
=
"zkEnableSessionInvalidateFilter"
before
=
"FILTER_SECURITY_INTERCEPTOR"
/>
<
custom-filter
ref
=
"zkLoginOKFilter"
after
=
"FILTER_SECURITY_INTERCEPTOR"
/>
<
custom-filter
ref
=
"zkError403Filter"
after
=
"LOGOUT_FILTER"
/>
</
http
>
<
authentication-manager
>
<
authentication-provider
>
<
user-service
properties
=
"classpath:/properties/security-users.properties"
/>
</
authentication-provider
>
</
authentication-manager
>
<
zksp:zk-event
login-template-close-delay
=
"1"
path-type
=
"ant"
>
<
zksp:intercept-event
event
=
"onClick"
path
=
"//**/cmdBtn_*"
access
=
"ROLE_ADMIN"
/>
<
zksp:intercept-event
event
=
"onClick"
path
=
"//**/menu_*"
access
=
"ROLE_ADMIN"
/>
<
zksp:intercept-event
event
=
"onClick"
path
=
"//**/treemenu_*"
access
=
"ROLE_ADMIN"
/>
<
zksp:intercept-event
event
=
"onClick"
path
=
"//**/btn_*"
access
=
"ROLE_USER"
/>
<
zksp:intercept-event
path
=
"/**"
access
=
"IS_AUTHENTICATED_ANONYMOUSLY"
/>
<
zksp:form-login
login-page
=
"/login.html"
/>
</
zksp:zk-event
>
</
beans:beans
>
|
详见 http://books.zkoss.org/wiki/Small_Talks/2010/April/Making_Spring_Security_Work_with_ZK
9. ZK全局配置
9.1. 按钮防止连击
在zk.xml配置:
<
language-config
>
<
addon-uri
>/WEB-INF/lang-addon.xml </
addon-uri
>
</
language-config
>
|
在lang-addon.xml添加配置:
<
component
>
<
component-name
>button</
component-name
>
<
extends
>button</
extends
>
<
property
>
<
property-name
>autodisable</
property-name
>
<
property-value
>self</
property-value
>
</
property
>
</
component
>
|
9.2. Theme换肤
在lang-addon.xml添加配置:
<
library-property
>
<
name
>org.zkoss.theme.preferred</
name
>
<
value
>sapphire</
value
>
</
library-property
>
|
10. 国际化i18n
<listbox id=
"localSelector"
mold=
"select"
rows=
"1"
width=
"80px"
>
<listitem label=
"语言/Local"
value=
""
/>
<listitem label=
"English"
value=
"en"
/>
<listitem label=
"简体中文"
value=
"zh_CN"
/>
</listbox>
@Listen
(
"onSelect = #localSelector"
)
public
void
onSelectLocal(Event event) {
Object localName = ((Listbox) event.getTarget()).getSelectedItem().getValue();
logger.debug(
"选择语言区域【"
+ localName +
"】"
);
CookieUtils.setLocal(Executions.getCurrent(), (String) localName);
Locale locale = Locales.getLocale((String) localName);
Executions.getCurrent().getSession().setAttribute(Attributes.PREFERRED_LOCALE, locale);
Executions.sendRedirect(
null
);
}
|
<
listener
>
<
listener-class
>com.xxx.base.LocalInterceptor</
listener-class
>
</
listener
>
|
public
class
LocalInterceptor
implements
RequestInterceptor {
@Override
public
void
request(org.zkoss.zk.ui.Session sess, Object request, Object response) {
String localName = CookieUtils.getLocal((HttpServletRequest) request);
Locale locale = Locales.getLocale(localName);
((HttpServletRequest) request).getSession().setAttribute(Attributes.PREFERRED_LOCALE, locale);
}
}
public
class
CookieUtils {
/**
* 添加名为zktheme的cookie就可以改变当前用户的theme
*/
static
String THEME_COOKIE_KEY =
"zktheme"
;
static
String LOCAL_COOKIE_KEY =
"zkLocal"
;
public
static
String getLocal(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if
(cookies ==
null
)
return
""
;
for
(
int
i =
0
; i < cookies.length; i++) {
Cookie c = cookies[i];
if
(LOCAL_COOKIE_KEY.equals(c.getName())) {
String theme = c.getValue();
if
(theme !=
null
)
return
theme;
}
}
return
""
;
}
public
static
void
setLocal(Execution exe, String localName) {
Cookie cookie =
new
Cookie(LOCAL_COOKIE_KEY, localName);
cookie.setMaxAge(
60
*
60
*
24
*
30
);
// store 30 days
String cp = exe.getContextPath();
// if path is empty, cookie path will be request path, which causes problems
if
(cp.length() ==
0
) {
cp =
"/"
;
}
cookie.setPath(cp);
((HttpServletResponse) exe.getNativeResponse()).addCookie(cookie);
}
}
|
welcome=Welcome
theme.sapphire=sapphire
theme.silvertail=silvertail
|
welcome=欢迎
theme.sapphire=蓝色
theme.silvertail=银灰
|
<
listitem
label
=
"${labels.theme.sapphire}"
value
=
"sapphire"
/>
|
11. 文件上传
<
menuitem
label
=
"添加附件"
image
=
"/images/attachment_16.png"
upload
=
"true,native,maxsize=10240"
onUpload
=
"@command('boardUploadFile')"
/>
<
button
label
=
"添加附件"
upload
=
"true,native,maxsize=10240"
onUpload
=
"@command('formUploadFile')"
/>
|
然后在ViewModel中处理上传的文件,如下:
1
2
3
4
5
6
7
8
9
10
11
|
@Command
@NotifyChange
({
"boardAttachmentList"
,
"boardAuditInfoList"
})
public
void
boardUploadFile(
@ContextParam
(ContextType.TRIGGER_EVENT) UploadEvent event)
throws
IOException {
Media media = event.getMedia();
logger.debug(
"文件名为【"
+ media.getName() +
"】"
);
logger.debug(
"文件大小为【"
+ media.getStreamData().read() +
"】"
);
logger.debug(
"文件类型为【"
+ media.getContentType() +
"】"
);
QualityPlanAttachment qualityPlanAttachment =
new
QualityPlanAttachment();
qualityPlanAttachment.setFileName(media.getName());
qualityPlanAttachment.setFileSize((
long
) media.getByteData().length);
qualityPlanAttachment.setContent(media.getByteData());
|
12. 文件下载
很简单,只需调用ZK相关API Filedownload.save;另外要注意不同浏览器对中文文件名可能产生乱码问题;
@Command
public
void
boardDownloadFile(
@BindingParam
(
"fileId"
) String fileId)
throws
UnsupportedEncodingException {
………………
String fileName = attachment.getFileName();
byte
[] content=attachment.getByteArray();
Filedownload.save(content,
null
, ZkUtils.encodingFileName(fileName));
}
//解决文件名乱码问题
public
static
String encodingFileName(String fileName)
throws
UnsupportedEncodingException {
HttpServletRequest httpRequest = (HttpServletRequest) Executions.getCurrent().getNativeRequest();
String browserName = Servlets.getBrowser(httpRequest);
if
(StringUtils.equalsIgnoreCase(
"gecko"
, browserName)) {
//firefox
fileName =
new
String(fileName.getBytes(
"UTF-8"
),
"ISO8859-1"
);
}
else
{
//ie浏览器
fileName = URLEncoder.encode(fileName,
"UTF-8"
);
}
return
fileName;
}
|
13. CKEditor
ZK的子项目ckez实现了对CKEditor(一款流行的在线文本编辑器)的封装,可以方便的集成到ZK应用中实现在线编辑复杂格式文档;
<
dependency
org
=
"org.zkoss.zkforge"
name
=
"ckez"
rev
=
"3.6.4.0"
conf
=
"runtime"
/>
|
然后zul中只需像textbox一样去用就可以了:
<
ckeditor
toolbar
=
"Basic"
value
=
"@bind(fx.content)"
hflex
=
"true"
width
=
"90%"
height
=
"95px"
/>
|
14. Chart图表
ZK自带的chart图表参考在线demo :http://www.zkoss.org/zkdemo/chart/pie_chart
另外ZK的子项目zhighcharts对higncharts-js(用于绘制各种常见图表的js库)进行了封装,可以方便的集成到ZK应用,详见:
- http://books.zkoss.org/wiki/Small_Talks/2012/November/ZHighCharts:_Integrating_ZK_with_Highcharts
- https://github.com/NGI-Maghreb/ZK/downloads
15. 自定义组件
15.1. taglib标签式自定义组件
<?
component
name
=
"progressBar"
extends
=
"hlayout"
class
=
"com.xxx.component.ProgressBarHlayout"
?>
<
progressBar
progress
=
"@load(vm.plan1.progressPercentageValue)"
widthValue
=
"120"
height
=
"9px"
/>
|
public
class
ProgressBarHlayout
extends
Hlayout
implements
IdSpace {
private
static
final
int
VALUE_LABEL_WIDTH =
0
;
//30
@Wire
Div progressDiv;
@Wire
Div grayDiv;
String defaultStyle =
"position:absolute;left:0px;z-index:1;background:"
;
public
ProgressBarHlayout() {
Executions.createComponents(
"/pages/common/progressBar.zul"
,
this
,
null
);
Selectors.wireVariables(
this
,
this
, Selectors.newVariableResolvers(getClass(), Hlayout.
class
));
Selectors.wireComponents(
this
,
this
,
false
);
this
.setSpacing(
"0"
);
}
………………
public
void
setProgress(
int
progress) {
progressDiv.setWidth(progress * (widthValue - ProgressBarHlayout.VALUE_LABEL_WIDTH) /
100
+
"px"
);
this
.setTooltiptext(progress +
"%"
);
// progeressValueLabel.setValue(progress + "%");
// progeressValueLabel.setWidth(ProgressBarHlayout.VALUE_LABEL_WIDTH + "px");
// progeressValueLabel.setStyle("font-size:11px");
if
(progress <=
30
) {
progressDiv.setStyle(defaultStyle +
"#CD3D38;"
);
}
else
if
(progress >=
80
) {
progressDiv.setStyle(defaultStyle +
"#69CD4B;"
);
}
else
{
progressDiv.setStyle(defaultStyle +
"#CF9E25;"
);
}
}
|
<
zk
>
<!-- <label id="progeressValueLabel" vflex="true" /> -->
<
div
id
=
"progressDiv"
style
=
"position:absolute;left:0px;z-index:1;background:;"
/>
<
div
id
=
"grayDiv"
style
=
"position:relative;top:0px;left:0px;background:#D5CCBE;"
/>
</
zk
>
|
15.2. 简单taglib标签式自定义组件
声明:
<?
component
name
=
"myImage"
class
=
"XX.MyImage"
?>
定义组件:
public class MyImage extends Image implements AfterCompose {
public void setMycontent(byte[] mycontent) {
if (null!=mycontent) {
try {
this.setContent(new AImage("t", mycontent));
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用组件:
<
myImage
mycontent
=
"@load(item.photo)"
width
=
"300px"
height
=
"300px"
/>
|
15.3. include宏式自定义组件
跟前面taglib风格正好相反,采用include方式,然后在被include页面里面就是正常的MVVM模式。
优点是可以使用MVVM,适合于复杂页面;示例如下:
<
include
chartInfo
=
"@load(node)"
hflex
=
"true"
vflex
=
"true"
width
=
"@load(node.boxWidthPx)"
src
=
"@load('/pages/common/componentAbc.zul')"
/>
|
然后被include的页面componentAbc就是一个普通的MVVM页面,
ViewModel实现如下(注意其中的init方法用来从外层页面传入参数):
import
org.zkoss.bind.annotation.Init;
………………
@VariableResolver
(DelegatingVariableResolver.
class
)
public
class
ComponentAbc {
@Init
public
void
init(
@ExecutionArgParam
(
"chartInfo"
) IndicatorChartInfo chartInfo,
@ExecutionArgParam
(
"maxMold"
) Boolean maxMold) {
this
.chartInfo = chartInfo;
this
.maxMold = maxMold;
}
|
然后就没什么特别的了。
16. ZATS集成测试
16.1. ZATS概述
ZATS是用来对ZK应用进行自动化功能测试的一套测试框架。
Web应用的自动化功能测试非常重要(可以有效的保证发布质量,同时节省大量的手工回归测试成本),因此出现了很多相关技术框架和工具,
最常用的的包括QTP、Selenium;
但这些工具有两个致命的弱点:
- 运行缓慢:传统的测试工具都是跨进程通讯的,运行测试时需要起至少三个进程——AppServer、浏览器、测试框架本身的Server,
- 导致运行缓慢、准备工作繁琐、容易出错;
- 测试脚本不稳定容易出错:另外由于传统测试框架是针对系统最终界面(HTML)进行测试,测试用例与页面HTML高度耦合,
- 为复杂页面编写的测试脚本也很复杂繁琐,而且页面稍有改动脚本就会出错,开发和维护成本都很高。
这两个弱点严重制约了自动化测试的进行。
相对于Selenium等测试框架,ZATS具有很大优势(当然这只限于ZK Web应用),它解决了传统自动化测试框架的两个最大软肋:
- 轻便快速:ZATS测试与被测页面运行在同一进程内,不需要起server和浏览器,运行起来方便快速,跟普通的JUnit单元测试基本没有区别;
- 测试脚本稳定、可维护性好:ZATS是针对ZUL的测试,由于ZUL比最终生成的HTML要简洁的多(代码量大概只有10%),
- 因此测试开发和维护成本很低,而且稳定;
16.2. ZATS典型测试案例
首先扩展测试框架基类,以便在每个ZATS测试用例运行前做一些统一的准备工作,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
protected
static
ZatsEnvironment env;
@BeforeClass
public
static
void
init() {
env =
new
DefaultZatsEnvironment(
"./zats"
);
//加载zats目录下的web.xml
env.init(
"./web"
);
}
@AfterClass
public
static
void
end() {
Zats.end();
}
@After
public
void
after() {
Zats.cleanup();
}
|
通常我们需要为ZATS准备一个简单的web.xml——例如这里可以去掉SpringSecurity等配置、加载测试专用的spring配置等;
然后就可以扩展这个基类来开发真正的ZATS测试了,基本套路如下:
- 连接被测页面;
- 定位要操作的组件;
- 断言页面的变化或后台数据的变化;
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
import
java.util.List;
import
junit.framework.Assert;
import
org.apache.log4j.Logger;
import
org.junit.Test;
import
org.zkoss.zats.mimic.Client;
import
org.zkoss.zats.mimic.ComponentAgent;
import
org.zkoss.zats.mimic.DesktopAgent;
public
class
HomeTest
extends
BaseZatsTestCase {
Logger logger = LoggerUtil.getLogger();
@Test
public
void
testHome() {
Client client = env.newClient();
DesktopAgent desktop = client.connect(
"/pages/home.zul"
);
//打开首页home.zul
ComponentAgent mainTabsAgent = desktop.query(
"#topWindow"
).query(
"#center"
).query(
"#mainTabbox"
)
.query(
"#mainTabs"
);
logger.debug(mainTabsAgent);
List<ComponentAgent> menuAgentList = desktop.queryAll(
"tree treechildren treeitem treechildren treeitem"
);
logger.debug(menuAgentList.size());
Assert.assertEquals(
9
, menuAgentList.size());
menuAgentList.get(
0
).click();
//点击打开west第一个菜单
logger.debug(
"mainTabsAgent size:"
+ mainTabsAgent.getChildren().size());
Assert.assertEquals(
"mainTabsAgent包含tab个数"
,
2
, mainTabsAgent.getChildren().size());
}
@Test
public
void
testProjectApply() {
Client client = env.newClient();
DesktopAgent desktop = client.connect(
"/pages/projectCodeApply.zul"
);
ComponentAgent listboxAgent = desktop.query(
"#dataListbox"
);
logger.debug(listboxAgent);
Assert.assertNotNull(listboxAgent);
}
}
|
16.3. ZATS与Mockito集成
Mockito是一个想打的Mock测试框架,Mock技术可以用来隔离外部接口、资源等依赖,使得单元测试可以不受外部依赖影响,简化测试工作并且可以方便的模拟一些异常情况;
由于ZATS测试本质上是针对运行在jetty server中的整个应用(中的zuls页面),因此属于一种端到端的功能测试,因此无法像单元测试那样通过Mock技术对接口和底层Service组件进行隔离。
为此需要对ZATS做一点Hacking:
- 反编译EmulatorClient并且为其emulator属性增加一个getter
然后就可以想如下示例获得jetty server中的Spring上下文并获得其中的Service
EmulatorClient client = (EmulatorClient) env.newClient();
WebApplicationContext wac = WebApplicationContextUtils
.getWebApplicationContext(client.getEmulator().getServletContext());
ArrayList<Policy> policyList =
new
ArrayList<Policy>();
Policy policy =
new
Policy();
policy.setInsurantName(
"李大壮"
);
policyList.add(policy);
PolicyCancelService policyCancelService=(PolicyCancelService) wac.getBean(
"policyCancelService"
);
Mockito.when(policyCancelService.getRetreatList(Mockito.anyMap(),Mockito.anyString())).thenReturn(policyList);
|
- 当然,还有一个前提——ZATS启动加载的Spring配置的是Mockito Service,这样才能通过Mockito框架设置其行为,Spring配置如下:
<
bean
id
=
"policyCancelService"
class
=
"org.mockito.Mockito"
factory-method
=
"mock"
>
<
constructor-arg
value
=
"com.cpic.p17.life.service.telGps.PolicyCancelService"
/>
</
bean
>
<
bean
id
=
"organDeptService"
class
=
"org.mockito.Mockito"
factory-method
=
"mock"
>
<
constructor-arg
value
=
"com.cpic.p17.base.service.OrganDeptService"
/>
</
bean
>