ZK是一套以 AJAX/XUL/Java 为基础的网页应用程序开发框架,用于丰富网页应用程序的使用界面。最大的好处是,在设计AJAX网络应用程序时,轻松简便的操作就像设计桌面程序一样。 ZK包含了一个以AJAX为基础、事件驱动(event-driven)、高互动性的引擎,同时还提供了丰富多样、可重复使用的XUL与HTML组件,以及以 XML 为基础的使用界面设计语言 ZK User-interfaces Markup Language (ZUML)。这是百度给的介绍。
然而作为一个国外开源框架,它给我的感觉就是不用写前端javascript,浏览器与服务器的交互次数变多,服务器压力变大。所以,使用该框架者应该慎重综合考虑。不要为了一时的偷懒毁掉整个项目。下面我就将罗列出我开发过程中积累的一些开发技巧、要点、注意事项,以及ZK开发的方法。
Ctrl页面定义页签头和页签容器变量:
private Tabs tabs;
private Tabpanels tabpanels;
然后在初始化afterCompose方法中初始化上面2个变量
public void afterCompose(@ContextParam(ContextType.VIEW) Component view) { tabs = (Tabs) view.getParent().getParent().getParent().query("tabs"); tabpanels = (Tabpanels) view.getParent().getParent(); ... }
Ctrl类中其他方法触发打开新页签
public void onAdd() { Tab tabNew = new Tab(); tabNew.setId("tab_id");//Tab的ID,必须唯一 tabNew.setClosable(true); tabNew.setLabel("TabName");//Tab名称 try { Tabpanel tabpanel = ZkUtils.newTab(tabNew, "/web/**/**.zul", tabs, tabpanels, ZkUtils.OverFlowType.AUTO, null); } catch (TabDuplicateException ex) { logger.error(ex.getMessage()); } logger.debug("正在新页签中打开页面【***.zul】"); }
注意事项:
新打开的页签页面zul中的window组件的id不要命名,否则会因window重名而无法重复打开多个。
打开新页签时的参数map中传入tab,然后就可以在tab页面中tab.close()关闭了......
TODO
金额统一按照小数点后2为显示,构造通用方法,供系统统一使用
// 在对应的zul页面引入如下代码<?xel-method prefix="c" name="formatStock" class="com.misumi.newcim.ctrl.component.FormatElNumber" signature="java.lang.String formatStock(double)"?> ......//然后按照如下方式应用<label value="${c:formatStock(each.igcStandardCredit)}"></label> ......
必填字段添加红色*号标记,因在textbox后方添加时,会导致textbox框缩小,与其他非必填项的textbox长度不一致
设计改为将*号显示在字段名称后方括号内 name(红色*)。 具体更改方案:
原lable字段更改为hbox
<hbox pack="end" align="end" hflex="true"> <label value="客户编码(" /> <label value="*" sclass="red" /> <label value="):" /> </hbox>
信息一览实现配置文件读取显示方式,配置在webinfo目录文件下,app.properties、sys.porperties文件分别载入基本功能区域的信息提示和系统管理的信息提示。具体案例如下:
启动时zk.xml文件加载配置:
<system-config> <label-location>/WEB-INF/i18n/sys.properties</label-location> <label-location>/WEB-INF/i18n/app.properties</label-location> </system-config>
字符串:
sys.porperties中配置
common.sys.queryError=检索失败!
Ctrl引用
Labels.getLabel("common.sys.queryError");
带参数字符串:
sys.porperties中配置
sys.NotifyRuleCtrl.ruleCodeRepeat=规则代码{0}和已有的重复,请修改后再保存。
Ctrl引用
Labels.getLabel("sys.NotifyRuleCtrl.ruleCodeRepeat", new java.lang.Object[] { notifyRule.getCode() });
页面constraint引用参考:
constraint="/^\w{1,6}$/:${labels.customer.cust.detail.groupCode.error}"
Combobox的默认选中(默认选择某个comboitem项)是常见需求,但是这里有些说道:
简单概括就是:zk通过==比较(即地址比较)来决定选中哪个comboitem!
zul页面:
<combobox selectedItem="@bind(fx.custCatg)" model="@load(vm.custCatgList)" itemRenderer="com.misumi.newcim.ctrl.renderer.ComboitemRenderer4CustCatg"></combobox>
例如上面的示例,fx.custCatg必须和vm.custCatgList中的某个元素是相同的实例,才能实现该元素的选中。
因此,即便你为fx.custCatg设置的po与vm.custCatgList中的某个元素id相同(在数据库中是同一条记录),但是如果它们在内存中不指向同一实例,也无法默认选中!
在hibernate作为持久层的时候,怎么才能确保上面的条件呢? 有两种方式:
开启OpenSessionInView,hibernate可以确保session一级缓存中任一id的PO实例只有一个,于是只要为fx.custCatg设置了PO就ok;
循环vm.custCatgList找到匹配的元素,然后赋值给fx.custCatg
注意 combobox.setValue(null);的使用!
介绍树形结构的实现:
...... <parameTreeBandbox selectProcessor="@load(vm.selectProcessorComplaintType)" bandboxdisabled="@load(vm.allowUpdateComplaint)" bandboxData="@load(vm.bandboxComplaintTypeData)" width="250px" paramTreeType="@load(vm.treeComplaintType)" bandboxValue="@save(fx2.complaintType) @load(vm.complaintType)" bandboxConstraint="@load(vm.const4NoEmpty)" /> ......
自定义的树形组件:
ParameTreeBandbox.java
setSelectProcessor():设置树形的选中事件 setBandboxdisabled():设置树形可用不可用 setBandboxData():设置树形的数据源 setParamTreeType():设置树形的数据类型 setBandboxValue():设置树形的value setBandboxConstraint():设置树形的校验
实现一颗树形结构:
//在页面引入 ..........<?component name="parameTreeBandbox" extends="vlayout" class="com.misumi.newcim.ctrl.component.ParameTreeBandbox"?>.......... <parameTreeBandbox selectProcessor="@load(vm.selectProcessorComplaintType)" bandboxdisabled="@load(vm.allowUpdateComplaint)" bandboxData="@load(vm.bandboxComplaintTypeData)" width="250px"paramTreeType="@load(vm.treeComplaintType)" bandboxValue="@save(fx2.complaintType) @load(vm.complaintType)" bandboxConstraint="@load(vm.const4NoEmpty)" /> ..........
ComplaintDetailCtrl.java
//声明数据类型 public String getTreeComplaintType() { return "com.misumi.newcim.model.system.ParamTreeComplaintType"; } //构造数据源 public ParameTreeBandboxData getBandboxComplaintTypeData() { return new ParameTreeBandboxData() { @Override public List<ParamTree> queryData(Class className) { ........ } }; } //重写选中事件 public ParameTreeBandboxSelectProcessor getSelectProcessorComplaintType() { return new ParameTreeBandboxSelectProcessor() { @Override public void onSelect(ParamTree pt) { ........ } }; }
表单校验是重要且常见的需求,ZK支持两种机制的Validation:
constraint前端验证:在输入组件的constraint属性指定验证规则,进行简单的验证;
集成JSR303、Hibernate Validation的服务端验证:在model字段上声明annotation,可以进行服务器端复杂校验,例如需要访问数据库的;
前者的优势是简单、用户体验好(焦点离开后立刻验证);后者优势是支持复杂的校验;
经过一番尝试后,建议优先采用constraint前端校验,
但是当表单过大出滚动条 且验证的字段很多时,漂浮的标签显示错误信息使得页面效果太凌乱、显示也不正常,
因此我们扩展了constraint,以label文本在出错字段下面红字显示错误提示。
zul代码
<label value="客户编码:" /> <vlayout> <textbox width="250px" value="@bind(fx.chaUniqueKey)" constraint="@load(vm.noEmptyConst)" disabled="@load(vm.allowUpdate)" /> <label sclass="red" /> </vlayout> <label value="客户名称:" /> <cell colspan="4"> <vlayout> <textbox width="93%" value="@bind(fx.customerNtvName)" constraint="@load(vm.noEmptyConst)" disabled="@load(vm.allowUpdate)" /> <label sclass="red" /> </vlayout> </cell>
VM代码
public Constraint getNoEmptyConst() { return new BaseConstraint("no empty, start_before :不能为空哦!"); } public Constraint getNatureNumConst() { return new BaseConstraint("no empty,no zero, no negative:rec数不能为空,且只能为自然数哦!"); } public Constraint getSoNumConst() { return new BaseConstraint("no empty,/^\\d{9}|\\d{12}|\\d{14}|\\d{15}$/:SO号码不能为空,且必须为9位、12位、14位或15位!"); } public Constraint getClassifyCodeConst() { return new BaseConstraint("no empty,/^(\\d){0,8}$/:classifycode不能超过8位且必须输入数字!"); } public Constraint getEmailConstraint() { return new BaseConstraint("/^(.+@.+\\.[a-z]+)|^$/ :非法的email!"); }
多个日期组件datebox可以互相影响对方的选中范围!!!
validator使用方式
直接属性验证(Property Validator)
对组件的属性相关的验证事件触发在onchange等情况下。适合单个属性的验证。
例子:
zul页面
<intbox value="@save(vm.quantity) @validator(vm.rangeValidator)"/>
Model.java
public Validator getRangeValidator(){ return new AbstractValidator() { public void validate(ValidationContext ctx) { Integer val = (Integer)ctx.getProperty().getValue(); if(val<10 || val>100){ addInvalidMessage(ctx, "value must not < 10 or > 100, but is "+val); } } }; }
依赖性的验证(Dependent Property Validation )
适合对form表单进行提交集体验证。我们通常需要验证一个属性在同一时间,最简单的方法是继承和重写abstractvalidator validate()实现自定义验证规则。
例子:
zul页面
...<toolbar> ... <button label="Save" onClick="@command('saveOrder')" disabled="@bind(empty vm.selected)" /> ...</toolbar><groupbox form="@id('fx') @load(vm.selected)@save(vm.selected, before='saveOrder') @validator(vm.shippingDateValidator)"> <grid hflex="true" > <columns> <column width="120px"/> <column/> </columns> <!-- other components --> <rows> <row>Creation Date <hlayout> <datebox id="cdBox" value="@bind(fx.creationDate) @validator(vm.creationDateValidator)"/> <label value="@load(vmsgs[cdBox])" sclass="red" /> </hlayout> </row> <row>Shipping Date <hlayout> <datebox id="sdBox" value="@bind(fx.shippingDate)"/> <label value="@load(vmsgs[sdBox])" sclass="red" /> </hlayout> </row> </rows> </grid></groupbox>
Validator.jva继承abstractvalidator
public class ShippingDateValidator extends AbstractValidator{ public void validate(ValidationContext ctx) { Date shipping = (Date)ctx.getProperty().getValue();//the main property Date creation = (Date)ctx.getProperties("creationDate")[0].getValue();//the collected //multiple fields dependent validation, shipping date have to large than creation more than 3 days. if(!isDayAfter(creation,shipping,3)){ addInvalidMessage(ctx, "must large than creation date at least 3 days"); } } static public boolean isDayAfter(Date date, Date laterDay , int day) { if(date==null) return false; if(laterDay==null) return false; Calendar cal = Calendar.getInstance(); Calendar lc = Calendar.getInstance(); cal.setTime(date); lc.setTime(laterDay); int cy = cal.get(Calendar.YEAR); int ly = lc.get(Calendar.YEAR); int cd = cal.get(Calendar.DAY_OF_YEAR); int ld = lc.get(Calendar.DAY_OF_YEAR); return (ly*365+ld)-(cy*365+cd) >= day; } }
页面提示信息展示(Validation Message Holder)
继承和重写abstractvalidator validate()实现自定义验证规则后,如果验证失败,使用addinvalidmessage()存储验证显示的消息。此时消息被加载到 Validation Message Holder中,页面定义validationMessages即可获取。其中下面例子中,@bind(vmsgs['fkey1'])、@bind(vmsgs['fkey2'])、@bind(vmsgs['fkey3'])这里面的key(fkey1、fkey2、fkey3)是可以自己在java代码自己定义(详见下面java代码注释掉的部分),默认key为验证组件的id(t41、t42、t43)
例子:
zul页面
<window apply="org.zkoss.bind.BindComposer" viewModel="@id('vm')@init('foo.MyViewModel')" validationMessages="@id('vmsgs')"><vbox form="@id('fx') @load(vm) @save(vm,before='submit') @validator(vm.formValidator)"> <hbox><textbox id="t41" value="@bind(fx.value1)"/><label id="l41" value="@bind(vmsgs['fkey1'])"/></hbox> <hbox><textbox id="t42" value="@bind(fx.value2)"/><label id="l42" value="@bind(vmsgs['fkey2'])"/></hbox> <hbox><textbox id="t43" value="@bind(fx.value3)"/><label id="l43" value="@bind(vmsgs['fkey3'])"/></hbox> <button id="submit" label="submit" onClick="@command('submit')" /> </vbox></window>
Validator中addInvalidMessage()添加验证消息
public Validator getShippingDateValidator() { return new AbstractValidator(){ public void validate(ValidationContext ctx) { Date shipping = (Date)ctx.getProperties("shippingDate")[0].getValue(); Date creation = (Date)ctx.getProperties("creationDate")[0].getValue(); //dependent validation, shipping date have to later than creation date for more than 3 days. if(!CalendarUtil.isDayAfter(creation,shipping,3)){ addInvalidMessage(ctx,"must large than creation date at least 3 days"); //addInvalidMessage(ctx, "fkey1", "value1 error"); //addInvalidMessage(ctx, "fkey2", "value2 error"); //addInvalidMessage(ctx, "fkey3", "value3 error"); } } }; }
采用Java annotation实现Validator注入
直接使用现有框架的annotation,如org.hibernate.validator。例子:
Java annotation实现Validator注入
public static class User{ private String _lastName = "Chen"; @NotEmpty(message = "Last name can not be null") public String getLastName() { return _lastName; } public void setLastName(String name) { _lastName = name; } }
自定义Java annotation并实现Validator
Model.java
@DistinctSysRole(targetProperty = "code") public class SysRole { private String code; public String getCode() { return code; } public void setCode(String code) { this.code = code; } }
自定义Java annotation
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target({ TYPE, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = SysRoleDistinctValidator.class) @Documentedpublic @interface DistinctSysRole { message() default "角色代码不能重复"; String targetProperty(); Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
自定义Validator
public class SysRoleDistinctValidator implements ConstraintValidator<DistinctSysRole, SysRole> { @Resource RoleService roleService; @Override public void initialize(DistinctSysRole constraintAnnotation) { // TODO Auto-generated method stub } @Override public boolean isValid(SysRole role, ConstraintValidatorContext context) { return roleService.checkDistinct(role); } }
Validator的更多详细的介绍,参考官方解说:
http://books.zkoss.org/wiki/ZK_Developer%27s_Reference/MVVM/Data_Binding/Validator