前言
本文是对ZK开发过程中必须掌握的关键知识点的总结,针对目前对新版本zk-6.5.2
关于ZK是什么参见前一篇博客 《ZK(The leading enterprise Ajax framework)入门指南》 http://blog.csdn.net/daquan198163/article/details/9304897
ZK具有非常高的开发效率(以至于可以取代HTML用来完成高质量的Fast Prototyping),最主要缘自它采用的页面布局技术——ZUL;
采用XML语言以声明式的方式创建用户界面——XML UI技术并不是ZK的独创,Android UI、JavaFX、Microsoft Silverlight和Mozilla XUL等开发
框架都采用了这种技术,这是目前最先进的构造用户界面技术;
ZUL只不过是为所有ZK组件指定了一个对应的XML Element,然后通过这些Element以声明式的方式定义页面由哪些组件以什么样的方式构成。
相对于Java编程式方式,这种声明式方式的优点十分明显:
直观、简洁的代码意味着容易理解、容易编写、容易修改维护、不容易出错,因此带来开发效率上的巨大优势。
值得注意的是,上述ZUL相对于Java编程的优势也适用于JS,比如EXT、DOJO等JS UI框架。
Borderlayout将屏幕划分成东西南北中五个区域,如下图所示,其灵活性可以实现绝大多数系统的首页整体布局。
首先纵向看,要指定N和S的高度,剩下中间部分就是W C E的高度;然后水平看,N S宽度百分百,中间部分指定W E的宽度后,
剩下的部分就是C了。
由于Center大小不是自己决定的,当里面摆放组件过多显示不全时,可以指定autoscroll="true"产生滚动条。
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.
企业应用往往需要在一个页面中显示大量信息或组件,如果随意摆放或只是简单的罗列,会让用户感觉很混乱难以使用,用户体验不好。
groupbox顾名思义就是用来分组布局的组件,它就像收纳盒一样可以把页面组件分门别类的摆放,标题栏可以清晰的标识分类名称,而且可收缩。
………………
|
像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
>
|
window和panel是GUI最常见的容器形式,可以在里面放置任意多的组件;
它们不同于其它容器之处在于可以关闭、最小化、最大化、模态显示(始终显示在最前面,除非最小化或关闭)、可拖动;
但是window和panel也有两个很小的区别:
在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
: Messagebox.show("Warning is pressed", "Warning", Messagebox.OK, Messagebox.EXCLAMATION);
Messagebox.show("Question is pressed. Are you sure?", "Question", Messagebox.OK | Messagebox.CANCEL, Messagebox.QUESTION);
Messagebox.show("Information is pressed", "Information", Messagebox.OK, Messagebox.INFORMATION);
Messagebox.show("Error is pressed", "Error", Messagebox.OK, Messagebox.ERROR);
Confirm Dialog
:详见http://www.zkoss.org/zkdemo/window/message_boxZK虽然支持在ZUL脚本语言编程,但显然更正规也更有效的开发模式是把交互逻辑放到后台Java代码中实现,MVC模式正是这样的风格。
ZK MVC很简单:页面apply指定Controller、Controller中注入页面组件、Controller方法监听页面事件并修改操纵页面组件;
详见 http://www.zkoss.org/zkdemo/getting_started/mvc
1
2
3
4
5
6
7
8
9
10
11
|
public
class
SearchController
extends
SelectorComposer
@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
carListbox.setModel(
new
ListModelList
//操纵页面组件(显示数据或改变状态)
}
|
其中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
当页面组件很多时,如果只用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());
//获取参数
}
|
Binding(绑定)是Web框架最重要特性之一,Binding没有一个统一的定义,通常的Binding是指:
在 页面元素 与 后台(Controller)组件字段 之间建立起链接,使得后台数据(及其变化)可以显示(同步更新)到页面,
同时用户在页面的输入(修改)也可以传递(更新)到后台;
从这个定义可以看出,Binding是很常见的需求,如果不采用Binding技术,那么手工完成上述工作(如request.getParameter或setAttribute)
会十分的繁琐无聊,产生大量重复代码;
幸好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());
}
}
|
而且用起来很简单——只要三个标签(@load、@save、@bind)就可以实现各种类型的数据绑定:
标签中还可以运用复杂表达式,例如:
比较运算:
绑定集合
:
<
grid
model
=
"@bind(vm.orders) @template(vm.type='foo'?'template1':'template2')"
>
<
template
name
=
"template1"
>
template
>
<
template
name
=
"template2"
>
template
>
grid
>
<
grid
model
=
"@bind(vm.orders) @template(each.type='A'?'templateA':'templateB')"
>
<
template
name
=
"templateA"
>
template
>
<
template
name
=
"templateB"
>
template
>
grid
>
|
对于表单提交场景,我们通常不希望表单中的各个字段单独进行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
binding只是在在前后台之间建立起了一个链接,但是还需要一个命令机制来通知框架什么时候以及如何在前后台同步状态;
页面使用@command标签调用后台组件,示例:
<
menuitem
label
=
"创建Xxx"
onClick
=
"@command('openXxxForm',id=each.id)"
/>
|
后台组件示例:
@Command
public
void
openXxxForm(
@BindingParam
(
"id"
) String roleId) {
|
只需在后台组件方法上声明@NotifyChange({ "property1" }),页面中的@load(vm.property1)就会刷新获取最新的值;
页面中的@command只能触发当前页面对应的后台组件的方法调用,要想通知其它页面的后台组件调用需要使用@GlobalCommand("refreshDataList");
调用也不是发生在页面,而是在后台显式调用:BindUtils.postGlobalCommand(null, null, "refreshDataList", null);
Web框架最重要的职责之一就是Validation校验——对客户端提交的数据进行合法性检查(长度、类型、取值范围等),
如果校验失败,则返回错误信息并在前端界面中友好清晰的显示错误信息。
前面binding章节的表单整体绑定中已经包含了validation:
<
window
id
=
"winEditUser"
apply
=
"org.zkoss.bind.BindComposer"
viewModel
=
"@id('vm') @init('com.xxx.ctrl.UserFormDialogCtrl')"
validationMessages
=
"@id('vmsgs')"
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"
/>
………………
|
public
org.zkoss.bind.Validator getValidator() {
return
validator;
}
|
JSR303是专门针对JavaBean Validation的规范,hibernate-validator是它的一个实现:
借助这一框架,可以在JavaBean类中添加对应的Annotation声明校验规则,非常简便而强大;
详见 http://books.zkoss.org/wiki/ZK_Developer%27s_Reference/MVVM/Data_Binding/Validator
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
dataListbox.setModel(carsModel);
|
Demo见 http://www.zkoss.org/zkdemo/getting_started/listbox
上面的内存分页无法用于真正的生产系统,因为一次加载出所有数据会耗尽服务器内存;
解决方法是数据库分页,每次请求只查询出一页数据然后显示到页面;但是这就不能简单的通过一个配置实现了。
参考ZK文档(http://books.zkoss.org/wiki/ZK_Developer's_Reference/MVVM/Advanced/Displaying_Huge_Amount_of_Data )可自行设计分页组件,
用于封装分页逻辑:构造查询条件、获取总记录数、查询当前页数据、处理返回结果。
代码详见:
<
hlayout
height
=
"90%"
vflex
=
"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
>
|
首先在页面中隐藏一个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;
}
}
|
然后就是处理菜单的点击事件,没什么特别的了。
ZK与Spring集成非常简单,只需在MVC的Controller或MVVM的ViewModel类上面声明@VariableResolver(DelegatingVariableResolver.class)即可,
然后就可以通过Annotation声明实例变量注入Spring Bean,代码如下:
@VariableResolver
(DelegatingVariableResolver.
class
)
public
class
AbcCtrl
extends
SelectorComposer
@WireVariable
private
AbcService abcService;
|
SpringSecurity是最主流的Web安全框架,框架中封装了一个Web应用通用的典型认证与授权流程,以及安全上下文、session管理、cookie管理等服务;
同时框架为那些不通用的部分留下了扩展点和配置点,例如用户信息获取、权限数据获取、登录页面、登录后跳转、出错页面等;
ZK应用也是Web应用,因此可以直接置于SpringSecurity的保护之下。
但ZK应用又有特殊之处:大量采用AJAX交互并且请求URL不规则,因此为了对ZK应用进行细粒度的权限控制需要借助zkspring-security这个库的帮助;
依赖的第三方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/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd
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"
/>
<
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
在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
>
|
在lang-addon.xml添加配置:
<
library-property
>
<
name
>org.zkoss.theme.preferred
name
>
<
value
>sapphire
value
>
library-property
>
|
@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"
/>
|
<
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());
|
很简单,只需调用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;
}
|
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"
/>
|
ZK自带的chart图表参考在线demo :http://www.zkoss.org/zkdemo/chart/pie_chart
另外ZK的子项目zhighcharts对higncharts-js(用于绘制各种常见图表的js库)进行了封装,可以方便的集成到ZK应用,详见:
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
>
<
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
>
|
声明:
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"
/>
|
跟前面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;
}
|
然后就没什么特别的了。
ZATS是用来对ZK应用进行自动化功能测试的一套测试框架。
Web应用的自动化功能测试非常重要(可以有效的保证发布质量,同时节省大量的手工回归测试成本),因此出现了很多相关技术框架和工具,
最常用的的包括QTP、Selenium;
但这些工具有两个致命的弱点:
这两个弱点严重制约了自动化测试的进行。
相对于Selenium等测试框架,ZATS具有很大优势(当然这只限于ZK Web应用),它解决了传统自动化测试框架的两个最大软肋:
首先扩展测试框架基类,以便在每个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
"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);
}
}
|
Mockito是一个想打的Mock测试框架,Mock技术可以用来隔离外部接口、资源等依赖,使得单元测试可以不受外部依赖影响,简化测试工作并且可以方便的模拟一些异常情况;
由于ZATS测试本质上是针对运行在jetty server中的整个应用(中的zuls页面),因此属于一种端到端的功能测试,因此无法像单元测试那样通过Mock技术对接口和底层Service组件进行隔离。
为此需要对ZATS做一点Hacking:
然后就可以想如下示例获得jetty server中的Spring上下文并获得其中的Service
EmulatorClient client = (EmulatorClient) env.newClient();
WebApplicationContext wac = WebApplicationContextUtils
.getWebApplicationContext(client.getEmulator().getServletContext());
ArrayList
new
ArrayList
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);
|
<
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
>