校验是指在用户输入数据时、或者在用户输入数据结束并向数据库提交时做出判断,判断用户输入的合法性,如不满足合法性要求,则要求用户进行修改。
转换是指在数据显示之前或数据提交之后对数据本身或其类型进行一定的改变。简单地说,转换是确保数据拥有正确的对象或者类型的过程。
常见的数据校验有两种:
在OperaMasks中有以下两种预置的校验器:
一种是构件内置的校验
所有的输入构件(UIInput的子类,如w:textField、w:dateField、w:numberField、w:combo等)都有required属性,这个属性设置为true后在form提交时会触发构件内置的校验。
<w:form>
<w:textField required="true" requiredMessage="此输入框值不能为空!"/>
<w:button label="提交"/>
</w:form>如果输入框中没有值直接点提交按钮时,会出现下面的结果:
所有的Field构件(UIField的子类,如w:textField、w:dateField、w:numberField、w:combo、w:checkBox等)都有allowBlank属性,此属性与required属性相反,这个属性设置为false后,在构件失去焦点时会触发构件内置的校验。
<w:dateField allowBlank="false" blankText="不能为空"/> <w:textField allowBlank="false" blankText="不能为空"/>
如果输入框中没有值时,会出现与上图类似的结果。
w:textField和w:combo构件有vtype属性(可选值有alpha、alphanum、email、url),当构件输入值并失去焦点时会触发内置的校验。
<w:textField vtype="email" vtypeText="不是一个有效的email地址!" msgTarget="under"/> <w:textField vtype="url" vtypeText="不是一个有效的url地址!" msgTarget="under"/>
如果在输入框中输入非法值时,会出现下面的结果:
有些特殊的构件内置了特殊的校验器。比如:w:dateField输入值并失去焦点时会触发内置的校验器来判断输入的是否是一个有效的日期;w:numberField输入值并失去焦点时会触发内置的校验器来判断输入的是否是一个有效的数字。
<w:dateField msgTarget="under"/> <w:numberField msgTarget="under"/>
如果在输入框中输入非法值时,会出现下面的结果:
另一种是校验器构件
在OperaMasks中提供了3个专门用于校验的构件。如下图:
1、 f:validateDoubleRange构件用于验证用户输入是否为处于有效范围之内的浮点数。
2、 f:validateLength构件用于验证用户输入的值是否为处于有效长度之内。
3、 f:validateLongRange构件用于验证用户输入是否为处于有效范围之内的整数(包括整形,长整形等)。
请在下面输入框中输入你的用户名,最少6个字符,最多10个字符
<w:form>
<w:textField>
<f:validateLength minimum="6" maximum="10" />
</w:textField>
<w:button label="提交" />
</w:form>如果输入框中输入的值长度小于6时,会出现下面的结果:
当然上面几个内置的检验是不足以满足日常应用的,这时我们可以自己定义校验,自己定义的校验分为两种:客户端校验和服务器端校验。其中服务器端校验分为5种:IoVC简单校验、@Validate校验、在Action或ActionListener中校验、独立校验器类校验、编写生命周期侦听器校验(由于编写生命周期侦听器校验方法比较复杂,并且需要维护配置文件,并不推荐使用,下面我们只讲前4种)。
顾名思义,ajax:clientValidator就是以Ajax方式在客户端对输入构件的值进行校验。我们看一段代码:
<w:form clientValidate="true"> <w:textField id="username"> <ajax:clientValidator message="admin是系统预留帐号,不可注册!"> if(value == 'admin') return false; else return true; </ajax:clientValidator> </w:textField> </w:form>
这段代码用于用户注册时判断用户输入的注册名是不是admin,如果是则提示错误。此校验在构件失去焦点时触发。效果如下:
当然啦,校验功能最强大的正则表达式在这里也是可以使用的。下面这段代码是对固定电话号码的校验:
<w:form clientValidate="true">
<w:textField id="tel">
<ajax:clientValidator message="请输入有效的电话号码(限国内固定电话)!">
var re = new RegExp("^0[1-9]\\d{1,2}-?[1-9]\\d{6,7}$");
return re.test(value);
</ajax:clientValidator>
</w:textField>
</w:form>其中正则表达式re的意思是说:3-4位区号,后面7-8位短号;区号和短号之间可以用-连接,也可以省略-;区号第一位必须是0,区号第二位和短号第一位必须是1-9之间的数字,不能是0。运行后效果与上图类似。
前面所有的例子都是在客户端进行的校验,那么如何在服务器端进行校验呢?
有时我们为了保证页面代码的简洁性(便于美工人员进行美工),我们需要将本章第一节中讲的OperaMasks预置的校验器校验移到LiteBean中,这时对于通过IoVC绑定的构件属性,我们可以通过在绑定属性上加上校验器注解的方式来请求校验。OperaMasks中预定义了一些常用的校验器注解:
@Required. 声明一个域或Bean属性必须具有输入值。
@ValidateDoubleRange. 检验一个浮点数取值是否在指定范围中。
@ValidateLongRange. 检验一个整型数取值是否在指定范围中。
@ValidateLength. 检验一个字符串的长度是否在指定范围中。
@ValidateRegexp. 检验一个字符串是否可以匹配指定的正则表达式。(OperaMasks提供了一个工具类org.operamasks.faces.validator.CommonRegexpPatterns,列出了一些常用的校验正则表达式,如Email地址、IP地址、货币、浮点数、整数、网址、身份证号码、中文字符、信用卡等)
例如,下面是一个很常见的页面代码:
<w:form> <layout:panelGrid columns="3"> <h:outputLabel for="name" value="姓名:" /> <w:textField id="name" /> <h:message for="name" /> <h:outputLabel for="weight" value="体重:" /> <w:textField id="weight" /> <h:message for="weight" /> <h:outputLabel for="age" value="年龄:" /> <w:textField id="age" /> <h:message for="age" /> <h:outputLabel for="usedName" value="曾用名:" /> <w:textField id="usedName" /> <h:message for="usedName" /> <h:outputLabel for="idNum" value="身份证号:" /> <w:textField id="idNum" /> <h:message for="idNum" /> </layout:panelGrid> <w:button label="提交" /> </w:form>
其中的layout:panelGrid只用于布局,与校验无关。h:message仅用于显示校验错误信息,与校验也无关。现在我们来进行了一些IoVC简单校验:
@Bind @Required(message="姓名不能为空!") private String name; @Bind @ValidateDoubleRange(minimum=10.00,maximum=200.00,message="体重必须为10.00-200.00之间的小数!") private Double weight; @Bind @ValidateLongRange(minimum=18,maximum=60,message="年龄必须为18-60之间的整数") private int age; @Bind @ValidateLength(minimum=2,maximum=4,message="必须是2-4个汉字!") private String usedName; @Bind @ValidateRegexp(value=CommonRegexpPatterns.IDENTITY_CARD,message="必须是有效的15位或18位的身份证号码!") private String idNum;
运行此页面,输入非法信息,点提交按钮后效果如下图。
上面这些注解同样只能进行一些简单的非空、长度、范围校验及正则式规则校验。如果我们要进行复杂一些的校验,代码较长时怎么办?
OperaMasks提供了@Validate注解,用于将LiteBean中的方法注解为指定构件的校验方法。@Validate绑定构件的方式有两种:
通过注解自身的value属性指定,例如,@Validate(id={"first"})表明被注解的方法将作为id是first的构件的校验方法。
噫,这个注解的id属性怎么跟其它注解中不一样?是的,多了一对大括号,注意这个注解的id属性是一个String[]类型的构件。这就意味着可以同时指定多个构件,当指定多个构件时,被注解的方法将同时为多个构件提供校验。
@Validate(id={"first","second"})
public boolean anyMethodName(Object value){
......
}通过“约定优于配置”规则绑定,若未指定id属性,而且被注解的方法签名以“validate”开头,则取后续字符串首字小写作为绑定id。例如以下方法将成为id是first的构件的校验方法:
@Validate
public boolean validateFirst(Object value){
......
}下面举个简单的例子来说明一下@Validate注解的用法。假如某个页面中有个输入框用于输入月份,且当月份小于10时允许前面输入0,也可以不输入。这时我们就不能用一个int型属性来保存它了,当然也就不能用上面的@ValidateLongRange来校验了。只能用一个String类型属性来保存它,那么如何校验呢?当然可以用@ValidateRegexp注解来写个正则表达式校验,我们看看用方法怎么校验。
<w:form>
<w:textField id="month" />
<w:button label="提交" />
</w:form>@Bind
private String month;
@Validate(message = "月份有效格式为1-12或01-12")
public boolean validateMonth(String value) {//此处参数类型与@Bind绑定的value属性类型一致
if (value.length() == 2 && value.startsWith("0") && value.compareTo("01") >= 0 && value.compareTo("09") <= 0)
return true;
if (value.length() == 2 && value.startsWith("1") && value.compareTo("10") >= 0 && value.compareTo("12") <= 0)
return true;
if (value.length() == 1 && value.compareTo("1") >= 0 && value.compareTo("9") <= 0)
return true;
return false;
}到目前为止,我们所见到的@Validate注解的方法的方法签名都是public boolean methodName(Object value);事实上,这个方法签名可以有很多种,除了直接返回boolean值的这种最简单形式,还允许:
标准写法:
public void validate(FacesContext context, UIComponent component, Object value);
这是javax.faces.validator.Validator接口所定义的标准方法,采用这种写法可以得到当前正在校验的UI构件,以获得更多的控制。由于方法没有返回值,用户需要使用构件的setValid(boolean)方法标记构件是否合法,并自行使用context.addMessage()方法添加出错信息。
简略写法
public boolean validate(Object value);
对输入值进行校验,如果成功则返回true,否则返回false。当采用这种写法时最好设置@Validate注解的message属性,用于提供出错信息。
返回不同出错信息的简略写法
public String validate(Object value);
当需要根据不同的校验结果显示不同的出错信息时可以采用这种写法,当方法返回null时表示校验成功,否则将返回值作为出错信息。注意在返回的字符串中可以包含EL表达式,因此可以很容易地实现国际化而不是硬编码的固定字符串。
抛出ValidatorException的简略写法
public void validate(Object value) throws ValidatorException;
同第三种方式类似,只不过将出错信息包含在ValidatorException中抛出。
校验的目的是为了保证在使用LiteBean的某个属性时,它的值是合法的。在OperaMasks请求处理生命周期中,使用这个值有3个地方:更新模型值阶段(Update Model Value Phase)、调用应用程序阶段(Invoke Application Phase)和渲染响应阶段(Render Response Phase),其中我们可以通过IoVC加入控制逻辑的时机有两个:调用应用程序整个周期及渲染响应阶段之前的BeforeRender周期。
默认的校验是发生在更新模型值阶段之前的处理验证阶段(Process Validations Phase)阶段,如果页面输入的值可以成功更新到模型中去,而只是逻辑上不合法,我们可以延后再检验它。
也就是说,如果我们会调用应用程序,且在此阶段使用LiteBean的某个属性值,我们可以在Action或ActionListener中校验。如果我们不调用应用程序,我们也可以在BeforeRender时进行校验。
再次说明一下:这种延后的校验不可处理输入数值的物理不合法(比如类型无法自动转换、数值超出范围等),只能处理类型合法范围合法而在某种场景下逻辑不合法的输入数值。
对于上一节中相同的页面代码,我们可以这样校验:
@Bind
private String month;
@Action
public void doSomething() {
boolean flag = false;
if (month.length() == 2 && month.startsWith("0") && month.compareTo("01") >= 0 && month.compareTo("09") <= 0)
flag = true;
if (month.length() == 2 && month.startsWith("1") && month.compareTo("10") >= 0 && month.compareTo("12") <= 0)
flag = true;
if (month.length() == 1 && month.compareTo("1") >= 0 && month.compareTo("9") <= 0)
flag = true;
if (flag == false){
//输出一个错误提示到页面中某个地方
}else{
//值已经合法了,可以执行正常的处理逻辑
}
}ActionListener中与Action类似,BeforeRender中写法也与上面类似,只是与校验仅仅能影响页面的显示,而不能影响业务逻辑的执行。
上面的这些服务器端检验的方法都将代码直接写在某个页面对应的LiteBean中了,一方面不便于复用,另一方面使得LiteBean代码比较臃肿,都不便于维护。我们可以编写一个独立的检验器类,在需要使用的地方引用这个类就可以了。
在OperaMasks中定义独立的校验器类也是非常方便的,只需要在类定义时加上@DefineValidator注解,此类就会成为一个有效的校验器类,无需在配置文件中声明,也无需继承 javax.faces.validator.Validator接口(继承亦可):
若被注解的类继承了javax.faces.validator.Validator接口,将使用其validate方法进行校验。
若被注解的类没有继承javax.faces.validator.Validator接口,将使用加上了@Validate的方法进行校验。
例如,需要定义一个简单的校验器,当输入值为字符串“bad”时校验失败。以下三种写法是完全等价的:
写法一,@Validate简略写法:
@DefineValidator
public class BadStringValidator {
@Validate(message="非法的字符串")
public boolean validate(Object value) {
if (value == null || "bad".equalsIgnoreCase(value.toString()))
return false;
else
return true;
}
}写法二,@Validate标准写法:
@DefineValidator(id="badStringValidator")
public class BadStringValidator {
@Validate
public void validate(FacesContext context, UIComponent component, Object value) {
if (value == null || "bad".equalsIgnoreCase(value.toString())) {
((UIInput) component).setValid(false);
context.addMessage(component.getClientId(context),
new FacesMessage(FacesMessage.SEVERITY_ERROR, "非法的字符串", "非法的字符串")
);
}
}
}写法三,实现Validator接口:
@DefineValidator(id="badStringValidator")
public class BadStringValidator implements Validator {
public void validate(FacesContext context, UIComponent component, Object value) {
if (value == null || "bad".equalsIgnoreCase(value.toString())) {
((UIInput) component).setValid(false);
context.addMessage(component.getClientId(context),
new FacesMessage(FacesMessage.SEVERITY_ERROR, "非法的字符串", "非法的字符串")
);
}
}
}这样一个校验器类就算定义好了,对于一些能设置validator属性的构件就可直接通过上面定义的校验器类中的校验器id使用它了,例如:
<w:textField id="first" validator="badStringValidator"/>
但是,由于没有为这个校验器类定义对应的校验器构件标签,对于那些通过子构件形式来设置校验器的构件,这个校验器类无法直接在页面使用。对此,可以在LiteBean中使用另一个IoVC标签@Validator进行绑定。
@Bind @Validator(BadStringValidator.class) private String first;
这种方法与上面直接使用构件的validator属性效果一样,如下图。
我们发现自己编写的独立校验器类工作正常了。但是如果我们在另一个系统中也到用到这个校验器类,但是要求当输入值为字符串“notbad”时校验失败,怎么办?改一下校验器代码?有没有更好的办法?能不能为这个校验器传入参数?
至此我们已经用了很多的注解了,而且这些注解大部分都可以使用参数,我们可不可以将我们的校验器类也做成注解?在OperaMasks中这也是可以做到的。我们可以再写一个Annotation类,指定其实现类为刚才那个BadStringValidator.class校验器类就行了。来看代码:
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Validator(BadStringValidator.class)
public @interface ValidateBadString {
}好了,我们的注解做好了。那个校验器类不用做任何修改,现在我们将LiteBean中使用校验类的地方改为:
@Bind @ValidateBadString //这里原来是@Validator(BadStringValidator.class) private String first;
这里@ValidateBadString的ValidateBadString就是那个Annotation类的类名。运行页面,可看到展现效果与改动前完全一致。
此时还是不能传入参数,我们再来改一下ValidateBadString.class这个Annotaion类。代码改为:
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Validator(BadStringValidator.class)
public @interface ValidateBadString {
public String invalidValue() default "bad";
}再来修改一下校验器类BadStringValidator.class,让它使用这个参数。代码如下:
@DefineValidator
public class BadStringValidator {
private String invalidValue;
public String getInvalidValue() {
return invalidValue;
}
public void setInvalidValue(String invalidValue) {
this.invalidValue = invalidValue;
}
@Validate
public void validate(FacesContext context, UIComponent component, Object value) {
if (value == null ||
this.invalidValue.equalsIgnoreCase(value.toString())) {
((UIInput) component).setValid(false);
context.addMessage(component.getClientId(context),
new FacesMessage(FacesMessage.SEVERITY_ERROR, "非法的字符串", "非法的字符串")
);
}
}
}正如我们所期望的那样,现在我们可以为这个注解传入参数了,把应用场景中的LiteBean代码改为:
@Bind @ValidateBadString(invalidValue="notbad") private String first;
好了,运行页面,在textField中输入notbad并提交,可看到校验器注解已生效。这样以后在任何要限制不能输入的字符时我们就可以用这个注解了,通过invalidValue参数传入要限制的值。当然你也可以自己对上面的代码进行修改,让它一次可以传入多个限制字符。
上面我们讲了各种校验方法,有客户端的,有服务器端的。客户端的效果好些,但是大量使用客户端校验的话使得页面的代码过于复杂,不便于美工对页面美化。服务器端解决了此问题,但是相对效率会降低,每次都要一个请求来回才能知道输入的值不合法。有没有一种方法可以将校验代码写到LiteBean中,但是在执行校验时却是发生在客户端呢?
幸运的是,所有预定义的校验器都支持切换到客户端校验。只需要在页面中构件所属的w:form标签上设定属性:clientValidate="true"就可以了。来做个试验看看:
<w:form clientValidate="true"> <layout:panelGrid columns="2"> <h:outputLabel for="name" value="姓名:" /> <w:textField id="name" /> <h:message for="name"/> <h:outputLabel for="age" value="年龄:" /> <w:textField id="age" /> <h:message for="age"/> </layout:panelGrid> <w:button label="提交" /> </w:form>
@Bind @Required(message = "姓名不能为空!") private String name; @Bind @ValidateLongRange(minimum = 18, maximum = 60, message = "年龄必须为18-60之间的整数") private int age;
先看一下运行效果:
工作也很正常,我们怎么知道这次是客户端校验呢?查看一下页面源代码:
OM.E(function(){
j_id2$name = new Ext.form.TextField({
validateOnBlur:false,
validator:function(value){
var validators = [new RequiredValidator('姓名不能为空!')];
for(var n = 0; n < validators.length; n++){
var result = validators[n].validate(value);
if(result===true){continue;} else {return result;}
}
return true;
},
value:'',
msgTarget:"qtip",
......
});
OM.E(function(){j_id2$name.on('blur',function(){return this.validate();});
});
OM.E(function(){
j_id2$age = new Ext.form.TextField({validateOnBlur:false,
validator:function(value){
var validators = [new IntegerValidator('年龄:: \'{0}\'必须是一个包含一或多个数字的整数。',-2147483648,2147483647),new IntegerValidator('年龄必须为18-60之音的整数',18,60)];
for(var n = 0; n < validators.length; n++){
var result = validators[n].validate(value);
if(result===true){continue;} else {return result;}
}
return true;
},
value:'0',
msgTarget:"qtip",
......
});
OM.E(function(){j_id2$age.on('blur',function(){return this.validate();});
});我们再去掉clientValidate="true"属性,再运行一下看看页面源代码:
OM.E(function(){
j_id2$name = new Ext.form.TextField({validateOnBlur:false,
value:'',
msgTarget:"qtip",
......
});
});
OM.E(function(){
j_id2$age = new Ext.form.TextField({validateOnBlur:false,
value:'0',
msgTarget:"qtip",
......
});
......
});通过比较,发现使用了clientValidate时页面源代码中多了一些代码,而且这些代码刚好是校验页面那些输入框的代码,也可以通过其它网络监控软件跟踪一下看看clientValidate="true"时校验过程中并没有发送网络请求,而clientValidate="false"时校验过程有发送网络请求。
再次说明一下:clientValidate="true"属性只支持预定义的校验器,对于用户自己定义的@Validate注解的方法校验及自定义的独立校验器类校验并不支持。
在一些情况下,我们可能需要对客户端校验进行一些更精细的控制,比如说,在构件的onchange事件触发后才进行校验,或者在一个构件输入后对表单中的其他构件进行校验等等。对于这些需求,OperaMasks 2.3及以后版本提供了更好的支持。
下面且看OperaMasks 2.3版本客户端校验的一些新特性
用户可自定义客户端校验的触发时机
在w:form中,如果只设置了的clientValidate属性为true,那么其中的Field构件一般会在输入时和表单提交前进行客户端校验。为更好地控制校验触发的时机,在2.3版本为w:form和Field构件添加了几个属性,如下
为w:form添加了属性validateBeforeSubmit(当clientValidate属性为true时生效,默认为true,可用来设置表单提交时是否进行客户端校验)和属性validateEvents(用来设置校验触发的事件)
为所有Field构件添加clientValidate属性(设置是否进行客户端校验)和validateEvents属性,这两个属性如果在w:form和Field构件都进行了设置,那么Field构件的设置覆盖w:form的设置
一个小例子如下
<w:form clientValidate="true" validateEvents="onchange, onfocus"validateBeforeSubmit="false"
> <w:textField required="true"> <f:validateLength minimum="3" maximum="6"></f:validateLength> </w:textField> </w:form>
ajax:clientValidator增强
我们知道,ajax:clientValidator 构件有一个"message"的属性可以用来设置校验错误时展现的信息,但是我们可能需要根据不同的结果显示不同的错误信息,如下
<w:form clientValidate="true">
<w:textField id="username">
<ajax:clientValidator>
if (value == 'admin')
return "admin为系统预留账户,不可注册!";
if (value == 'guest')
return "guest已被注册,请选用其他账户!";
return true;
</ajax:clientValidator>
</w:textField>
</w:form>在ajax:clientValidator中除了可返回boolean类型,还可以返回String类型,如果返回String类型,那么该String将作为错误信息展现.
使用JavaScript代码触发校验
为构件声明一个"jsvar"属性,然后可用JavaScript代码使其进行校验,如
<w:form clientValidate="true" onsubmit="return field.validate()">
<w:textField jsvar="field" required="true">
<f:validateLength minimum="3" maximum="6"></f:validateLength>
</w:textField>
</w:form>下面我们看看在一些可能出现的场景里如何应用客户端校验
表单中所有构件在onchange事件发生时进行客户端校验
<w:form clientValidate="true" validateEvents="onchange">
<w:textField required="true"></w:textField>
<w:textField required="true"></w:textField>
</w:form>表单中一个构件响应onkeyup进行校验,其他构件响应onblur进行校验
<w:form clientValidate="true" validateEvents="onblur">
<w:textField required="true" validateEvents="onkeyup"></w:textField>
<w:textField required="true"></w:textField>
<w:textField required="true"></w:textField>
</w:form>表单中的一个构件在某个事件发生时对自己和其他构件进行校验,比如常见的确认密码校验
<script>
//<![CDATA[
function validatePassConform(value) {
if (password.validate() && (value != password.getValue()
)) {
return "两次密码不一致";
}
return true;
}
//]]>
</script>
<w:form clientValidate="true">
<w:textField required="true" jsvar="password">
<f:validateLength minimum="6" maximum="15"></f:validateLength>
</w:textField>
<w:textField required="true" validateEvents="onblur">
<f:validateLength minimum="6" maximum="15"></f:validateLength>
<ajax:clientValidator>
return validatePassConform(value);
</ajax:clientValidator>
</w:textField>
</w:form>表单中有多个构件,提交时只对某个构件进行校验
<w:form clientValidate="true" validateBeforeSubmit="false" onsubmit="return password.validate();">
<w:textField jsvar="username" required="true">
<f:validateLength minimum="3" maximum="6"/>
</w:textField>
<w:textField jsvar="password" required="true">
<f:validateLength minimum="3" maximum="6"/>
</w:textField>
</w:form>表单只在提交时进行客户端校验
<w:form clientValidate="true" validateEvents=""> <w:textField jsvar="username" > <f:validateLength minimum="3" maximum="6"/> </w:textField> <w:textField jsvar="password"> <f:validateLength minimum="3" maximum="6"/> </w:textField> </w:form>