设计一个 validatable control(可校验控件)、兼论“三级联动”
春 鱼 2004-11-21
摘要
本文说明了使 asp:Validator 系列 control 可检查我们的web user control的实现方法。validatable指的是可以被<asp:Validator .../>系列control进行有效性检验。
重点难点: 为类引入 ValidationPropertyAttribute.
模拟效果图
前言
在作者最近的一个项目中,有一项要求用户可以从省、市、区县构成的数据中进行选择。最终可选择到区县。由于我国行政区域数目繁多,合理的做法是使用“三级联动”的选择器()来方便用户操作。我最初的想法是以简单,容易实现为准则,而又不希望这个“三级联动”(某些情况下相当复杂)的逻辑与其他业务逻辑混合在一起。(对于某些对逻辑混乱有厌恶倾向的开发人员来说,这将使他情绪低落以致意志消沉),应该避免这一状况的出现。于是我想到把这三个 drop down list 用一个ASCX(ASP.NET web user control)包起来,这样做是不是一定行呢?我没有仔细思考,就开始了我的尝试。等到把成品拿出来,调试成功,放到项目里可以用时,我发现我做了以下几项工作:
1. 集中了访问后端数据源的逻辑
2. 实现了省、市、区县间由上之下的制约关系
3. 容器必须可以访问当前值,为此我必须为 control实现SelectedValue public property,属性必须可以由get, set 访问器过程
4. 必须实现post back data handler接口
5. 实现validatable
<%@ Control Language="c#" AutoEventWireup="false" Codebehind="RegionList.ascx.cs" Inherits="TopNameSpace.Controls.RegionList" TargetSchema="http://schemas.microsoft.com/intellisense/ie5"%>
<asp:DropDownList ID=ProvinceList Runat=server Width=100 AutoPostBack=True />
<asp:DropDownList ID=CityList Runat=server Width=100 AutoPostBack=True />
<asp:DropDownList ID=ZoneList Runat=server Width=100 />
HTML 部分是极简单的。 仅罗列了三个服务器端的 <asp:DropDownList /> 标记:,在客户端则显示为三个select, 分别可以选择省、市、区县。
相信大家都开发过类似的应用,这里仅简述基本思路
001 初始化过程
code behind 部分, 首先必须使control可以完成初始化过程:在控件第一次载入页时,用户希望每个select都是空的(没有选择任何有效数据时的初始状态),但是第一个select应该缺省地载入全部省级数据。对于web user control来说,我们有一个缺省的Page_Load过程可以使用。Page_Load定义在private void InitializeComponent过程中定义的,(private void InitializeComponent过程则定义在override protected void OnInit过程中),但Page_Load过程发生得太早了。我们重新截获control的另外一个发生得比较晚的事件:PreRender。 在InitializeComponent过程中输入 this.PreRender += 后连续按两次[Tab]键,就会自动创建一个_PreRender(object sender, EventArgs e)过程, 在这一过程,我们给第一个<asp:DropDownList />载入数据。在这一过程之前,我们假设用户可以取到以一定数据格式存储的行政区域数据。我们使用可绑定 数据源保存数据,并以一定逻辑绑定到<asp:DropDownList />中。
注意:这一数据绑定过程仅发生在control第一次载入时,在以后的post back过程中,<asp:DropDownList/>可以自行保持自己的数据(使用 ViewState机制)。
002 联动逻辑
分析之后正常的逻辑是,当用户选择一个省级行政区时,应该载入当前省节点下的市级区域数据。而当选择市级行政区域时,应载入相应区县级行政区域数据。这两个过程分别需要截获第一二级<asp:DropDownList />的SelectedIndexChanged evnet.
003 SelectedValue public property
当web user control做为一个整体出现在容器中时,容器应该可以访问其在服务器端运行期的值。不然我们就失去了应用的基础。基于高内聚低耦合的原则,一般应通过使用public property来实现。分别定义其set,get访问过程:
1. get过程:简单地return第三级(区县)<asp:DropDownList />的SelectedValue值
2. set过程:是不是同样简单呢?
让我们来考虑以下一个基本事实:
set访问器过程,也就是control必须载入时设置有指定值,这个时候我们除了实施get过程的反过程(设置第三级)之外,还必须给第三级<asp:DropDownList />填充数据,而且设置key等于当前SelectedValue public property的项选中。而用户所见,市级数据也需要设置大当前区县的上级区域,一直上溯到省级区域。这一过程的实现是必须的。
005 post back data handling 实现
这一特性是设计处理值的后端control所必须的,因为control必须可以在post back之间保持自己的服务器端值,这就需要我们的control必须实现IPostBackDataHandler interface. 这样的逻辑我曾经在以前的讨论中谈到过。具体实现方法则需要一个与control 的服务器端ClientID相同的前端<input .../> element. 这一点与下面讨论的validatable实现相同--
004 validatable 实现
这一开发需求源于本文的标题:“validatable control”-可以被<asp:Validator .../>检验的control.
在项目进行中, 客户突然要求这个"联动选择器"不允许被置空, 以及我们必须在表单提交的时刻校验该control是否取得有效值,而这个校验过程是在客户端实现的。一般而言,<asp:Validator .../>系列control是仅能校验内置的一些标准服务器端 input control.
怎样使我们的web user control可以被校验呢?我们找到 MSDN有关章节:
(原文):
Use the ControlToValidate property to specify the input control to validate. This property must be set to the ID of an input control for all validation controls except for the CustomValidator control, which can be left blank. If you do not specify a valid input control, an exception will be thrown when the page is rendered. The ID must refer to a control within the same container as the validation control. It must be in the same page or user control, or it must be in the same template of a templated control.
The standard controls that can be validated are TextBox, ListBox, DropDownList, RadioButtonList, HtmlInputText, HtmlInputFile, HtmlSelect and HtmlTextArea.
Note For an input control to be validated, the System.Web.UI.ValidationPropertyAttribute must be applied to the control.
(设置到input control的ID以指定校验目标,被校验control必须是某种input control. 目前必须是几个特定类型:以上粗体部分。
注意:为将某control 配置为validatable control, 必须将ValidationPropertyAttribute应用到control.)
最后一句话很关键。也为我们指明了方向。
在类定义之前增加ValidationPropertyAttribute attribute, 并指明需校验的特性为:SelectedValue。
public class RegionList : System.Web.UI.UserControl
{
...
这样,我们就可以以通常的方式为我们的control附加一个validator:
不设置以上attribute,解析ASPX时会出错。
另外,我们知道<asp:Validator .../>系列校验控件是在客户端工作的。我们分析以下页前端生成的目标HTML,我们分析一个校验不同<asp:TextBox .../>的validator实例:
以上代码取字某页前端目标HTML,我们发现每个<asp:Validator .../>都被构造成了一个前端的普通element,其behavior由ASP.NET的标准脚本机制完成。我们不需要做细致的分析,但我们可以看到controltovalidate被原封不动地写到了前端attribute, value则为一个页内唯一的id。我们比较以下就可以看出来,从后端到前端ControlToValidate发生了变化。而这样变化是基于ASP.NET的固有机制。通过写到前端的attribute是一定可以找到解析到前端的 input element的。然后通过一定机制读取前端值并进行检查处理。
这样我们就知道了我们应该完成的目标:
1. control后端值必须可以一定方式反映到前端
2. 必须存在一个前端id可访问到的input element
3. 值在post到后端之前必须可被前端脚本改变
以下是实现方法:
1. 在control PreRender时为control写入一个特制的<input type=hidden .../>element:
control.Attributes.Add("id", this.ClientID);
control.Attributes.Add("type", "hidden");
control.Attributes.Add("value", this.ViewState["HiddenValue"]==null ? this.SelectedValue : (string)this.ViewState["HiddenValue"]);
this.Controls.Add(control);
关于以上源程序,我们看到我们给<input type=hidden .../>写入了id attribute, 取值为control的ClientID特性,而vaue则使用了ViewState机制在post back间保持该值。这样后端值就写入到了前端,并且我们validator可以访问到这个<input type=hidden .../> element。
2. 随前端用户操作改变,则需要写到前端一段脚本,使得<input type=hidden .../>可以随第三级element selectedIndexChange 前端 event变化。
builder.AppendFormat("<script>");
builder.AppendFormat("jesse_list=document.all['{0}'];", this.MainList.ClientID);
builder.AppendFormat("jesse_hidden=document.all['{0}'];", this.ClientID);
builder.Append(@"
jesse_list.attachEvent('onchange', __set_typelist_value);
function __set_typelist_value(){
jesse_hidden.value=jesse_list.value;
}
</script>");
string script = builder.ToString();
if (!this.Page.IsStartupScriptRegistered("REGION_LIST_CLIENT")) this.Page.RegisterStartupScript("REGION_LIST_CLIENT", script);
构造脚本块并register到前端。
如此我们已经基本实现了一个比较完备的“联动”control. 如果遇到特定的逻辑,则应进行相应修改。将其应用到ASPX或者其他ASCX中,则可以以使用普通<asp:Input 系列control的方式使用validator.
本文仓促间完成,不知道是把问题点清楚,并且对大家有帮助。
以下源代码取材自作者实际的工程项目,仅供参考:
https://files.cnblogs.com/jesse/RegionList_Control_Sample_2004-11-21.zip
注意: 源代码可能需要修改才能正常编译和运行