Designing an airline passenger reservation system

27 June 2008

User Interface - Part 2 - Encapsulating UI Controls

Filed under: User Interface — Tags: , , , , — Kristofer @ 5:00

As a continuation to the previous entry on user interface I am now going to cover the topic of encapsulation. This almost goes without saying on any platform, but is worth repeating: whenever using a control that need or may need to be extended, improved or replaced at some point it is a good idea to encapsulate it so any such changes only need to be done in one place.

CalendarExtender -> DatePicker

In my previous blog entry covering my choice of UI technology I mentioned the ASP.NET AJAX Control Toolkit, a set of sample controls for asp.net that showcase some of the new client-side functionality that ships with .net 3.5. Some of these controls are useful right out of the box (download) but in a few cases it still makes sense to encapsulate them. One such example is the calendar extender. The calendar extender adds a calendar dropdown to textboxes, dropdowns or other web form controls through the new concept of extender controls; controls that add client side functionality to server-side controls by adding client side script and css to the page at rendering time. However, the calendar extender does not address keyboard input, validation etc so this is one area where I will add some functionality while encapsulating it. The encapsulation also makes it easy to replace the dropdown part with another calendar dropdown - client based or server based - if needed.

Control Definition (ascx)

To avoid confusion I am calling the encapsulated control DatePicker. The control definition ascx is short and straightforward:

First a textbox is needed for entering and displaying the date:

<asp:TextBox ID="txtDate" runat="server" MaxLength="12" Columns="12" AutoPostBack="false"
    AutoCompleteType="Disabled" onfocus="this.select();" />

The textbox is immediately followed by an image button displaying a calendar icon, for displaying the calendar extender’s calendar dropdown:

<asp:ImageButton runat="server"
        ID="btnCalendar" ImageUrl="../images/calendar.png" CausesValidation="false" />

Once the textbox and image button has been defined it is time to add the calendar extender itself, referencing the previous two controls as the target control and popup button. 

<act:CalendarExtender ID="calDate" runat="server" TargetControlID="txtDate" Format="d"
        PopupButtonID="btnCalendar" FirstDayOfWeek="Monday">

In order to allow for validation of any entered date, and to enforce date fields that are mandatory, throw in a couple of standard asp.net validation controls:

<asp:RequiredFieldValidator ID="rfvDate" ControlToValidate="txtDate" runat="server"
    ErrorMessage="Please enter a date." Display="None" Enabled="false" />
<asp:CustomValidator runat="server" ID="cvlDate" ErrorMessage="Invalid date format."
    ClientValidationFunction="ValidateDate" ControlToValidate="txtDate" EnableClientScript="true"
    Display="None" />

…immediately backed up by the Validation Callout Extender from the Ajax Control Toolkit:

<act:ValidatorCalloutExtender CssClass="vceValidatorCallout" ID="vceDate" runat="server"
    TargetControlID="cvlDate">
</act:ValidatorCalloutExtender>
<act:ValidatorCalloutExtender CssClass="vceValidatorCallout" ID="vceDate2" runat="server"
    TargetControlID="rfvDate">
</act:ValidatorCalloutExtender>

The validation callout extender is a control that extends the standard asp.net validation controls with a fly-out bubble showing the validation error message in a bubble next to the control where validation failed. This brings the user’s attention directly to the control where validation failed in a much better manner than traditional asp.net inline validation messages or validation summaries.

Server Side Code

The server side code for the control is basically just a few properties for handling enabled/disabled, mandatory/not mandatory, valid date format and for retrieving/setting the value of the control:

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.ComponentModel;
namespace HuagatiWebControls
{
    public partial class DatePicker : System.Web.UI.UserControl
    {
        private bool _mandatory = false;
        private string _dateFormat = "dd MMM yyyy";
        protected void Page_Load(object sender, EventArgs e)
        {
            //set date format for the calendar extender
            calDate.Format = _dateFormat;
            //set error message for the format validator
            cvlDate.ErrorMessage = "Invalid date, please enter a date in the format "
                                 + DateTime.Now.ToString(_dateFormat, System.Globalization.CultureInfo.InvariantCulture) + ".";
            cvlDate.Attributes.Add("cvl", txtDate.ClientID);
            //register client side script
            ScriptManager.RegisterClientScriptInclude(this, this.GetType(), "DatePickerJS", "scripts/datepicker.js");
        }
        //presentation date format
        [DefaultValue("dd MMM yyyy")]
        public string DateFormat
        {
            get
            {
                return _dateFormat;
            }
            set
            {
                _dateFormat = value;
            }
        }
        //mandatory or optional field?
        [DefaultValue(false)]
        public bool Mandatory
        {
            get { return _mandatory; }
            set
            {
                _mandatory = value;
                rfvDate.Enabled = _mandatory;
            }
        }
        //validation group that this control belongs to
        [DefaultValue(null)]
        public string ValidationGroup
        {
            get
            {
                return rfvDate.ValidationGroup;
            }
            set
            {
                rfvDate.ValidationGroup = value;
                cvlDate.ValidationGroup = value;
            }
        }
        //control tab index
        [DefaultValue(0)]
        public short TabIndex
        {
            get
            {
                return txtDate.TabIndex;
            }
            set
            {
                txtDate.TabIndex = value;
            }
        }
        //set or get the currently selected date
        public DateTime? SelectedDate
        {
            get
            {
                DateTime? selectedDate = null;
                DateTime dt = DateTime.MinValue;
                if (DateTime.TryParseExact(txtDate.Text, _dateFormat,
                        System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat,
                        System.Globalization.DateTimeStyles.None, out dt) == true)
                {
                    selectedDate = dt;
                }
                else if (DateTime.TryParse(txtDate.Text,
                        System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat,
                        System.Globalization.DateTimeStyles.None, out dt) == true)
                {
                    selectedDate = dt;
                }
                return selectedDate;
            }
            set
            {
                if (value == null)
                {
                    txtDate.Text = "";
                }
                else
                {
                    DateTime dt = (DateTime)value;
                    txtDate.Text = dt.ToString(_dateFormat,
                                               System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat);
                }
            }
        }
        //control enabled or disabled?
        [DefaultValue(true)]
        public bool Enabled
        {
            get
            {
                return txtDate.Enabled;
            }
            set
            {
                txtDate.Enabled = value;
                calDate.Enabled = value;
                cvlDate.Enabled = value;
                rfvDate.Enabled = value && _mandatory;
            }
        }
    }
}

Keyboard Entry / Client Side Validation

Next up, I want to add some client side code for validating and parsing dates entered directly in the textbox by users so dates can be both validated and formatted properly before a page postback occurs. The client side script for doing so is referenced by the custom validation control cvlDate previously added through the ClientValidationFunction attribute.

The script for doing client side validation looks as follows:

 

//default date presentation format
var _dateFormat = "dd MMM yyyy";
//default date validation formats
var _dateValidationPatterns = "dd|dd/MM|dd-MM|dd MMM|dd-MMM|dd MMM yy|dd MMM yyyy|dd-MMM-yyyy|dd/MM/yy|dd/MM/yyyy|ddMMyy|ddMMyyyy";
function ValidateDate(source, clientsideArguments)
{
    //get the value to validate
    var controlValue = clientsideArguments.Value;
    //attempt to parse using presentation format
    var newDate = Date.parseLocale(controlValue, _dateFormat);
    if (newDate == null)
    {
        //date could not be parsed using presentation format, attempt parsing using alternative formats
        var formats = _dateValidationPatterns.split('|');
        var idx = 0;
        for (idx in formats)
        {
            var format = formats[idx].trim();
            newDate = Date.parseInvariant(controlValue, format);
            if (newDate != null)
            {
                break;
            }
        }
   �
        //if a valid date has not yet been parsed, attempt to parse as a positive offset (+90d, +3m etc)
        var unit = null;
        var unitCount = null;
        if (newDate == null && controlValue.indexOf('+') == 0)
        {
            unit = controlValue.trim().substring(controlValue.length - 1).toLowerCase();
            unitCount = new Number(controlValue.trim().toLowerCase().replace('+', '').replace('d', '').replace('m', ''));
            if (unitCount != 'NaN')
            {
                newDate = new Date();
                if (unit.indexOf("m") > -1)
                {
                    newDate.setMonth(newDate.getMonth() + unitCount);
                }
                else
                {
                    newDate.setDate(newDate.getDate() + unitCount);
                }
            }
        }
        //if a valid date has not yet been parsed, attempt to parse as a negative offset (-90d, -3m etc)
        if (newDate == null && controlValue.indexOf('-') == 0)
        {
            unit = controlValue.trim().substring(controlValue.length - 1).toLowerCase();
            unitCount = new Number(controlValue.trim().toLowerCase().replace('-', '').replace('d', '').replace('m', ''));
            if (unitCount != 'NaN')
            {
                newDate = new Date();
                if (unit.indexOf("m") > -1)
                {
                    newDate.setMonth(newDate.getMonth() - unitCount);
                }
                else
                {
                    newDate.setDate(newDate.getDate() - unitCount);
                }
            }
        }
    }
    //check if we got a valid date in the parsing above...
    if (newDate != null)
    {
        //date is valid
        $get(source.getAttribute('cvl')).value = newDate.localeFormat(_dateFormat);
        clientsideArguments.IsValid = true;
    }
    else
    {
        //the date is not valid
        clientsideArguments.IsValid = false;
    }
}

Also fairly straightforward, it uses the method signature used by the CustomValidator control for client side validation. First it tries to parse the input using the display format, if that fails it attempts to parse it using a pre-defined list of common formats, and thereafter it tries to interpret the input as an offset (e.g. +90d, -90d, +2m etc). This allows for a great deal of flexibility in user input; one can enter only a date, date and month, or date month and year in various formats as illustrated below.

 

The functions used for parsing and formatting; Date.localeFormat, Date.parseLocale, Date.parseInvariant are all part of the Microsoft Ajax client libraries and include nearly full support for culture specific formatting and parsing by passing translations and other culture specific formatting and parsing information from the .net framework on the server to the client when the scripts are generated, as client side scripts. A handful of cultures are not fully supported but that is mentioned in the community content on msdn, and can be worked around separately as needed.

Configurable Settings / Formatting 

In this example I have left some values hard-coded such as display format, parsing list, error messages etc but when implemented these all come from application config settings or user settings and will be set at page_load in all master pages. For server-side properties it is fairly straight-forward to find all instances of the DatePicker control and set the DateFormat, and for the client side it only needs to be emitted as client script once per page load:

public partial class HuagatiResMasterPage : System.Web.UI.MasterPage
{
    protected void Page_Load(object sender, EventArgs e)
    {
        //generate client side script for the config settings that are needed by client script
        string dateFormatConfigSettings
            = "var _dateFormat = '" + ConfigSettings.DateFormat.Replace("'", "''") + "';\r\n"
            + "var _dateValidationPatterns = '" + ConfigSettings.DateFormatValidation.Replace("'", "''") + "';\r\n";
        ScriptManager.RegisterStartupScript(this, this.GetType(), "DateFormatConfigSettings", dateFormatConfigSettings, true);
    }
}

Final Result

The final control is easy to use in a page; the markup for adding a date picker is as simple as:

<dtp:DatePicker ID="dtpDatePicker" runat="server" />

…and any changes or improvements that need to be made to date fields system wide can now be done in one single place.

A sample of this control in action is available at http://blog.huagati.com/samples/datepickersample.aspx

26 June 2008

User Interface - Part 1 - Choice of User Interface technology

Filed under: User Interface — Tags: , — Kristofer @ 5:00

My primary choice of UI technology for enterprise applications is usually web based UIs unless there is a very compelling reason (or requirement) to use a traditional WinForms UI. The benefits of not having to deploy client-side software along, advances in user-friendliness and improvement in the development tools makes this an easy choice.

ASP.NET 3.5 and Ajax

The currently latest release of the Microsoft .net framework, .net 3.5, comes with some nice additions to asp.net. All of them have been available since before, in some cases for years, but finally Microsoft brought them together in a structured manner and added IDE support for them in Visual Studio. One of these ‘new’ technologies is AJAX; Asynchronous JAvascript and Xml. The usage of the term has expanded beyond its original meaning which referred to using client side script along to make service calls retrieving xml data that is then used to make client side updates to pages without the need to refresh the entire page. This has been possible (and used albeit not under the AJAX name) since MSIE 4.0 was released in the late 1990’s. With standardization, cross-browser support, IDE support, and client side code libraries it has become a lot easier to develop web based applications today, using Visual Studio 2008, than it was back in 1999.

There are several things that sail under the ‘Microsoft Ajax’ name:

  1. A set of (client side) javascript libraries that encapsulates common functionality, written to be cross-browser compatible (bridging the differences in object models between the major browsers). It includes extensions for the common javascript objects with new methods for parsing, formatting etc, functionality for calling web services from client side script, serialization/deserialization, UI/measurement/positioning functionality etc.
  2. Server side controls such as the UpdatePanel which allows partial page updates and ScriptManager that handles the inclusion/serving/updating of client-side scripts. Although the UpdatePanel control takes away the need to write client side code for partial page updates it comes with a price; roundtrips using the UpdatePanel is not exactly cheap and in many cases makes the pages slower than fullblown page refreshes so it need to be used with caution on complex pages. In many cases it comes in very handy though.
  3. A set of sample controls called the ASP.NET Ajax Control Toolkit. Although not part of VS 2008 / .net 3.5 it is a nice set of samples that can be downloaded from ajax.asp.net with full source code, some of them reusable out of the box and others just nice samples. Most of them are just nice javascript/DOM/DHTML implementations without any actual AJAX functionality but I guess someone at MSFT decided that using a new buzzword Ajax was nicer than calling it the “DHTML control toolkit”. My favorites among the useful controls in the toolkit include the CalendarExtender, the DragPanelExtender, the ResizableControlExtender, the TabContainer, and the ValidationCalloutExtender.

The existing asp.net controls, some new ones in 3.5 along with the ajax client- and server-side functionality makes it easier than ever to develop user friendly web based apps.

Later I will be providing some examples of asp.net 3.5 with Ajax in use and how I encapsulate some of the control toolkit controls together with other existing controls to make them more useful for rapid app development and to avoid repeating code. Meanwhile, check out the samples and videos over at ajax.asp.net

Powered by WordPress