Click here to Skip to main content
15,069,838 members
Articles / Web Development / ASP.NET
Article
Posted 29 Sep 2003

Stats

123.7K views
1.4K downloads
41 bookmarked

An ASP.NET Composite Control for US and UK dates

Rate me:
Please Sign up or sign in to vote.
2.93/5 (13 votes)
29 Sep 20036 min read
A composite custom control and validator for handling US/UK dates

Sample Image - datecontrol.jpg

Introduction

While building a web-based, enterprise wide system for a company spanning multiple continents, we frequently ran into localization-type issues that spanned the English language, let alone non-English languages. The most significant data problem we encountered within English-speaking countries was the entry of dates. The application initially accepted dates in textboxes, causing much confusion when the date was saved in the database. In the United States (US), the date 4/5/2003 is April 5th, 2003. In the United Kingdom (UK), this date is May 4th, 2003. To make matters worse, some UK users had US localization set on their computers, and some had UK localization set on their computers.

There was no reliable method for determining the format of date the user really was using (mm/dd/yyy or dd/mm/yyyy) for data entry. The application displays dates in the format dd-Mon-yyyy which is unambiguous to the users. The users found typing that format clunky, and labels near the fields showing the proper format and addtional training of the users was of little or no help to the problem. Thus was born an idea for a date control based solely on list boxes. I chose to create a Composite Control over a User Control so I could deploy the control in more than one application simultaneously.

Background

Composite controls must implement the INamingContainer interface, so that multiple instances of the control on the page have unique names. Also, a composite control must override the CreateChildControls method to instantiate the child controls. I chose to implement IPostBackDataHandler to manage the state of the control over postbacks instead of hooking to the built-in events of the child controls. This was mainly due to the fact that my control produced one value, yet used three controls. If the user changed all three, each control raised a change event and caused needless execution of the code for maintaining the single value of the control. The IPostBackDataHandler interface allows the control to check the state of all three and then raise a single change event.

The DateControl Class

Public Properties

PropertySelectCurrentDateA boolean that causes the control's value to be initially set to the current date.
PropertyValueA string representing the value of the three controls in the format "mm/dd/yyyy". A blank value is the empty string.
PropertyYearsBackAn integer for setting the number of years into the past that the control renders in the year listbox.

Public Events

EventChangeOccurs when any of the listboxes change.

To instantiate the control on a page, first you need to register it on the page:

ASP.NET
<%@ Register TagPrefix="custom" namespace="CustomControls" 
    assembly="YourDLLName"%>

Then in the HTML:

HTML
<custom:datecontrol id="TheDate" runat="server" selectcurrentdate="true">
</custom:datecontrol>

How the Date Control Works

The control consisits of three child controls but exposes only one value which is the combination of all three children. The control is pretty straightforward in the CreateChildControls method that is overridden. Each listbox takes the appropriate array and binds it. The days and months are static, the years array is calculated. When the control's Value property is set, the SetSelected method splits the value and stores the value of each control separately for handling the listboxes on postback, and the single Value property is saved in ViewState. The control is not concerned with the validity of the data, only the format.

C#
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;
    }
}

The control implements IPostBackDataHandler to manage resetting the value of the control when it changes on PostBack, and also to reset the value if changes occurred. For each control it checks the posted selectedIndex/value and compares it to the selectedIndex/value restored from ViewState. If a change occurred then Value of the control gets updated. Any listbox not explicitly set gets a value of -1, and any listbox whose value was removed (the first blank item in the listbox was selected) is also set to -1. This may seem a little odd, but it will help tremendously for validation of the control. When LoadPostData returns true, RaisePostDataChangedEvent is called by ASP.Net so the control's Change event fires.

C#
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;
}

Next Step: Validation

As you may have noticed by now, the control will return values like "4/-1/1975" or "-1/-1/2003". It will never return -1/-1/-1, as that is recognized and being empty and returns an empty string instead. Database date fields are notoriously picky, and will not accept a partial date like 4/2003 even though we use them in common speech and writing. The individual values set at -1 allow 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

Public Properties

PropertyRequiredA boolean that causes the validator to check that all three fields are not blank when true. When false the validator will allow all three fields to be blank simultaneously.

The custom-written validator was much more of a challenge than the control. It works seamlessly with the Microsoft validators and behaves as they do with regard to UpLevel and DownLevel browsers and the EnableClientScriptproperty. The JavaScript contains one main function, DateControlIsValid, which checks the value of Required and acts accordingly. The validator emits it's own error messages (both client and server), it ignores what the user inputs into the ErrorMessage property of the validator. This is due to the multiple distinct error conditions that may occur:

  1. An incomplete state, such as 4/-1/2003 where one or more of the three child controls has not been selected.
  2. An invalid date, such as September 31, 2003 (September has only 30 days).
  3. The DateControl is required and no child controls were selected.

One error message cannot provide enough information for the user to differentiate between these problems and resolve the error condition easily.

A custom-written validator must override EvaluateIsValid. Additionally, to support the use of client script the validator must override AddAttributesToRender and OnPreRender. A validator is rendered as a <div> on the HTML that is emitted to the client. The AddAttributesToRender places the necessary attributes into the <div> so that the Microsoft-written client-side validation routines find the required data and can manipulate the <div> properly when client validation occurs. The Micrsoft-written Javascript can be found in WebUIValidation.js in the aspnet_client folder heirarchy.

C#
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");
    }
}

The OnPreRender routine hooks the JavaScript file for the custom-written validator into the HTML and into the Microsoft client-side validation routines. For UpLevel browsers, the Microsoft client validation functions are hooked to the HTML control's client events dynamically when the page loads. Since the DateControl is really three separate HTML controls, the Microsoft routines cannot identify a single HTML control to bind to the validator's client validation function, so they disable the validator. In fact, if the validator was bound to only one of the three controls, the DateControl would not validate properly. The OnPreRender routine emits a custom start-up script that loops through the DateValidator controls (<div>) on the HTML page and enables each of them to counteract the behavior of the Microsoft-written JavaScript. Microsoft does supply a routine that accomplishes the same thing, but it causes the validation to occur with the onpageload event of the HTML<body>). Required DateControls that start out blank immediately report an error condition even though the user did nothing.

C#
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());
    }

}

Future Improvements

The DateControl certainly has further to go, but these changes were not an immediate need, so they have yet to be implemented:

  • Support for Data binding
  • Inherit from WebControl
  • Designer Support

A VB.Net version of the DateControl and DateControl validator classes is included in the .zip files.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

David Truxall
Team Leader Hewlett-Packard
United States United States
Dave has been programming for a living since 1995, working previously with Microsoft technologies modeling internal business processes, and now working as a mobile architect and team lead. He is currently employed by DXC.technology in the metropolitan Detroit area.

Comments and Discussions

 
GeneralNomenclature Pin
stixoffire28-Apr-09 0:26
Memberstixoffire28-Apr-09 0:26 
Generaleasy question, I hope Pin
Les Stockton3-May-07 7:54
MemberLes Stockton3-May-07 7:54 
GeneralRe: easy question, I hope Pin
Les Stockton3-May-07 8:11
MemberLes Stockton3-May-07 8:11 
Generalpls send full project Pin
pbk_baskaran15-Nov-06 21:39
Memberpbk_baskaran15-Nov-06 21:39 
GeneralUK vs US dates Pin
Jon Hulatt2-Feb-06 23:57
MemberJon Hulatt2-Feb-06 23:57 
QuestionHow do I get this to work Pin
CJ Man31-Jan-06 6:24
MemberCJ Man31-Jan-06 6:24 
AnswerRe: How do I get this to work Pin
David Truxall31-Jan-06 13:52
MemberDavid Truxall31-Jan-06 13:52 
GeneralRe: How do I get this to work Pin
eng22009-Jun-06 7:13
Membereng22009-Jun-06 7:13 
GeneralGood example Pin
AgeKay3-Apr-05 21:53
MemberAgeKay3-Apr-05 21:53 
GeneralPositioning the DateControl Pin
MoFrey10-Jun-04 10:40
MemberMoFrey10-Jun-04 10:40 
GeneralRe: Positioning the DateControl Pin
David Truxall11-Jun-04 11:21
MemberDavid Truxall11-Jun-04 11:21 
QuestionPage_Load, why implement this? Pin
3sL11-May-04 21:16
Member3sL11-May-04 21:16 
AnswerRe: Page_Load, why implement this? Pin
David Truxall18-May-04 14:29
MemberDavid Truxall18-May-04 14:29 
AnswerRe: Page_Load, why implement this? Pin
struck14-Aug-04 14:58
Memberstruck14-Aug-04 14:58 
Generalcompile date_control returns a gazillion errors Pin
webber12345618-Mar-04 10:11
Memberwebber12345618-Mar-04 10:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.