Lately I decided to see how easy was to create AJAX custom controls specifically using the included javascript libraries. I thought it might be useful to try to use the included javascript libraries to enable client side access to the selected value from a custom control.
The example I will use is a date time picker which uses the calendar extender from the ajax control toolkit for the date and a simple drop down for the time. Much like outlook, the date is pickable from a textbox and the time in a list of 15 minute intervals. The goal is to allow the client and server side code to have access to the same DateTime type without having to do any work parsing the strings in the HTML elements on the page.
Usage Pattern
The control on the client page looks like the following.
<pgc:DateTimePicker ID="dtpMyDate" runat="server" TimeInterval="15" Format="dd/MM/yyyy"/>
The client script needed to get the value from the control is as follows
{
alert( $find( "dtpMyDate" ).get_selectedDateTime().format("dddd dd MMMM yyyy HH:mm") );
}
....and that's it! The screenshot below shows the output.
The Implementation
To implement the custom control described above we have to implement the IScriptControl interface from the AJAX Extensions as well as the standard CompositeControl. This requires that we implement a few methods on our control above and beyond the standard control methods, these are the GetScriptReferences and the GetScriptDescriptors methods.
GetScriptReferences allows us to add in the client script files to the output sent to the user. This tells the ScriptManager on the page to send down our client script file.
{
ScriptReference ProtoReference = newScriptReference();
ProtoReference.Path = Page.ClientScript.GetWebResourceUrl(this.GetType(), "PGCodeWorks.AjaxControls.DateTimePicker.DateTimePicker.js");
return newScriptReference[] { ProtoReference };
}
GetScriptDescriptors allows us to tell the control which variables to pass through to the client script. These variables are set on the client when the page is sent down and allow us to use the same javascript object to represent multiple controls on one page with different instances and properties.
{
ScriptBehaviorDescriptor descriptor = newScriptBehaviorDescriptor("PGCodeWorks.DateTimePicker", this.ClientID);
descriptor.AddProperty("textBoxClientID", this.txtDate.ClientID);
descriptor.AddProperty("dropDownClientID", this.ddlTime.ClientID);
descriptor.AddProperty("selectedDateTime", this._selectedDateTime);
descriptor.AddProperty("dateFormat", this.ceCalExtender.Format);
descriptor.AddProperty("timeFormat", this._timeFormat);
descriptor.AddProperty("timeInterval", this._timeInterval);
return newScriptDescriptor[] { descriptor };
}
Then, after implementing the rest of our control we need to add the client script .js file to the solution and mark it as an embedded resource. To make the file embed itself within the DLL select the file in solution explorer and change the "Build Action" in the properties window. Also you must add a reference to it in the AssemblyInfo.cs file.
The actual file follows the object-oriented javascript approach to allow properties etc on the prototype model. Below is the structure of the .js file (some code has been cut-out to save space).
// Register the namespace for the control
Type.registerNamespace('PGCodeWorks');
//
// Define the control properties
//
PGCodeWorks.DateTimePicker = function(element) {
PGCodeWorks.DateTimePicker.initializeBase(this, [element]);
// Set properties to null
this._textBoxClientID = null;
this._dropDownClientID = null;
// *** Code Truncated ***
}
//
//
PGCodeWorks.DateTimePicker.prototype = {
initialize : function() {
PGCodeWorks.DateTimePicker.callBaseMethod(this, 'initialize');
this._onDateTimeChangeHandler = Function.createDelegate(this, this._onDateTimeChange);
this._onDateChangeHandler = Function.createDelegate(this, this._onDateChange);
this._onTimeChangeHandler = Function.createDelegate(this, this._onTimeChange);
$addHandler($get(this._textBoxClientID), 'change' , this._onDateChangeHandler );
$addHandler($get(this._dropDownClientID), 'change', this._onTimeChangeHandler);
},
dispose : function() {
$removeHandler($get(this._textBoxClientID), 'change' , this._onDateChangeHandler );
$removeHandler($get(this._dropDownClientID), 'change', this._onTimeChangeHandler);
PGCodeWorks.DateTimePicker.callBaseMethod(this, 'dispose');
},
//
// Custom Methods
//
setDateTimeValues : function() {
// Get Values
var strDate = $get(this._textBoxClientID).value;
var strTime = $get(this._dropDownClientID).value;
var strTimeFormat = this._timeFormat;
if( strDate != null && strDate != "" )
{
if( strTime == null || strTime == "" )
{
strTime = "00:00";
strTimeFormat = "HH:mm";
}
var totalDateTimeFormat = this._dateFormat + ' ' + strTimeFormat;
var newDate = Date.parseInvariant( strDate + ' ' + strTime, totalDateTimeFormat );
this.set_selectedDateTime( newDate );
//alert( this._selectedDateTime );
}
},
//
// Event delegates
//
_onDateTimeChange : function(e) {
this.setDateTimeValues();
},
_onDateChange : function(e) {
this.setDateTimeValues();
},
_onTimeChange : function(e) {
this.setDateTimeValues();
},
//
// Control properties
//
get_textBoxClientID : function() {
return this._textBoxClientID;
},
set_textBoxClientID : function(value) {
if (this._textBoxClientID !== value) {
this._textBoxClientID = value;
this.raisePropertyChanged('textBoxClientID');
}
},
// *** Code Truncated ***
get_timeInterval : function() {
return this._timeInterval;
},
set_timeInterval : function(value) {
if(this._timeInterval != value) {
this._timeInterval = value;
this.raisePropertyChanged('timeInterval');
}
}
} // end of prototype declaration
// Optional descriptor for JSON serialization.
PGCodeWorks.DateTimePicker.descriptor = {
properties: [ {name: 'textBoxClientID', type: String},
{name: 'dropDownClientID', type: String},
{name: 'selectedDateTime', type: Date},
{name: 'dateFormat', type: String},
{name: 'timeFormat', type: String},
{name: 'timeInterval', type: Number} ]
}
// Register the class as a type that inherits from Sys.UI.Control.
PGCodeWorks.DateTimePicker.registerClass('PGCodeWorks.DateTimePicker', Sys.UI.Control);
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
The above code has allowed the client to be able to call the javascript object representing the overall control and retrieve it's selectedDateTime property.
How Does it Work?
The libraries provided with the ASP.Net AJAX Extensions allow us to create composite controls which we can deal with as a single object on the client using object-oriented javascript. Using the patterns and examples given on the AJAX web site we can recreate the types of functionality in the AjaxControlToolkit and start building rich client side experiences.
In this example we have simply provided a public method on our object which grabs the values from the controls on the page and uses the type extensions in AJAX to provide a similar experience in client side development as we experience on the server side. To extend this example I have created a RequiredFieldValidator and a CompareValidator which will work with the above control and I hope to post this when it has been cleaned up.
The full code and solution for this example is available here