Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An ASP.NET Composite Control for US and UK dates

0.00/5 (No votes)
29 Sep 2003 1  
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

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

Public Events

EventChange Occurs when any of the listboxes change.

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

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

Then in the 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.

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.

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

PropertyRequired A 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.

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.

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