用于美国和英国日期的 ASP.NET 复合控件






2.93/5 (13投票s)
2003年9月30日
6分钟阅读

128635

1410
用于处理美国/英国日期的复合自定义控件和验证器
引言
在为一家跨越多个大陆的公司构建基于 Web 的企业级系统时,我们经常遇到本地化方面的问题,这些问题不仅涉及非英语语言,甚至在英语国家内部也存在。在英语国家中,我们遇到的最重要的数据问题是日期的输入。应用程序最初在文本框中接受日期,导致在数据库中保存日期时产生很多混淆。在美国 (US),日期 4/5/2003 是 2003 年 4 月 5 日。在英国 (UK),这个日期是 2003 年 5 月 4 日。更糟糕的是,一些英国用户在其计算机上设置了美国本地化,而另一些用户则设置了英国本地化。
没有可靠的方法可以确定用户实际使用的日期格式(月/日/年或日/月/年)进行数据录入。该应用程序以 dd-Mon-yyyy 的格式显示日期,这对用户来说是明确无误的。用户觉得输入这种格式很笨拙,旁边的标签显示正确格式以及对用户的额外培训几乎没有帮助。因此,便诞生了一个完全基于列表框的日期控件的想法。我选择创建一个复合控件而不是用户控件,以便能够同时将控件部署到多个应用程序中。
背景
复合控件必须实现 INamingContainer
接口,以便页面上的多个控件实例具有唯一的名称。此外,复合控件必须重写 CreateChildControls
方法来实例化子控件。我选择实现 IPostBackDataHandler
来管理控件在回发时的状态,而不是挂钩到子控件的内置事件。这主要是因为我的控件产生一个值,但使用了三个控件。如果用户更改了所有三个,每个控件都会引发一个更改事件,并导致维护控件单个值的代码不必要地执行。IPostBackDataHandler
接口允许控件检查所有三个控件的状态,然后引发一个单一的更改事件。
DateControl 类
公共属性
![]() SelectCurrentDate |
一个布尔值,用于将控件的值初始设置为当前日期。 |
![]() Value |
一个字符串,表示三个控件的值,格式为“mm/dd/yyyy”。空值为空字符串。 |
![]() YearsBack |
一个整数,用于设置控件在年份列表框中显示的过去年份的数量。 |
公共事件
![]() Change |
当任何列表框发生更改时发生。 |
要在页面上实例化该控件,首先需要将其注册到页面上
<%@ Register TagPrefix="custom" namespace="CustomControls"
assembly="YourDLLName"%>
然后是 HTML
<custom:datecontrol id="TheDate" runat="server" selectcurrentdate="true">
</custom:datecontrol>
Date Control 的工作原理
该控件由三个子控件组成,但只公开一个值,该值是所有三个子控件的组合。该控件在重写的 CreateChildControls
方法中相当直接。每个列表框都接受相应的数组并绑定它。日和月是静态的,年数组是计算出来的。当设置控件的 Value
属性时,SetSelected
方法会拆分该值,并将每个控件的值分开存储,以便在回发时处理列表框,并且单个 Value
属性保存在 ViewState 中。控件不关心数据的有效性,只关心格式。
public string Value
{
get
{
string s = (string)ViewState["Value"];
if(s == null)
return String.Empty;
else
return s;
}
set
{
this.SetSelected(value);
ViewState["Value"] = value;
}
}
private void SetSelected(string When)
{
string[] ResultList;
Regex DateSplitter = new Regex("^\\d{1,2}\\/\\d{1,2}\\/\\d{4}$");
if(DateSplitter.IsMatch(When)) //The date has a good format
{
char divider = '/';
ResultList = When.Split(divider);
SelectedMonth = Int32.Parse(ResultList[0]);
SelectedDay = Int32.Parse(ResultList[1]);
SelectedYear = Int32.Parse(ResultList[2]);
}
else //When must be empty or not recognizable,
//so set the listboxes to the empty state
{
SelectedDay = -1;
SelectedMonth = -1;
SelectedYear = -1;
}
}
该控件实现了 IPostBackDataHandler
来管理回发时控件值的重置,以及在发生更改时重置值。对于每个控件,它会检查已发布的 selectedIndex/value,并将其与从 ViewState 恢复的 selectedIndex/value 进行比较。如果发生更改,则控件的 Value
将被更新。任何未显式设置的列表框都将获得值 -1,并且任何值被移除的列表框(选择了列表框中的第一个空项)也将被设置为 -1。这可能看起来有点奇怪,但它对控件的验证非常有帮助。当 LoadPostData
返回 true 时,ASP.Net 会调用 RaisePostDataChangedEvent
,以便控件的 Change
事件触发。
bool IPostBackDataHandler.LoadPostData(string postDataKey,
NameValueCollection postCollection)
{
int Day = 0;
int Month = 0;
int Year = 0;
bool Changed = false;
string MonthName;
if(postCollection[this.UniqueID.ToString() + ":Day"] != "")
{
Day = Int32.Parse(postCollection[this.UniqueID.ToString() +
":Day"]);
if (Day != this.SelectedDay)
{
this.SelectedDay = Day;
Changed = true;
}
}
else
{
if(this.SelectedDay != -1)
{
this.SelectedDay = -1;
Changed = true;
}
}
MonthName = postCollection[this.UniqueID.ToString() + ":Month"];
if (MonthName != "")
{
Month = Array.IndexOf(this.MonthArrayLong, MonthName);
if(Month != this.SelectedMonth)
{
this.SelectedMonth = Month;
Changed = true;
}
}
else
{
if(this.SelectedMonth != -1)
{
this.SelectedMonth = -1;
Changed = true;
}
}
if(postCollection[this.UniqueID.ToString() + ":Year"] != "")
{
Year = Int32.Parse(postCollection[this.UniqueID.ToString()
+ ":Year"]);
if (Year != this.SelectedYear)
{
this.SelectedYear = Year;
Changed = true;
}
}
else
{
if(this.SelectedYear != -1)
{
this.SelectedYear = -1;
Changed = true;
}
}
if(this.SelectedDay == -1 && this.SelectedMonth == -1 &&
this.SelectedYear == -1)
this.Value = "";
else
{
string NewDate = this.SelectedDay.ToString() + "/";
NewDate += this.SelectedMonth.ToString() + "/";
NewDate += this.SelectedYear.ToString();
this.Value = NewDate;
}
return Changed;
}
下一步:验证
正如您可能已经注意到的,该控件将返回诸如“4/-1/1975”或“-1/-1/2003”之类的值。它永远不会返回 -1/-1/-1,因为那被识别为已空,并返回一个空字符串。数据库日期字段非常挑剔,并且不会接受像 4/2003 这样的部分日期,即使我们在日常说话和写作中使用它们。individual values set at -1 allows for effective validation of the control and contain values unacceptable to a database date field so that bad data cannot inadvertently enter the database. Instead of rolling the validation into the control, I created a validator to go along with the date control. Validation could occur via a custom validator, but I liked my own better for deploying the client script. The DateControlValidator
inherits from BaseValidator
and only implements one custom property.
DateControlValidator
公共属性
![]() Required |
一个 boolean 值,当为 true 时,验证器会检查所有三个字段是否均不为空。当为 false 时,验证器允许所有三个字段同时为空。 |
自定义编写的验证器比控件更具挑战性。它与 Microsoft 的验证器无缝协作,并且在 UpLevel 和 DownLevel 浏览器以及 EnableClientScript
属性方面表现得与它们一样。JavaScript 包含一个主要函数 DateControlIsValid
,该函数检查 Required
的值并相应地执行操作。验证器会发出自己的错误消息(客户端和服务器),它会忽略用户在验证器的 ErrorMessage
属性中输入的内容。这是因为可能发生多个不同的错误条件
- 不完整状态,例如 4/-1/2003,其中一个或多个子控件未被选中。
- 无效日期,例如 2003 年 9 月 31 日(九月只有 30 天)。
- DateControl 是必需的,并且没有选择子控件。
一个错误消息无法提供足够的信息,使用户能够区分这些问题并轻松解决错误状况。
自定义编写的验证器必须重写 EvaluateIsValid
。此外,为了支持客户端脚本的使用,验证器必须重写 AddAttributesToRender
和 OnPreRender
。验证器在发送到客户端的 HTML 中呈现为一个 <div>
。AddAttributesToRender
将必要的属性放入 <div>
中,以便 Microsoft 编写的客户端验证例程找到所需数据,并在客户端验证发生时正确地操作 <div>
。Microsoft 编写的 Javascript 可以在 aspnet_client 文件夹层次结构中的 WebUIValidation.js 文件中找到。
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
if(this.DetermineRenderUplevel() && this.EnableClientScript)
{
base.AddAttributesToRender(writer);
writer.AddAttribute("controltovalidate", this.ControlToValidate);
writer.AddAttribute("evaluationfunction", "DateControlIsValid");
writer.AddAttribute("display", this.Display.ToString());
writer.AddAttribute("style", "display:none");
writer.AddAttribute("errormessage", this.ErrorMessage);
if(ControlRequired)
writer.AddAttribute("required", "true");
else
writer.AddAttribute("required", "false");
}
}
OnPreRender
例程会将自定义编写的验证器的 JavaScript 文件挂钩到 HTML 和 Microsoft 客户端验证例程中。对于 UpLevel 浏览器,Microsoft 客户端验证函数会在页面加载时动态地挂钩到 HTML 控件的客户端事件。由于 DateControl
实际上是三个独立的 HTML 控件,Microsoft 例程无法识别单个 HTML 控件以绑定到验证器的客户端验证函数,因此它们会禁用该验证器。事实上,如果验证器只绑定到三个控件中的一个,DateControl 将无法正确验证。OnPreRender
例程会发出自定义的启动脚本,该脚本会循环遍历 HTML 页面上的 DateValidator 控件(<div>
),并启用每一个,以抵消 Microsoft 编写的 JavaScript 的行为。Microsoft 提供了一个实现相同功能的例程,但它会导致在 HTML <body>
的 onpageload
事件中发生验证。Required DateControls 初始为空时,即使用户什么都没做,也会立即报告错误情况。
protected override void OnPreRender(EventArgs e)
{
if(this.DetermineRenderUplevel() && this.EnableClientScript)
{
string EOL = Environment.NewLine;
StringBuilder script = new StringBuilder();
//Make sure the MS standard javascript file is included
this.RegisterValidatorCommonScript();
//Include our own file for the control
script.Append("<script language='javascript' " +
"src='../include/date_control.js'></script>" + EOL);
Page.RegisterClientScriptBlock("DateValidate", script.ToString());
//Register this validator in the array of validators
//on the page, and the array of
//date controls on the page
string element = "document.all[\"" + ClientID + "\"]";
Page.RegisterArrayDeclaration("Date_Controls", element);
Page.RegisterArrayDeclaration("Page_Validators", element);
//Because the date control is a composite control,
//there is no single HTML control to
//bind the evaluation function to. Therefore,
//the MS javascript disables this validator.
//The client script below re-enables the validator when
//the page is submitted.
script.Remove(0, script.Length - 1);
script.Append("<script language='javascript'>");
//This is the "Microsoft Preferred" method for enabling
//a control, but it causes the
//control to validate, causing an immediate
//error condition when the field is required.
// script.Append(sVbCrLf)
// script.Append("ValidatorEnable(" & ClientID & ", true);")
// Instead I am setting the enabled property directly
// of all registered date controls.
// the EOLs are for human readablity only
script.Append(EOL);
script.Append("var x;");
script.Append(EOL);
script.Append("for (x = 0; x < Date_Controls.length; x++)");
script.Append(EOL);
script.Append("Date_Controls[x].enabled = true;");
script.Append(EOL);
script.Append("</script>");
script.Append(EOL);
Page.RegisterStartupScript("SetEnabled", script.ToString());
}
}
未来改进
DateControl
当然还有进一步的改进空间,但这些更改并非紧急需求,因此尚未实现
- 支持数据绑定
- 继承自 WebControl
- 设计器支持
zip 文件中包含 DateControl
和 DateControl
验证器类的 VB.Net 版本。