65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.93/5 (13投票s)

2003年9月30日

6分钟阅读

viewsIcon

128635

downloadIcon

1410

用于处理美国/英国日期的复合自定义控件和验证器

Sample Image - datecontrol.jpg

引言

在为一家跨越多个大陆的公司构建基于 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 属性中输入的内容。这是因为可能发生多个不同的错误条件

  1. 不完整状态,例如 4/-1/2003,其中一个或多个子控件未被选中。
  2. 无效日期,例如 2003 年 9 月 31 日(九月只有 30 天)。
  3. DateControl 是必需的,并且没有选择子控件。

一个错误消息无法提供足够的信息,使用户能够区分这些问题并轻松解决错误状况。

自定义编写的验证器必须重写 EvaluateIsValid。此外,为了支持客户端脚本的使用,验证器必须重写 AddAttributesToRenderOnPreRender。验证器在发送到客户端的 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 文件中包含 DateControlDateControl 验证器类的 VB.Net 版本。

© . All rights reserved.