[9/12/2009 – Note this post is an update of an older post that discussed a wrapper around Marc Garbanski’s jQuery-calender control. This post updates to the current jQuery.ui version (1.72). The control has been integrated into the West Wind Web & Ajax Toolkit for ASP.NET and you can grab the code from there in the Westwind.Web project. The links to the sample and download point at the Toolkit]

I've posted a wrapper ASP.NET around the jQuery.UI Datepickercontrol. This small client side calendar control is compact, looks nice and is very easy to use and I've added it some time back to my control library.

This is primarily an update for the jQuery.ui version, and so I spend a few hours or so cleaning it up which wasn’t as easy as it could have been since the API has changed quite drastically from Marc’s original implementation. The biggest changes have to do with the theming integration and the resulting explosion of related resources.

If you want to use this component you can check it out a sample and the code here:

The control is a very basic wrapper around jQuery.ui Datepicker control and it provides the basic features of the client control wrapped with server side properties so you can just drag and drop the control onto a page.

Building a control wrapper around this control is pretty straight forward. The main complications arise out of determining the best way of dealing with the resources. ASP.NET controls tend to embed all resources into the control assemblies - which has certain advantages such as the ability to automatically compress the content. But it's not always optimal to do this for example, if you have many sites and can rely on shared script resources in a server - or even on a remote server - to serve resources which is more efficient then letting ASP.NET serve resources.

Unlike the original component that I posted 2+ years back this component does not wrap up all of the required resources in ASP.NET resource for fully self contained operation. The reason for this change is that jquery.ui’s theming has drastically changed the amount of dependencies required and so both jquery.ui and the theme items must be deployed separately. By default the expected location are ~/script/jquery-ui-custom.js and a themes folder below it, but this configurable on the control.

This control works by a SelectedDate property that is tied to the underlying text box - or in the case of the Inline calendar a hidden value. Although jQuery datepicker  is all client side the control implementation is Postback aware and appropriately persists date values.

The main task of the control is simply to map server properties to the appropriate jQuery-calendar initialization code (in jQuery().ready). Thanks to Marc's simple front end to the control it's pretty straight forward to set up  a server control. All of the initialization happens through JavaScript code, so there's a bit of not so clean script generation by the control in the sense the script code generation is always pretty ugly. 

The control is super easy to use. In its default configuration it looks something like this:

<div class="samplebox">
   <h3>Image Button Popup:</h3>
   <hr />
   This version shows a calendar image button to click on to pop up the calendar:<br /><br />
   
   Enter Date: 
        <ww:jQueryDatePicker runat="server" 
                       id="txtImageButton" 
                       DisplayMode="ImageButton" 
                       DateFormat="MM-dd-yyyy" 
                       ShowButtonPanel="true"
                       SelectedDate="08-10-2009" 
                       Theme="Redmond"  />                    
</div>

which looks like this (using the Redmond theme from theme roller):

jQueryDatePicker 

In code behind you can read and write the SelectedDate or Text properties.

There's not a ton of code here so you can check it out for yourself here or by downloading the code from the link above. The code has a dependency on my ClientScript component which is also part of the West Wind Web Toolkit so you may have to download a couple of additional files (ClientScriptProxy.cs, WebUtils.cs at least) or you can replace those calls with the ASP.NET ClientScript or ScriptManager objecs.

Anyways here’s the code:
[updated: Sept. , '09 with for jQuery.ui  1.72]

    /// <summary>
    /// ASP.NET jQuery DatePicker Control Wrapper
    /// by Rick Strahl
    /// http://www.west-wind.com/
    /// based on jQuery UI DatePicker client control by Marc Grabanski    
    /// http://marcgrabanski.com/code/ui-datepicker/
    /// 
    /// Simple DatePicker control that uses jQuery UI DatePicker to pop up 
    /// a date picker. 
    /// 
    /// Important Requirements (configurable):
    /// ~/scripts/jquery.js             (available from WebResource)
    /// ~/scripts/jquery-ui-custom.js   (custom build of jQuery.ui)
    /// ~/scripts/themes/base           (set Theme property for other themes to apply)
    /// 
    /// Resources are embedded into the assembly so you don't need
    /// to reference or distribute anything. You can however override
    /// each of these resources with relative URL based resources.
    /// </summary>
    [ToolboxBitmap(typeof(System.Web.UI.WebControls.Calendar)), DefaultProperty("Text"),
    ToolboxData("<{0}:jQueryDatePicker runat=\"server\"  />")]
    public class jQueryDatePicker : TextBox
    {

        public jQueryDatePicker()
        {
            // Date specific width
            this.Width = Unit.Pixel(80);
        }

        /// <summary>
        /// The currently selected date
        /// </summary>
        [DefaultValue(typeof(DateTime), ""),
        Category("Date Selection")]
        public DateTime? SelectedDate
        {
            get
            {
                DateTime defaultDate = DateTime.Parse("01/01/1900", CultureInfo.InstalledUICulture);

                if (this.Text == "")
                    return defaultDate;

                DateTime.TryParse(this.Text, out defaultDate);
                return defaultDate;
            }
            set
            {
                if (!value.HasValue)
                    this.Text = "";
                else
                {
                    string dateFormat = this.DateFormat;
                    if (dateFormat == "Auto")
                        dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
                    this.Text = value.Value.ToString(dateFormat);
                }
            }
        }


        /// <summary>
        /// Determines how the datepicking option is activated
        /// </summary>
        [Description("Determines how the datepicking option is activated")]
        [Category("Date Selection"), DefaultValue(typeof(DatePickerDisplayModes), "ImageButton")]
        public DatePickerDisplayModes DisplayMode
        {
            get { return _DisplayMode; }
            set { _DisplayMode = value; }
        }
        private DatePickerDisplayModes _DisplayMode = DatePickerDisplayModes.ImageButton;



        /// <summary>
        /// Url to a Calendar Image or WebResource to use the default resource image.
        /// Applies only if the DisplayMode = ImageButton
        /// </summary>
        [Description("Url to a Calendar Image or WebResource to use the default resource image")]
        [Category("Resources"), DefaultValue("WebResource")]
        public string ButtonImage
        {
            get { return _ButtonImage; }
            set { _ButtonImage = value; }
        }
        private string _ButtonImage = "WebResource";

        /// <summary>
        /// The CSS that is used for the calendar
        /// </summary>
        [Category("Resources"), Description("The CSS that is used for the calendar or empty. WebResource loads from resources. This property serves as the base url - use Theme to apply a specific theme"),
         DefaultValue("~/scripts/themes/base/ui.all.css")]
        public string CalendarCss
        {
            get { return _CalendarCss; }
            set { _CalendarCss = value; }
        }
        private string _CalendarCss = "~/scripts/themes/base/ui.all.css";


        /// <summary>
        /// Theme applied to the base CSS url. Replaces /base/ with the theme selected
        /// </summary>
        [Category("Resources"),
         Description("Theme applied to the base CSS url. Replaces /base/ with the theme selected"),
         DefaultValue("Redmond")]        
        public string Theme
        {
            get { return _Theme; }
            set { _Theme = value; }
        }
        private string _Theme = "Redmond";

        /// <summary>
        /// Location for the calendar JavaScript
        /// </summary>
        [Description("Location for the calendar JavaScript or empty for none. WebResource loads from resources")]
        [Category("Resources"), DefaultValue("~/scripts/jquery-ui-custom.js")]
        public string CalendarJs
        {
            get { return _CalendarJs; }
            set { _CalendarJs = value; }
        }
        private string _CalendarJs = "~/scripts/jquery-ui-custom.js";


        /// <summary>
        /// Location of jQuery library. Use WebResource for loading from resources
        /// </summary>
        [Description("Location of jQuery library or empty for none. Use WebResource for loading from resources")]
        [Category("Resources"), DefaultValue("WebResource")]
        public string jQueryJs
        {
            get { return _jQueryJs; }
            set { _jQueryJs = value; }
        }
        private string _jQueryJs = "WebResource";


        /// <summary>
        /// Determines the Date Format used. Auto uses CurrentCulture. Format: MDY/  month, date,year separator
        /// </summary>
        [Description("Determines the Date Format used. Auto uses CurrentCulture. Format: MDY/  month, date,year separator")]
        [Category("Date Selection"), DefaultValue("Auto")]
        public string DateFormat
        {
            get { return _DateFormat; }
            set { _DateFormat = value; }
        }
        private string _DateFormat = "Auto";

        /// <summary>
        /// Minumum allowable date. Leave blank to allow any date
        /// </summary>
        [Description("Minumum allowable date")]
        [Category("Date Selection"), DefaultValue(typeof(DateTime?), null)]
        public DateTime? MinDate
        {
            get { return _MinDate; }
            set { _MinDate = value; }
        }
        private DateTime? _MinDate = null;

        /// <summary>
        /// Maximum allowable date. Leave blank to allow any date.
        /// </summary>
        [Description("Maximum allowable date. Leave blank to allow any date.")]
        [Category("Date Selection"), DefaultValue(typeof(DateTime?), null)]
        public DateTime? MaxDate
        {
            get { return _MaxDate; }
            set { _MaxDate = value; }
        }
        private DateTime? _MaxDate = null;


        /// <summary>
        /// Client event handler fired when a date is selected
        /// </summary>
        [Description("Client event handler fired when a date is selected")]
        [Category("Date Selection"), DefaultValue("")]
        public string OnClientSelect
        {
            get { return _OnClientSelect; }
            set { _OnClientSelect = value; }
        }
        private string _OnClientSelect = "";


        /// <summary>
        /// Client event handler that fires before the date picker is activated
        /// </summary>
        [Description("Client event handler that fires before the date picker is activated")]
        [Category("Date Selection"), DefaultValue("")]
        public string OnClientBeforeShow
        {
            get { return _OnClientBeforeShow; }
            set { _OnClientBeforeShow = value; }
        }
        private string _OnClientBeforeShow = "";


        /// <summary>
        /// Determines where the Close icon is displayed. True = top, false = bottom.
        /// </summary>
        [Description("Determines where the Today and Close buttons are displayed on the bottom (default styling) of the control.")]
        [Category("Date Selection"), DefaultValue(true)]
        public bool ShowButtonPanel
        {
            get { return _CloseAtTop; }
            set { _CloseAtTop = value; }
        }
        private bool _CloseAtTop = true;


        /// <summary>
        /// Code that embeds related resources (.js and css)
        /// </summary>
        /// <param name="scriptProxy"></param>
        protected void RegisterResources(ClientScriptProxy scriptProxy)
        {
            scriptProxy.LoadControlScript(this, this.jQueryJs, ControlResources.JQUERY_SCRIPT_RESOURCE, ScriptRenderModes.HeaderTop);
            scriptProxy.RegisterClientScriptInclude(this.Page, typeof(ControlResources), this.CalendarJs, ScriptRenderModes.Header);

            string cssPath = this.CalendarCss;
            if (!string.IsNullOrEmpty(this.Theme))
                cssPath = cssPath.Replace("/base/", "/" + this.Theme + "/");

            scriptProxy.RegisterCssLink(this.Page, typeof(ControlResources), cssPath, cssPath);
        }

        protected override void  OnInit(EventArgs e)
        {
                base.OnInit(e);

                // Retrieve the date explicitly - NOTE: Date written by CLIENTID id & name.
                if (this.Page.IsPostBack && this.DisplayMode == DatePickerDisplayModes.Inline)
                    this.Text = this.Page.Request.Form[this.ClientID]; // Note this is the right value!            
        }

        

        
        /// <summary>
        /// Most of the work happens here for generating the hook up script code
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            // MS AJAX aware script management
            ClientScriptProxy scriptProxy = ClientScriptProxy.Current;

            // Register resources
            this.RegisterResources(scriptProxy);

            string dateFormat = this.DateFormat;

            if (string.IsNullOrEmpty(dateFormat) || dateFormat == "Auto")
            {
                // Try to create a data format string from culture settings
                // this code will fail if culture can't be mapped on server hence the empty try/catch
                try
                {
                    dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
                }
                catch { }
            }

            dateFormat = dateFormat.ToLower().Replace("yyyy", "yy");

            // Capture and map the various option parameters
            StringBuilder sbOptions = new StringBuilder(512);
            sbOptions.Append("{");

            string onSelect = this.OnClientSelect;

            if (this.DisplayMode == DatePickerDisplayModes.Button)
                sbOptions.Append("showOn: 'button',");
            else if (this.DisplayMode == DatePickerDisplayModes.ImageButton)
            {
                string img = this.ButtonImage;
                if (img == "WebResource")
                    img = scriptProxy.GetWebResourceUrl(this, typeof(ControlResources), ControlResources.CALENDAR_ICON_RESOURCE);
                else
                    img = this.ResolveUrl(this.ButtonImage);

                sbOptions.Append("showOn: 'button', buttonImageOnly: true, buttonImage: '" + img + "',buttonText: 'Select date',");
            }
            else if (this.DisplayMode == DatePickerDisplayModes.Inline)
            {                
                // need to store selection in the page somehow for inline since it's
                // not tied to a textbox
                scriptProxy.RegisterHiddenField(this, this.ClientID, this.Text);
                onSelect = this.ClientID + "OnSelect";
            }

            if (!string.IsNullOrEmpty(onSelect))
                sbOptions.Append("onSelect: " + onSelect + ",");

            if (this.DisplayMode != DatePickerDisplayModes.Inline)
            {
                if (!string.IsNullOrEmpty(this.OnClientBeforeShow))
                    sbOptions.Append("beforeShow: function(y,z) { $('#ui-datepicker-div').maxZIndex(); " + 
                                     this.OnClientBeforeShow + "(y,z); },");
                else
                    sbOptions.Append("beforeShow: function() { $('#ui-datepicker-div').maxZIndex(); },");
                        
            }


            if (this.MaxDate.HasValue)
                sbOptions.Append("maxDate: " + WebUtils.EncodeJsDate(MaxDate.Value) + ",");

            if (this.MinDate.HasValue)
                sbOptions.Append("minDate: " + WebUtils.EncodeJsDate(MinDate.Value) + ",");

            if (this.ShowButtonPanel)
                sbOptions.Append("showButtonPanel: true,");

            sbOptions.Append("dateFormat: '" + dateFormat + "'}");


            // Write out initilization code for calendar
            StringBuilder sbStartupScript = new StringBuilder(400);
            sbStartupScript.AppendLine("$(document).ready( function() {");


            if (this.DisplayMode != DatePickerDisplayModes.Inline)
            {
                scriptProxy.RegisterClientScriptBlock(this.Page,
                                                      typeof(ControlResources),
                                                      "__attachDatePickerInputKeys",
                                                      this.AttachDatePickerKeysScript, true);

                sbStartupScript.AppendFormat("var cal = jQuery('#{0}').datepicker({1}).attachDatepickerInputKeys();\r\n",
                                             this.ClientID, sbOptions);
            }
            else
            {
                sbStartupScript.AppendLine("var cal = jQuery('#" + this.ClientID + "Div').datepicker(" + sbOptions.ToString() + ")");

                if (this.SelectedDate.HasValue && this.SelectedDate.Value > new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc))
                {
                    WestwindJsonSerializer ser = new WestwindJsonSerializer();
                    ser.DateSerializationMode = JsonDateEncodingModes.NewDateExpression;
                    string jsDate = ser.Serialize(this.SelectedDate);

                    sbStartupScript.AppendLine("cal.datepicker('setDate'," + jsDate + ");");
                }
                else
                    sbStartupScript.AppendLine("cal.datepicker('setDate',new Date());");

                // Assign value to hidden form var on selection
                scriptProxy.RegisterStartupScript(this, typeof(ControlResources), this.UniqueID + "OnSelect",
                    "function  " + this.ClientID + "OnSelect(dateStr)\r\n" +
                    "{\r\n" +
                    ((!string.IsNullOrEmpty(this.OnClientSelect)) ? this.OnClientSelect + "(dateStr);\r\n" : "") +
                    "jQuery('#" + this.ClientID + "')[0].value = dateStr;\r\n}\r\n", true);
            }

            sbStartupScript.AppendLine("} );");
            scriptProxy.RegisterStartupScript(this.Page, typeof(ControlResources), "_cal" + this.UniqueID,
                 sbStartupScript.ToString(), true);
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="writer"></param>
        public override void RenderControl(HtmlTextWriter writer)
        {
            if (this.DisplayMode != DatePickerDisplayModes.Inline)
                base.RenderControl(writer);
            else
            {
                
                if (this.DesignMode)
                    writer.Write("<div id='" + this.ClientID + "Div' style='width: 200px; height: 200px; padding: 20px;background: silver; color; white'>Inline Calendar Placeholder</div>");
                else
                    writer.Write("<div id='" + this.ClientID + "Div'></div>");
            }

            // this code is only for the designer
            if (HttpContext.Current == null)
            {
                if (this.DisplayMode == DatePickerDisplayModes.Button)
                {
                    writer.Write(" <input type='button' value='...' style='width: 20px; height: 20px;' />");
                }
                else if ((this.DisplayMode == DatePickerDisplayModes.ImageButton))
                {
                    string img;
                    if (this.ButtonImage == "WebResource")
                        img = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), ControlResources.CALENDAR_ICON_RESOURCE);
                    else
                        img = this.ResolveUrl(this.ButtonImage);

                    writer.AddAttribute(HtmlTextWriterAttribute.Src, img);
                    writer.AddAttribute("hspace", "2");
                    writer.RenderBeginTag(HtmlTextWriterTag.Img);
                    writer.RenderEndTag();
                }
            }
        }

        private string AttachDatePickerKeysScript =
@"
$.fn.attachDatepickerInputKeys = function(callback) {
    if (this.length < 1) return this;

    this.keydown(function(e) {
        var j = jQuery(this);
        var di = $.datepicker._getInst(this);
        if (!di)
            return;

        $.datepicker._setDateFromField(di);  // force update first

        var d = j.datepicker('getDate');
        if (!d)
            return true;

        var month = d.getMonth();
        var year = d.getFullYear();
        var day = d.getDate();

        switch (e.keyCode) {
            case 84: // [T]oday
                d = new Date(); break;
            case 109: case 189:
                d = new Date(year, month, day - 1); break;
            case 107: case 187:
                d = new Date(year, month, day + 1); break;
            case 77: //M
                d = new Date(year, month - 1, day); break;
            case 72: //H
                d = new Date(year, month + 1, day); break;
            default:
                return true;
        }
        
        j.datepicker('setDate', d);
        if (callback)
            callback(this);
        return false;
    });
    return this;
}
$.fn.maxZIndex = function(opt) {
    var def = { inc: 10, group: ""*"" };
    $.extend(def, opt);
    var zmax = 0;
    $(def.group).each(function() {
        var cur = parseInt($(this).css('z-index'));
        zmax = cur > zmax ? cur : zmax;
    });
    if (!this.jquery)
        return zmax;

    return this.each(function() {
        zmax += def.inc;
        $(this).css(""z-index"", zmax);
    });
}
";
    }


    public enum DatePickerDisplayModes
    {
        Button,
        ImageButton,
        AutoPopup,
        Inline
    }

Enjoy,

+++ Rick --