7.3 改善验证控件的用户体验
习惯于线性思维的程序员往往着眼于功能,但有时将目光转移到一些非功能性的角度,也许能获得事半功倍的效果。ASP.NET AJAX ControlToolkit中的ValidatorCalloutExtender正是一个绝好的例子。

图7-3 ValidatorCalloutExtender控件
ValidatorCalloutExtender控件并没有尝试去增强验证控件的功能,而是改善它的用户界面,从而获得令人惊讶的效果。
不过ValidatorCalloutExtender控件依赖于ASP.NET AJAX框架,而且它是一个控件扩展器,它对页面验证控件进行扩展,所以我们需要往页面中添加一个验证控件,再添加一个针对这个验证控件的ValidatorCalloutExtender控件才能得到这样的效果,因此使用起来并不方便。
接下来我们将实现一个必填项验证控件——CalloutReqiredFieldValidator,与RequiredFieldValidator不同的是,它显示错误消息的方式与应用了ValidatorCalloutExtender控件时相似。
7.3.1 ValidatorCallout客户端实现
和之前一样,我们先实现客户端行为。CalloutRequiredFieldValidator控件不需要重新设计验证算法,只是在原有验证过程的基础上增加显示丰富界面的功能。
var ValidatorCallout = function(validator){
if(typeof(validator) == 'string')
this._validator = document.getElementById(validator);
else
this._validator = validator;
}
ValidatorCallout.show = function(validator){
validator.callout._divContainer.style.display = 'block';
}
ValidatorCallout.hide = function(validator){
validator.callout._divContainer.style.display = 'none';
}
ValidatorCallout.zIndex = 999;
ValidatorCallout.prototype = {
smallAlertPic :
'<%= WebResource("CustomValidators.alertsmall.gif")%>',
largeAlertPic :
'<%= WebResource("CustomValidators.alertlarge.gif")%>',
closePic : '<%= WebResource("CustomValidators.close.gif")%>',
initiate:function(){
var divContainer = document.createElement("DIV");
this._divContainer = divContainer;
divContainer.style.position = 'absolute';
divContainer.style.zIndex = ValidatorCallout.zIndex;
ValidatorCallout.zIndex++;
// UI被点击后显示在最前端,解决多个UI重叠的问题
divContainer.onclick = function(){
this.style.zIndex = ValidatorCallout.zIndex;
ValidatorCallout.zIndex ++;
};
divContainer.style.cursor = 'default';
var divCorner = document.createElement("DIV");
divContainer.appendChild(divCorner);
divCorner.style.width = 15;
divCorner.style.height = 15;
divCorner.style.position = 'relative';
divCorner.style.top = 10;
divCorner.style.left = 1;
divCorner.style.borderTop = 'solid 1px black';
divCorner.style.styleFloat = 'left';
// 用多个元素拼出尖角效果
for(var i = 14; i > 0; i--){
var div = document.createElement("DIV");
div.style.width = i;
div.style.height = 1;
div.style.styleFloat = 'right';
div.style.clear = 'right';
div.style.borderLeft = "solid 1px black";
div.style.backgroundColor = 'lemonchiffon';
divCorner.appendChild(div);
}
var divPanel = document.createElement('DIV');
divContainer.appendChild(divPanel);
divPanel.style.styleFloat = 'left';
divPanel.style.border = 'solid 1px black';
divPanel.style.padding = 12;
divPanel.style.backgroundColor = 'lemonchiffon';
var img = document.createElement("IMG");
divPanel.appendChild(img);
img.src = this.smallAlertPic;
img.style.styleFloat = 'left';
img.style.marginRight = 10;
img.src = this.largeAlertPic;
var h4 = document.createElement("H4");
divPanel.appendChild(h4);
h4.style.styleFloat = 'left';
h4.style.clear = 'right';
h4.innerText = 'Validation Message!';
divPanel.appendChild(document.createElement('BR'));
var span = document.createElement('SPAN');
divPanel.appendChild(span);
span.style.display = 'block';
span.style.styleFloat = 'left';
span.style.clear = 'right';
span.appendChild(document.createTextNode(
this._validator.errormessage));
var close = document.createElement('IMG');
divPanel.appendChild(close);
close.src = this.closePic;
close.style.position = 'absolute';
close.style.top = 2;
close.style.right = 2;
close.onclick = function(){divContainer.style.display = 'none';};
document.body.insertBefore(
divContainer,document.body.children[0]);
// 将UI定位在验证控件的旁边
var position = WebForm_GetElementPosition(this._validator);
position.x += this._validator.offsetWidth;
WebForm_SetElementX(divContainer,position.x);
WebForm_SetElementY(divContainer,position.y);
divContainer.style.display = 'none';
this._validator.callout = this;
//验证控件的验证方法上附加显示UI的功能
if(typeof(this._validator.evaluationfunction) == 'String'){
this._validator.evaluationfunction =
eval(this._validator.evaluationfunction);
}
var evaluationfunction = this._validator.evaluationfunction;
this._validator.evaluationfunction = function(val){
if(!evaluationfunction(val)){
ValidatorCallout.show(val);
return false;
}
else{
ValidatorCallout.hide(val);
return true;
}
};
}
};
同样的,我们将CalloutValidator实现为一个JavaScript组件,只需传给它一个验证控件的ID,并调用它的initiate(),即可实现不一样的界面。
CalloutValidator的原型(prototype)中定义了几个内嵌资源图片,它们将用于组成Callout UI,如图7-4所示。

图7-4 ValidatorCallout外观
特别需要注意的是ValidatorCallout左边的小尖角,正是由于这个小小的尖角让整个UI活泼了许多,不过这个尖角并不是一个图片,而是由十多个宽度不一的DIV组成的,这些DIV都停靠在父DIV的右边,每个DIV都有黑色的左边框,高1px。由于每个DIV宽度相差一个像素,所以它们的左边框就形成了一条斜边。此外,它们的父容器DIV向右偏移1px,这样就遮住了右边主DIV的黑色边框,使尖角与主DIV融为一体。
整个UI都是在initiate()方法中动态生成的,为了解决多个UI相互重叠使得处于下方的UI不能完整显示的问题,我们还编写了UI的oncLick事件处理函数,其效果如图7-5所示。
divContainer.style.zIndex = ValidatorCallout.zIndex;
ValidatorCallout.zIndex++;
// UI被点击后显示在最前端,解决多个UI重叠的问题
divContainer.onclick = function(){
this.style.zIndex = ValidatorCallout.zIndex;
ValidatorCallout.zIndex ++;
};

图7-5 将被点击的UI置于前端
7.3.2 ValidatorCallout服务端实现
由于客服端功能设计得十分内聚,所以服务端的实现就非常简单了。
[assembly:WebResource("CustomValidators.alertlarge.gif","image/gif")]
[assembly: WebResource("CustomValidators.alertsmall.gif", "image/gif")]
[assembly: WebResource("CustomValidators.close.gif", "image/gif")]
[assembly: WebResource("CustomValidators.ValidatorCallout.js", "application /x-javascript",PerformSubstitution=true)]
namespace CustomValidators
{
[ToolboxData("<{0}:CalloutRequiredFieldValidator runat=\"server\"
ErrorMessage=\"CalloutRequiredFieldValidator\">
</{0}:CalloutRequiredFieldValidator>")]
public class CalloutRequiredFieldValidator:RequiredFieldValidator
{
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (RenderUplevel)
{
Page.ClientScript.RegisterClientScriptResource(
typeof(CalloutRequiredFieldValidator),
"CustomValidators.ValidatorCallout.js");
Page.ClientScript.RegisterStartupScript(
typeof(CalloutRequiredFieldValidator),
this.ClientID + "Callout",
"new ValidatorCallout('"
+this.ClientID+"').initiate();\r\n", true);
}
}
}
}
CalloutRequiredFieldValidator继承自RequiredFieldValidator,仅重写了它的OnPreRender()方法。OnPreRender()方法中使用的RenderUplevel属性由BaseValidator基类提供,通过调用base.OnPreRender()方法,RenderUplevel可以指示访问浏览器是否支持客户端验证。
此外,我们在页面中注册了必需的客户端脚本文件,并生成初始化ValidatorCallout客户端组件的脚本。
7.3.3 ValidatorCalloutExtender
ASP.NET AJAX ControlTookit 中的Extender控件不修改现有控件而扩展现有控件的方式很有借鉴意义,它体现的正是OOP的“开放-关闭”原则。不过ControlToolkit中的ValiatorCalloutExtender需要为每个需要扩展的验证控件添加一个Extender,并不便于大量重构现有页面。而刚完成的ValidatorCallout组件非常易于与现有验证控件框架结合,所以可以实现一个扩展控件,添加一个就可扩展页面中的所有验证控件,如图7-6所示。

图7-6 ValidatorCalloutExtender扩展页面中的所有验证控件
接下来要实现的ValidatorCalloutExtender控件非常简单,它在页面中注册一段脚本,为Page_Validators数组中的每个验证控件初始化一个ValidatorCallout组件。
[Designer(typeof(ValidatorCalloutExtenderDesigner))]
public class ValidatorCalloutExtender:Control
{
protected override void OnPreRender(EventArgs e)
{
// 检测访问浏览器是否支持脚本功能
if (Page.Request.Browser.MSDomVersion.Major > 0
&& Page.Request.Browser.EcmaScriptVersion.Major > 0)
{
Page.ClientScript.RegisterClientScriptResource(
typeof(ValidatorCalloutExtender),
"CustomValidators.ValidatorCallout.js");
if (!Page.ClientScript.IsStartupScriptRegistered(
"ValidatorCalloutExtender"))
{
Page.ClientScript.RegisterStartupScript(
typeof(ValidatorCalloutExtender),
"ValidatorCalloutExtender",
"for(var i = 0 ; i < Page_Validators.length; i++)
{new ValidatorCallout(Page_Validators[i])
.initiate();}\r\n", true);
}
}
}
public override void RenderControl(HtmlTextWriter writer)
{
}
}
public class ValidatorCalloutExtenderDesigner : ControlDesigner
{
public override string GetDesignTimeHtml()
{
return CreatePlaceHolderDesignTimeHtml(
"ValidatorCalloutExtender");
}
}
与数据源控件等控件一样,ValidatorCalloutExtender控件并不生成和用户交互的客户端控件,所以从严格意义上讲,它其实不是控件,而是一个组件,所以我们为它提供了ValidatorCalloutExtenderDesigne设计器,返回只有占位块的设计时HTML代码,如图7-7所示。
图7-7 ValidatorCalloutExtender设计时效果







