Hybridizing HTML
混合式HTML(二)
November 8, 2007
创建窗体
在CSS/JavaScript/AJAX窗体上我有不少负面经历,其中包含使用困难的时间选择器,这些也在旅游网站上出现过。还是让我们创建一个简单窗体,它可以找到你的启程和返程日期、姓名和email地址。
在输入数据提交至服务器之前,让我们先在客户端上加入对输入数据的有效性检查。因为这个功能会避免页面之间来回跳转,各类网站都用AJAX和JavaScript实现这个功能,但很难甚至看不到好效果。而Flex提供的内置Valuator对象可为你轻松完成这件事。对于特殊情况你也可以很方便地创建自己的Valuator子类型。我们总体上可以保证所有的输入数据在窗体被提交之前都是正确的,但用JavaScript实现的话就得花费些力气才能保证其可靠性。
这个窗体使用MXML窗体组件进行布局。该组件拥有FormItems,每一个FormItems都包含一个标签和一些其他类型的组件。你需要注意的是标签和组件都被巧妙地组织到一起了—而这是你要用HTML的table编写更多代码才能完成的。
在这篇文章中我大概介绍了一下MXML。下面是代码,在后面我会加以解释。所有<Form>标签里面的是窗体标签和组件,之后是有效性检查并提交至服务器。这里大多数代码是由FlexBuilder通过上下文补全(context-completion)自动生成的:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:validators="validators.*" applicationComplete="validator()">
<mx:Style> global { font-size: 15; } </mx:Style>
<mx:Form>
<mx:FormItem label="Departure Date" required="true">
<mx:DateField id="departureDate" change="departureChanged()"
selectedDate="{new Date()}" />
</mx:FormItem>
<mx:FormItem label="Return Date" required="true">
<mx:DateField id="returnDate" change="validator()"
selectedDate="{new Date(new Date().time + validators.ReturnDateValidator.ONEDAY)}" />
</mx:FormItem>
<mx:FormItem label="Traveler Last Name" required="true">
<mx:TextInput id="lastName" change="validator()"/>
</mx:FormItem>
<mx:FormItem label="Traveler First Name" required="true">
<mx:TextInput id="firstName" change="validator()"/>
</mx:FormItem>
<mx:FormItem label="Traveler's Email" required="true">
<mx:TextInput id="email" change="validator()"/>
</mx:FormItem>
<mx:FormItem>
<mx:Button id="submitButton" label="Submit" click="submit()"/>
</mx:FormItem>
</mx:Form>
<mx:Validator id="requireLastName" required="true" source="{lastName}" property="text" />
<mx:Validator id="requireFirstName" required="true" source="{firstName}" property="text" />
<mx:EmailValidator id="validateEmail" required="true" source="{email}" property="text" />
<validators:ReturnDateValidator id="validateReturnDate"
source="{returnDate}" property="selectedDate" departureDate="{departureDate}"/>
<mx:Script> <![CDATA[
private function departureChanged():void {
const ONEDAY:int = validators.ReturnDateValidator.ONEDAY;
if(returnDate.selectedDate.time >= departureDate.selectedDate.time + ONEDAY) return;
returnDate.selectedDate = new Date(departureDate.selectedDate.getTime() + ONEDAY);
validator();
}
private function validator():void {
submitButton.enabled = Validator.validateAll(
[requireLastName, requireFirstName, validateEmail, validateReturnDate]).length == 0;
}
private function submit():void {
var args:URLVariables = new URLVariables();
args.FirstName = firstName.text;
args.LastName = lastName.text;
args.Email = email.text;
args.DepartureDate = departureDate.selectedDate.toDateString();
args.ReturnDate = returnDate.selectedDate.toDateString();
var url:URLRequest = new URLRequest("http://www.mindviewinc.com/demos/flex/trip.php");
url.method = "POST";
url.data = args;
navigateToURL(url,"_blank");
}
]]> </mx:Script>
</mx:Application>
全局性地调整风格也很容易。在这里我使用<Style>设置窗体中所有字体大小为15。
可以看到,每一个FormItem中的required设置为true。这个标志可能会被误解,它的作用并不是要求用户须填满所有栏。而是在组件旁显示一个红色星号以告诉用户该栏是必须填写的。为了实现必填栏,后面你会看到我们是如何利用有效性检查做到的。
DateField是一个内置Flex组件,用于在选择日期时弹出日历项。它和常见的AJAX组件很相近,但前者可以在所有平台和配置下运行。在departureDate中,使用一个绑定表达式(大括号)将selectedDate设置为当前日期。returnDate被初始化为当前日期加1。
FormItem剩余部分就是TextInput和最后的一个提交按钮。只有当输入数据正确时按钮才会有效,点击按钮数据就会提交至服务器。
注意,窗体中每一个输入组件设置的change属性是一个方法,该方法执行有效性检查。
窗体数据的有效性检查
在<Form>之后就见到了四个Validator组件:其中的三个来自标准Flex库,还有一个是我自己写的。前两个保证了lastName和firstName TextInputs不为空,第三个要求email TextInput内容必须遵循email地址格式(这是一个成熟的标准Flex组件,你没必要自己重新造一个)。而自定义的ReturnDateValidator确保了返程日期大于启程日期。
每一个Validator都要知道在哪能找到要检查的数据,通过设置它的source(待检查栏,使用绑定表达式)和property栏(该组件内部所测试的属性)来完成。ReturnDateValidator也得要知道待比较的departureDate。
检查整个窗体有效性最简便的方法是调用static validateAll()。你可以在<Script>块里面的validator()方法中找到它。validator()包含一个Validator对象的数组并测试其中每一个元素,然后返回一个results数组。由于任意results都表示一种有效性检查失败,所以如果数组length不为0,那么窗体有效性检查即为失败(提交按钮也会无效)。
下面这个是一个更为复杂一点的窗体。我们不只想检查输入有效性,还想限制一下日期范围,使得旅行返程至少是在启程的一天之后。如果用户选择了启程日期,返程日期应自动初始化为启程的第二天,这样看起来才更合理一些。这也是departureDate DateField调用departureChanged()的原因(而不会像上面的validator()那样做)。在departureChanged()中返程日期被强制设置为启程的一天之后—之后它同样也调用了validator()。
然而,如果用户选择返程日期,我们就无法假定启程日期。能做的只是告诉用户为什么该栏是无效的。这也是ReturnDateValidator的入口。
一个自定义的Validator
自定义Validator是另一种自定义组件,我在这里曾介绍过。当子类化时,必须覆盖的方法只有doValidation()。在有效性检查期间,Validator的MXML声明中的source和property作为参数传递给该方法。
package validators {
import mx.controls.DateField;
import mx.validators.ValidationResult;
import mx.validators.Validator;
public class ReturnDateValidator extends Validator {
private var departure:DateField;
public static const ONEDAY:int = 1000 * 60 * 60 * 24; // In milliseconds
public function set departureDate(depart:DateField):void {
departure = depart;
}
protected override function doValidation(value:Object):Array {
var results:Array = super.doValidation(value);
if(results.length > 0)
return results;
if(value.time < departure.selectedDate.time + ONEDAY)
results.push(new ValidationResult(true, null, "returnBeforeDeparture",
"Return date must happen after departure date"));
return results;
}
}
}
该Validator和一般的Validator不太一样,因为它使用了两个UI组件,而大多数只用了一个。为了连接至第二个组件(另一个DateField),我引入了名为departureDate的属性,并通过set方法将其保存在departure栏中。
我在设计Validator的时候遇到了点麻烦。我首先覆盖了validator()方法,运行没问题,validateAll()也是。但是文档中提到说必须覆盖doValidation()而不应该覆盖validator()。可当我这么做的时候却出现一个运行时错误,因为创建对象Validator时必须设定source和property,然后value参数通过合并source和property由框架传递给doValidation()。我原以为只需用到source,但为了满足框架两个确实都要用。这是定义你自己的Validator的必要条件。这样设计的原因对我来说虽不是显而易见的,但它们确实能保证其运行起来。
在ReturnDateValidator中,作为参数传递的value来自于与property(selectedDate中的)合并的source(DateField中的)。所以我们用value.time保存returnDate在1970年之后的秒数,用departure.selectedDate.time保存departureDate的秒数。这样的设计可能起初又把你搞懵了,但应该还算吃得消。
在doValidation()中,通常会首先调用基类的doValidation()方法,它返回一个数组。如果数组为空,成功完成基类的有效性检查。在这种情况下,基类调用可能就是多余的了,因为它仅保证对应栏不空,况且我认为在DateField上总是会有一些数据的,因而也总能调用成功。总之,这些代码给出了在自定义Validator下一个普通窗体的创建过程。
如果返程日期不是被设置为至少在启程日期的一天之后,就会出现问题。因此我们向results数组增加了一个ValidationResult。它可以在检查失败的时候给出提示,但更重要的是它可以返回一个消息来告诉用户问题出在哪里。当用户移动鼠标经过输入框时就会显示该消息(如是无效就会出现一个红框)。
这就是Validator的基本思想。当再遇到特殊窗体约束时,创建一个自定义窗体就不是难事了。(未完待续)
(原文链接网址:http://www.artima.com/weblogs/viewpost.jsp?thread=213902)