The control is a very basic wrapper around jQuery Calendar 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. The control also wraps all the resources including jQuery so the control is self contained.
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.
I'm still trying to decide how to best integrate with jQuery in general in my own control libraries and with additional libraries in particular. My current leanings are to leaving scripts external with options to explicitly set the script path. One of the big problems is always script versioning, but also the possibility that scripts are already being included into a page - a possibly likely scenario for jQuery for example.
In this control the default is WebResource which uses embedded resources, with specific path overrides for jQuery.js, jquery-calendar.js and jquery-calendar.css which adds a few extra properties, but gives a little more controls. I'd be interested to hear how others are dealing with maintstream script resources.
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 calendar is all client side the control 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.
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.
[updated: Oct. 10, '07 with feedback from comments]
/// <summary>
/// ASP.NET jQuery Calendar Control Wrapper
/// by Rick Strahl
/// http://www.west-wind.com/
///
/// based on jQuery calendar client control by Marc Grabanski
/// http://marcgrabanski.com/code/jquery-calendar/
///
/// Simple DatePicker control that uses jQuery-calendar to pop up
/// a date picker.
///
/// Important Requirements:
/// scripts/jquery.js
/// scripts/jquery-calendar.js
/// scripts/calendar.css
///
/// 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}:jQueryCalendar runat='server' width='80px'></{0}:jQueryCalendar>")]public class jQueryCalendar : TextBox
{ public jQueryCalendar()
{ 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
this.Text = value.Value.ToString("d"); }
}
/// <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."), DefaultValue("WebResource")] public string CalendarCss
{ get { return _CalendarCss; } set { _CalendarCss = value; } }
private string _CalendarCss = "WebResource";
/// <summary>
/// Location for the calendar JavaScript
/// </summary>
[Description("Location for the calendar JavaScript or empty for none. WebResource loads from resources")] [Category("Resources"), DefaultValue("WebResource")] public string CalendarJs
{ get { return _CalendarJs; } set { _CalendarJs = value; } }
private string _CalendarJs = "WebResource";
/// <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>
/// Determines where the Close icon is displayed. True = top, false = bottom.
/// </summary>
[Description("Determines where the Close icon is displayed. True = top, false = bottom.")] [Category("Date Selection"), DefaultValue(true)] public bool CloseAtTop
{ get { return _CloseAtTop; } set { _CloseAtTop = value; } }
private bool _CloseAtTop = true;
/// <summary>
/// Code that embeds related resources (.js and css)
/// </summary>
/// <param name="p"></param>
protected void RegisterResources(ClientScriptProxy p)
{ // *** Make sure jQuery is loaded
if (this.jQueryJs == "WebResource")
ControlResources.LoadjQuery(this.Page);
else if(!string.IsNullOrEmpty(this.jQueryJs))
p.RegisterClientScriptInclude(this.Page, typeof(ControlResources),
"_jqueryjs", this.ResolveUrl(this.jQueryJs));
// *** Load the calandar script
string script = this.CalendarJs;
// *** Load jQuery Calendar Scripts
if (script == "WebResource")
p.RegisterClientScriptResource(this.Page, typeof(ControlResources),
ControlResources.JQUERY_CALENDAR_SCRIPT_RESOURCE);
else if (!string.IsNullOrEmpty(script))
p.RegisterClientScriptInclude(this.Page, typeof(ControlResources),
"__jqueryCalendar",
this.ResolveUrl(script));
// *** Load the related CSS reference into the page
script = this.CalendarCss;
if (script == "WebResource")
script = p.GetWebResourceUrl(this.Page, typeof(ControlResources),
ControlResources.JQUERY_CALENDAR_CSS_RESOURCE);
else if (!string.IsNullOrEmpty(script))
script = this.ResolveUrl(this.CalendarCss);
// *** Register Calendar CSS 'manually'
string css = @"<link href=""" + script +
@""" type=""text/css"" rel=""stylesheet"" />";
p.RegisterClientScriptBlock(this.Page, typeof(ControlResources), "_calcss", css, false);
}
/// <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 p = ClientScriptProxy.Current;
// *** Register resources
this.RegisterResources(p);
string dateFormat = this.DateFormat;
if (string.IsNullOrEmpty(dateFormat))
dateFormat = "MDY/";
else if(dateFormat == "Auto")
{ string sep = CultureInfo.CurrentCulture.DateTimeFormat.DateSeparator;
dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern.ToLower().Replace(sep, "").Replace("yyyy", "y").Replace("mm", "m").Replace("dd", "d").ToUpper() + sep; }
// *** 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( "autoPopUp: 'button',");
else if (this.DisplayMode == DatePickerDisplayModes.ImageButton)
{ string img = this.ButtonImage;
if (img == "WebResource")
img = p.GetWebResourceUrl(this, typeof(ControlResources), ControlResources.CALENDAR_ICON_RESOURCE);
else
img = this.ResolveUrl(this.ButtonImage);
sbOptions.Append("autoPopUp: 'button', buttonImageOnly: true, buttonImage: '" + img + "',buttonText: 'Select date',"); }
else if (this.DisplayMode == DatePickerDisplayModes.Inline)
{ p.RegisterHiddenField(this, this.ClientID, this.Text);
onSelect = this.ClientID + "OnSelect";
}
if (!string.IsNullOrEmpty(onSelect))
sbOptions.Append("onSelect: " + onSelect + ",");
if (this.MaxDate.HasValue)
sbOptions.Append("maxDate: " + wwWebUtils.EncodeJsDate(MaxDate.Value) + ",");
if (this.MinDate.HasValue)
sbOptions.Append("minDate: " + wwWebUtils.EncodeJsDate(MinDate.Value) + ",");
if (!this.CloseAtTop)
sbOptions.Append("closeAtTop: false,");
sbOptions.Append("dateFormat: '" + dateFormat + "'}");
// *** Write out initilization code for calendar
StringBuilder sbStartupScript = new StringBuilder(400);
sbStartupScript.AppendLine("jQuery(document).ready( function() {");
if (this.DisplayMode != DatePickerDisplayModes.Inline)
sbStartupScript.AppendLine("var cal = jQuery('#" + this.ClientID + "').calendar(" + sbOptions.ToString() + ");"); else
{ sbStartupScript.AppendLine("var cal = jQuery('#" + this.ClientID + "Div').calendar(" + sbOptions.ToString() + ");");
if (this.SelectedDate.HasValue && this.SelectedDate.Value > new DateTime(1900,1,1,0,0,0,DateTimeKind.Utc))
sbStartupScript.AppendLine("popUpCal.setDateFor(cal[0],new Date('" + this.Text + "'));");
sbStartupScript.AppendLine("popUpCal.reconfigureFor(cal[0]);");
// *** Assign value to hidden form var on selection
p.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("} );"); p.RegisterStartupScript(this.Page, typeof(ControlResources), "_cal" + this.ID ,
sbStartupScript.ToString() , true);
}
/// <summary>
///
/// </summary>
/// <param name="writer"></param>
public override void RenderControl(HtmlTextWriter writer)
{ if (this.DisplayMode != DatePickerDisplayModes.Inline)
base.RenderControl(writer);
else
{ writer.Write("<div id='" + this.ClientID + "Div'></div>"); }
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();
}
}
}
}
public enum DatePickerDisplayModes
{ Button,
ImageButton,
AutoPopup,
Inline
}