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
>
相关推荐
1.版本:matlab2014/2019a/2021a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
android 源码学习. 资料部分来源于合法的互联网渠道收集和整理,供大家学习参考与交流。本人不对所涉及的版权问题或内容负法律责任。如有侵权,请通知本人删除。感谢CSDN官方提供大家交流的平台
libADLMIDI1-1.5.0-bp153.1.1.x86_64.rpm 是用于在 x86_64 架构的设备上安装的 RPM 包,具体功能如下: 名称:libADLMIDI1 版本:1.5.0 摘要:带有 OPL3 (YMF262) 模拟器的软件 MIDI 合成器库 许可证:GPL-3.0-only 和 LGPL-3.0-only 该库提供了一个基于 ADLMIDI 的软件 MIDI 合成器,它模拟了 OPL3 音源芯片(FM 合成)。它可以通过使用 ADLMIDI 库来实现多平台的 MIDI 播放和 OPL3 模拟。 该 RPM 包适用于 x86_64 架构,用于在相关设备上安装 libADLMIDI1 库文件。库文件包括: /usr/lib64/libADLMIDI.so.1 和 /usr/lib64/libADLMIDI.so.1.5.0:库文件 /usr/share/doc/packages/libADLMIDI1/AUTHORS、/usr/share/doc/packages/libADLMIDI1/README.md 等文档文件:文档文件
基于qt+C++实现u盘插拔检测.+源码,适合毕业设计、课程设计、项目开发。项目源码已经过严格测试,可以放心参考并在此基础上延申使用~ 基于qt+C++实现u盘插拔检测.+源码,适合毕业设计、课程设计、项目开发。项目源码已经过严格测试,可以放心参考并在此基础上延申使用~ 基于qt+C++实现u盘插拔检测.+源码,适合毕业设计、课程设计、项目开发。项目源码已经过严格测试,可以放心参考并在此基础上延申使用~ 基于qt+C++实现u盘插拔检测.+源码,适合毕业设计、课程设计、项目开发。项目源码已经过严格测试,可以放心参考并在此基础上延申使用~
Quectel_Product_Brochure_CN_V7.9.pdf
android 源码学习. 资料部分来源于合法的互联网渠道收集和整理,供大家学习参考与交流。本人不对所涉及的版权问题或内容负法律责任。如有侵权,请通知本人删除。感谢CSDN官方提供大家交流的平台
phpstudy
python入门
android 源码学习. 资料部分来源于合法的互联网渠道收集和整理,供大家学习参考与交流。本人不对所涉及的版权问题或内容负法律责任。如有侵权,请通知本人删除。感谢CSDN官方提供大家交流的平台
移动机器人机械臂的设计开题报告.doc
用法链接:https://menghui666.blog.csdn.net/article/details/137977678?spm=1001.2014.3001.5502 基于QT+C++开发的智能平台访客系统+源码,包含主界面、系统设置、警情查询、调试帮助、用户退出功能。 基于QT+C++开发的智能平台访客系统+源码,包含主界面、系统设置、警情查询、调试帮助、用户退出功能。 基于QT+C++开发的智能平台访客系统+源码,包含主界面、系统设置、警情查询、调试帮助、用户退出功能。
三菱机械臂校点说明.pptx
android 源码学习. 资料部分来源于合法的互联网渠道收集和整理,供大家学习参考与交流。本人不对所涉及的版权问题或内容负法律责任。如有侵权,请通知本人删除。感谢CSDN官方提供大家交流的平台
android 源码学习. 资料部分来源于合法的互联网渠道收集和整理,供大家学习参考与交流。本人不对所涉及的版权问题或内容负法律责任。如有侵权,请通知本人删除。感谢CSDN官方提供大家交流的平台
C语言诞生于美国的贝尔实验室,由丹尼斯·里奇(Dennis MacAlistair Ritchie)以肯尼斯·蓝·汤普森(Kenneth Lane Thompson)设计的B语言为基础发展而来,在它的主体设计完成后,汤普森和里奇用它完全重写了UNIX,且随着UNIX的发展,c语言也得到了不断的完善。为了利于C语言的全面推广,许多专家学者和硬件厂商联合组成了C语言标准委员会,并在之后的1989年,诞生了第一个完备的C标准,简称“C89”,也就是“ANSI C”,截至2020年,最新的C语言标准为2018年6月发布的“C18”。 [5] C语言之所以命名为C,是因为C语言源自Ken Thompson发明的B语言,而B语言则源自BCPL语言。 1967年,剑桥大学的Martin Richards对CPL语言进行了简化,于是产生了BCPL(Basic Combined Programming Language)语言。
python入门 单元测试和测试用例 Python标准库中的模块unittest提供了代码测试工具。 单元测试用于核实函数的某个防霾呢没有问题; 测试用例是一组单元测试,这些单元测试仪器一起核实函数在各种情形下的行为都符合要求。良好的测试用例考虑到了函数可能收到的各种收入,包含所有针对这些情形的测试。 全覆盖式测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式。 对于大型项目,要实现全覆盖可能很难。通常,最初只要对针对代码的重要行为编写测试即可,等项目给广泛使用时再考虑全覆盖。 可通过的测试 创建测试用例的语法需要一段时间才能习惯,但测试用例创建后,再添加针对函数的单元测试就很简单了。要为函数编写测试用例,可先导入模块unittest以及要测试的函数,在创建一个继承unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。 下面test_name_function.py一个只包含一个方法的测试用例,它检查函数get_formatted_name()在给定名和姓时能否正确的工作。
基于matlabbenders分解算法.zip
dsp工程设计讲座.ppt
Adams空间复杂机械臂动力学仿真研究.doc
【资源说明】 基于Android+OpenCV的车牌识别系统源码+使用文档+全部资料(优秀项目).zip基于Android+OpenCV的车牌识别系统源码+使用文档+全部资料(优秀项目).zip基于Android+OpenCV的车牌识别系统源码+使用文档+全部资料(优秀项目).zip 【备注】 1、该项目是个人高分毕业设计项目源码,已获导师指导认可通过,答辩评审分达到95分 2、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 3、本项目适合计算机相关专业(如软件工程、计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载使用,也可作为毕业设计、课程设计、作业、项目初期立项演示等,当然也适合小白学习进阶。 4、如果基础还行,可以在此代码基础上进行修改,以实现其他功能,也可直接用于毕设、课设、作业等。 欢迎下载,沟通交流,互相学习,共同进步!