The goal of this blog entry is to explain how you can extend the Ajax Control Toolkit with custom Ajax Control Toolkit controls. I describe how you can create the two halves of an Ajax Control Toolkit control: the server-side control extender and the client-side control behavior. Finally, I explain how you can use the new Ajax Control Toolkit control in a Web Forms page. At the end of this blog entry, there is a link to download a Visual Studio 2010 solution which contains the code for two Ajax Control Toolkit controls: SampleExtender and PopupHelpExtender. The SampleExtender contains the minimum skeleton for creating a new Ajax Control Toolkit control. You can use the SampleExtender as a starting point for your custom Ajax Control Toolkit controls. The PopupHelpExtender control is a super simple custom Ajax Control Toolkit control. This control extender displays a help message when you start typing into a TextBox control. The animated GIF below demonstrates what happens when you click into a TextBox which has been extended with the PopupHelp extender. Here’s a sample of a Web Forms page which uses the control: <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ShowPopupHelp.aspx.cs" Inherits="MyACTControls.Web.Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html >
<head runat="server">
<title>Show Popup Help</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<act:ToolkitScriptManager ID="tsm" runat="server" />
<%-- Social Security Number --%>
<asp:Label
ID="lblSSN"
Text="SSN:"
AssociatedControlID="txtSSN"
runat="server" />
<asp:TextBox
ID="txtSSN"
runat="server" />
<act:PopupHelpExtender
id="ph1"
TargetControlID="txtSSN"
HelpText="Please enter your social security number."
runat="server" />
<%-- Social Security Number --%>
<asp:Label
ID="lblPhone"
Text="Phone Number:"
AssociatedControlID="txtPhone"
runat="server" />
<asp:TextBox
ID="txtPhone"
runat="server" />
<act:PopupHelpExtender
id="ph2"
TargetControlID="txtPhone"
HelpText="Please enter your phone number."
runat="server" />
</div>
</form>
</body>
</html>
In the page above, the PopupHelp extender is used to extend the functionality of the two TextBox controls. When focus is given to a TextBox control, the popup help message is displayed.
An Ajax Control Toolkit control extender consists of two parts: a server-side control extender and a client-side behavior. For example, the PopupHelp extender consists of a server-side PopupHelpExtender control (PopupHelpExtender.cs) and a client-side PopupHelp behavior JavaScript script (PopupHelpBehavior.js).
Over the course of this blog entry, I describe how you can create both the server-side extender and the client-side behavior.
Writing the Server-Side Code
Creating a Control Extender
You create a control extender by creating a class that inherits from the abstract ExtenderControlBase class. For example, the PopupHelpExtender control is declared like this:
public class PopupHelpExtender: ExtenderControlBase {
}
The ExtenderControlBase class is part of the Ajax Control Toolkit. This base class contains all of the common server properties and methods of every Ajax Control Toolkit extender control.
The ExtenderControlBase class inherits from the ExtenderControl class. The ExtenderControl class is a standard class in the ASP.NET framework located in the System.Web.UI namespace. This class is responsible for generating a client-side behavior. The class generates a call to the Microsoft Ajax Library $create() method which looks like this:
<script type="text/javascript">
$create(MyACTControls.PopupHelpBehavior, {"HelpText":"Please enter your social security number.","id":"ph1"}, null, null, $get("txtSSN"));
});
</script>
The JavaScript $create() method is part of the Microsoft Ajax Library. The reference for this method can be found here:
http://msdn.microsoft.com/en-us/library/bb397487.aspx
This method accepts the following parameters:
type – The type of client behavior to create. The $create() method above creates a client PopupHelpBehavior.
Properties – Enables you to pass initial values for the properties of the client behavior. For example, the initial value of the HelpText property. This is how server property values are passed to the client.
Events – Enables you to pass client-side event handlers to the client behavior.
References – Enables you to pass references to other client components.
Element – The DOM element associated with the client behavior. This will be the DOM element associated with the control being extended such as the txtSSN TextBox.
The $create() method is generated for you automatically. You just need to focus on writing the server-side control extender class.
Specifying the Target Control
All Ajax Control Toolkit extenders inherit a TargetControlID property from the ExtenderControlBase class. This property, the TargetControlID property, points at the control that the extender control extends. For example, the Ajax Control Toolkit TextBoxWatermark control extends a TextBox, the ConfirmButton control extends a Button, and the Calendar control extends a TextBox.
You must indicate the type of control which your extender is extending. You indicate the type of control by adding a [TargetControlType] attribute to your control. For example, the PopupHelp extender is declared like this:
[TargetControlType(typeof(TextBox))]
public class PopupHelpExtender: ExtenderControlBase {
}
The PopupHelp extender can be used to extend a TextBox control. If you try to use the PopupHelp extender with another type of control then an exception is thrown.
If you want to create an extender control which can be used with any type of ASP.NET control (Button, DataView, TextBox or whatever) then use the following attribute:
[TargetControlType(typeof(Control))]
Decorating Properties with Attributes
If you decorate a server-side property with the [ExtenderControlProperty] attribute then the value of the property gets passed to the control’s client-side behavior. The value of the property gets passed to the client through the $create() method discussed above.
The PopupHelp control contains the following HelpText property:
[ExtenderControlProperty]
[RequiredProperty]
public string HelpText {
get { return GetPropertyValue("HelpText", "Help Text"); }
set { SetPropertyValue("HelpText", value); }
}
The HelpText property determines the help text which pops up when you start typing into a TextBox control. Because the HelpText property is decorated with the [ExtenderControlProperty] attribute, any value assigned to this property on the server is passed to the client automatically.
For example, if you declare the PopupHelp extender in a Web Form page like this:
<asp:TextBox
ID="txtSSN"
runat="server" />
<act:PopupHelpExtender
id="ph1"
TargetControlID="txtSSN"
HelpText="Please enter your social security number."
runat="server" />
Then the PopupHelpExtender renders the call to the the following Microsoft Ajax Library $create() method:
$create(MyACTControls.PopupHelpBehavior, {"HelpText":"Please enter your social security number.","id":"ph1"}, null, null, $get("txtSSN"));
You can see this call to the JavaScript $create() method by selecting View Source in your browser. This call to the $create() method calls a method named set_HelpText() automatically and passes the value “Please enter your social security number”.
There are several attributes which you can use to decorate server-side properties including:
ExtenderControlProperty – When a property is marked with this attribute, the value of the property is passed to the client automatically.
ExtenderControlEvent – When a property is marked with this attribute, the property represents a client event handler.
Required – When a value is not assigned to this property on the server, an error is displayed.
DefaultValue – The default value of the property passed to the client.
ClientPropertyName – The name of the corresponding property in the JavaScript behavior. For example, the server-side property is named ID (uppercase) and the client-side property is named id (lower-case).
IDReferenceProperty – Applied to properties which refer to the IDs of other controls.
URLProperty – Calls ResolveClientURL() to convert from a server-side URL to a URL which can be used on the client.
ElementReference – Returns a reference to a DOM element by performing a client $get().
The WebResource, ClientResource, and the RequiredScript Attributes
The PopupHelp extender uses three embedded resources named PopupHelpBehavior.js, PopupHelpBehavior.debug.js, and PopupHelpBehavior.css. The first two files are JavaScript files and the final file is a Cascading Style sheet file.
These files are compiled as embedded resources. You don’t need to mark them as embedded resources in your Visual Studio solution because they get added to the assembly when the assembly is compiled by a build task. You can see that these files get embedded into the MyACTControls assembly by using Red Gate’s .NET Reflector tool:
In order to use these files with the PopupHelp extender, you need to work with both the WebResource and the ClientScriptResource attributes.
The PopupHelp extender includes the following three WebResource attributes.
[assembly: WebResource("PopupHelp.PopupHelpBehavior.js", "text/javascript")]
[assembly: WebResource("PopupHelp.PopupHelpBehavior.debug.js", "text/javascript")]
[assembly: WebResource("PopupHelp.PopupHelpBehavior.css", "text/css", PerformSubstitution = true)]
These WebResource attributes expose the embedded resource from the assembly so that they can be accessed by using the ScriptResource.axd or WebResource.axd handlers. The first parameter passed to the WebResource attribute is the name of the embedded resource and the second parameter is the content type of the embedded resource.
The PopupHelp extender also includes the following ClientScriptResource and ClientCssResource attributes:
[ClientScriptResource("MyACTControls.PopupHelpBehavior", "PopupHelp.PopupHelpBehavior.js")]
[ClientCssResource("PopupHelp.PopupHelpBehavior.css")]
Including these attributes causes the PopupHelp extender to request these resources when you add the PopupHelp extender to a page. If you open View Source in a browser which uses the PopupHelp extender then you will see the following link for the Cascading Style Sheet file:
<link href="/WebResource.axd?d=0uONMsWXUuEDG-pbJHAC1kuKiIMteQFkYLmZdkgv7X54TObqYoqVzU4mxvaa4zpn5H9ch0RDwRYKwtO8zM5mKgO6C4WbrbkWWidKR07LD1d4n4i_uNB1mHEvXdZu2Ae5mDdVNDV53znnBojzCzwvSw2&t=634417392021676003" type="text/css" rel="stylesheet" />
You also will see the following script include for the JavaScript file:
<script src="/ScriptResource.axd?d=pIS7xcGaqvNLFBvExMBQSp_0xR3mpDfS0QVmmyu1aqDUjF06TrW1jVDyXNDMtBHxpRggLYDvgFTWOsrszflZEDqAcQCg-hDXjun7ON0Ol7EXPQIdOe1GLMceIDv3OeX658-tTq2LGdwXhC1-dE7_6g2&t=ffffffff88a33b59" type="text/javascript"></script>
The JavaScrpt file returned by this request to ScriptResource.axd contains the combined scripts for any and all Ajax Control Toolkit controls in a page.
By default, the Ajax Control Toolkit combines all of the JavaScript files required by a page into a single JavaScript file. Combining files in this way really speeds up how quickly all of the JavaScript files get delivered from the web server to the browser.
So, by default, there will be only one ScriptResource.axd include for all of the JavaScript files required by a page. If you want to disable Script Combining, and create separate links, then disable Script Combining like this:
<act:ToolkitScriptManager ID="tsm" runat="server" CombineScripts="false" />
There is one more important attribute used by Ajax Control Toolkit extenders. The PopupHelp behavior uses the following two RequirdScript attributes to load the JavaScript files which are required by the PopupHelp behavior:
[RequiredScript(typeof(CommonToolkitScripts), 0)]
[RequiredScript(typeof(PopupExtender), 1)]
The first parameter of the RequiredScript attribute represents either the string name of a JavaScript file or the type of an Ajax Control Toolkit control. The second parameter represents the order in which the JavaScript files are loaded (This second parameter is needed because .NET attributes are intrinsically unordered).
In this case, the RequiredScript attribute will load the JavaScript files associated with the CommonToolkitScripts type and the JavaScript files associated with the PopupExtender in that order. The PopupHelp behavior depends on these JavaScript files.
Writing the Client-Side Code
The PopupHelp extender uses a client-side behavior written with the Microsoft Ajax Library. Here is the complete code for the client-side behavior:
(function () {
// The unique name of the script registered with the
// client script loader
var scriptName = "PopupHelpBehavior";
function execute() {
Type.registerNamespace('MyACTControls');
MyACTControls.PopupHelpBehavior = function (element) {
/// <summary>
/// A behavior which displays popup help for a textbox
/// </summmary>
/// <param name="element" type="Sys.UI.DomElement">The element to attach to</param>
MyACTControls.PopupHelpBehavior.initializeBase(this, [element]);
this._textbox = Sys.Extended.UI.TextBoxWrapper.get_Wrapper(element);
this._cssClass = "ajax__popupHelp";
this._popupBehavior = null;
this._popupPosition = Sys.Extended.UI.PositioningMode.BottomLeft;
this._popupDiv = null;
this._helpText = "Help Text";
this._element$delegates = {
focus: Function.createDelegate(this, this._element_onfocus),
blur: Function.createDelegate(this, this._element_onblur)
};
}
MyACTControls.PopupHelpBehavior.prototype = {
initialize: function () {
MyACTControls.PopupHelpBehavior.callBaseMethod(this, 'initialize');
// Add event handlers for focus and blur
var element = this.get_element();
$addHandlers(element, this._element$delegates);
},
_ensurePopup: function () {
if (!this._popupDiv) {
var element = this.get_element();
var id = this.get_id();
this._popupDiv = $common.createElementFromTemplate({
nodeName: "div",
properties: { id: id + "_popupDiv" },
cssClasses: ["ajax__popupHelp"]
}, element.parentNode);
this._popupBehavior = new $create(Sys.Extended.UI.PopupBehavior, { parentElement: element }, {}, {}, this._popupDiv);
this._popupBehavior.set_positioningMode(this._popupPosition);
}
},
get_HelpText: function () {
return this._helpText;
},
set_HelpText: function (value) {
if (this._HelpText != value) {
this._helpText = value;
this._ensurePopup();
this._popupDiv.innerHTML = value;
this.raisePropertyChanged("Text")
}
},
_element_onfocus: function (e) {
this.show();
},
_element_onblur: function (e) {
this.hide();
},
show: function () {
this._popupBehavior.show();
},
hide: function () {
if (this._popupBehavior) {
this._popupBehavior.hide();
}
},
dispose: function() {
var element = this.get_element();
$clearHandlers(element);
if (this._popupBehavior) {
this._popupBehavior.dispose();
this._popupBehavior = null;
}
}
};
MyACTControls.PopupHelpBehavior.registerClass('MyACTControls.PopupHelpBehavior', Sys.Extended.UI.BehaviorBase);
Sys.registerComponent(MyACTControls.PopupHelpBehavior, { name: "popupHelp" });
} // execute
if (window.Sys && Sys.loader) {
Sys.loader.registerScript(scriptName, ["ExtendedBase", "ExtendedCommon"], execute);
}
else {
execute();
}
})();
In the following sections, we’ll discuss how this client-side behavior works.
Wrapping the Behavior for the Script Loader
The behavior is wrapped with the following script:
(function () {
// The unique name of the script registered with the
// client script loader
var scriptName = "PopupHelpBehavior";
function execute() {
// Behavior Content
} // execute
if (window.Sys && Sys.loader) {
Sys.loader.registerScript(scriptName, ["ExtendedBase", "ExtendedCommon"], execute);
}
else {
execute();
}
})();
This code is required by the Microsoft Ajax Library Script Loader. You need this code if you plan to use a behavior directly from client-side code and you want to use the Script Loader. If you plan to only use your code in the context of the Ajax Control Toolkit then you can leave out this code.
Registering a JavaScript Namespace
The PopupHelp behavior is declared within a namespace named MyACTControls. In the code above, this namespace is created with the following registerNamespace() method:
Type.registerNamespace('MyACTControls');
JavaScript does not have any built-in way of creating namespaces to prevent naming conflicts. The Microsoft Ajax Library extends JavaScript with support for namespaces. You can learn more about the registerNamespace() method here:
http://msdn.microsoft.com/en-us/library/bb397723.aspx
Creating the Behavior
The actual Popup behavior is created with the following code.
MyACTControls.PopupHelpBehavior = function (element) {
/// <summary>
/// A behavior which displays popup help for a textbox
/// </summmary>
/// <param name="element" type="Sys.UI.DomElement">The element to attach to</param>
MyACTControls.PopupHelpBehavior.initializeBase(this, [element]);
this._textbox = Sys.Extended.UI.TextBoxWrapper.get_Wrapper(element);
this._cssClass = "ajax__popupHelp";
this._popupBehavior = null;
this._popupPosition = Sys.Extended.UI.PositioningMode.BottomLeft;
this._popupDiv = null;
this._helpText = "Help Text";
this._element$delegates = {
focus: Function.createDelegate(this, this._element_onfocus),
blur: Function.createDelegate(this, this._element_onblur)
};
}
MyACTControls.PopupHelpBehavior.prototype = {
initialize: function () {
MyACTControls.PopupHelpBehavior.callBaseMethod(this, 'initialize');
// Add event handlers for focus and blur
var element = this.get_element();
$addHandlers(element, this._element$delegates);
},
_ensurePopup: function () {
if (!this._popupDiv) {
var element = this.get_element();
var id = this.get_id();
this._popupDiv = $common.createElementFromTemplate({
nodeName: "div",
properties: { id: id + "_popupDiv" },
cssClasses: ["ajax__popupHelp"]
}, element.parentNode);
this._popupBehavior = new $create(Sys.Extended.UI.PopupBehavior, { parentElement: element }, {}, {}, this._popupDiv);
this._popupBehavior.set_positioningMode(this._popupPosition);
}
},
get_HelpText: function () {
return this._helpText;
},
set_HelpText: function (value) {
if (this._HelpText != value) {
this._helpText = value;
this._ensurePopup();
this._popupDiv.innerHTML = value;
this.raisePropertyChanged("Text")
}
},
_element_onfocus: function (e) {
this.show();
},
_element_onblur: function (e) {
this.hide();
},
show: function () {
this._popupBehavior.show();
},
hide: function () {
if (this._popupBehavior) {
this._popupBehavior.hide();
}
},
dispose: function() {
var element = this.get_element();
$clearHandlers(element);
if (this._popupBehavior) {
this._popupBehavior.dispose();
this._popupBehavior = null;
}
}
};
The code above has two parts. The first part of the code is used to define the constructor function for the PopupHelp behavior. This is a factory method which returns an instance of a PopupHelp behavior:
MyACTControls.PopupHelpBehavior = function (element) {
}
The second part of the code modified the prototype for the PopupHelp behavior:
MyACTControls.PopupHelpBehavior.prototype = {
}
Any code which is particular to a single instance of the PopupHelp behavior should be placed in the constructor function. For example, the default value of the _helpText field is assigned in the constructor function:
this._helpText = "Help Text";
Any code which is shared among all instances of the PopupHelp behavior should be added to the PopupHelp behavior’s prototype. For example, the public HelpText property is added to the prototype:
get_HelpText: function () {
return this._helpText;
},
set_HelpText: function (value) {
if (this._HelpText != value) {
this._helpText = value;
this._ensurePopup();
this._popupDiv.innerHTML = value;
this.raisePropertyChanged("Text")
}
},
Registering a JavaScript Class
After you create the PopupHelp behavior, you must register the behavior as a class by using the Microsoft Ajax registerClass() method like this:
MyACTControls.PopupHelpBehavior.registerClass('MyACTControls.PopupHelpBehavior', Sys.Extended.UI.BehaviorBase);
This call to registerClass() registers PopupHelp behavior as a class which derives from the base Sys.Extended.UI.BehaviorBase class. Like the ExtenderControlBase class on the server side, the BehaviorBase class on the client side contains method used by every behavior.
The documentation for the BehaviorBase class can be found here:
http://msdn.microsoft.com/en-us/library/bb311020.aspx
The most important methods and properties of the BehaviorBase class are the following:
dispose() – Use this method to clean up all resources used by your behavior. In the case of the PopupHelp behavior, the dispose() method is used to remote the event handlers created by the behavior and disposed the Popup behavior.
get_element() -- Use this property to get the DOM element associated with the behavior. In other words, the DOM element which the behavior extends.
get_id() – Use this property to the ID of the current behavior.
initialize() – Use this method to initialize the behavior. This method is called after all of the properties are set by the $create() method.
Creating Debug and Release Scripts
You might have noticed that the PopupHelp behavior uses two scripts named PopupHelpBehavior.js and PopupHelpBehavior.debug.js. However, you never create these two scripts. Instead, you only create a single script named PopupHelpBehavior.pre.js.
The pre in PopupHelpBehavior.pre.js stands for preprocessor. When you build the Ajax Control Toolkit (or the sample Visual Studio Solution at the end of this blog entry), a build task named JSBuild generates the PopupHelpBehavior.js release script and PopupHelpBehavior.debug.js debug script automatically.
The JSBuild preprocessor supports the following directives:
#IF
#ELSE
#ENDIF
#INCLUDE
#LOCALIZE
#DEFINE
#UNDEFINE
The preprocessor directives are used to mark code which should only appear in the debug version of the script. The directives are used extensively in the Microsoft Ajax Library. For example, the Microsoft Ajax Library Array.contains() method is created like this:
$type.contains = function Array$contains(array, item) {
//#if DEBUG
var e = Function._validateParams(arguments, [
{name: "array", type: Array, elementMayBeNull: true},
{name: "item", mayBeNull: true}
]);
if (e) throw e;
//#endif
return (indexOf(array, item) >= 0);
}
Notice that you add each of the preprocessor directives inside a JavaScript comment. The comment prevents Visual Studio from getting confused with its Intellisense.
The release version, but not the debug version, of the PopupHelpBehavior script is also minified automatically by the Microsoft Ajax Minifier. The minifier is invoked by a build step in the project file.
Conclusion
The goal of this blog entry was to explain how you can create custom AJAX Control Toolkit controls. In the first part of this blog entry, you learned how to create the server-side portion of an Ajax Control Toolkit control. You learned how to derive a new control from the ExtenderControlBase class and decorate its properties with the necessary attributes.
Next, in the second part of this blog entry, you learned how to create the client-side portion of an Ajax Control Toolkit control by creating a client-side behavior with JavaScript. You learned how to use the methods of the Microsoft Ajax Library to extend your client behavior from the BehaviorBase class.
Download the Custom ACT Starter Solution