ASP.NET 3.5: Create AJAX Extender Controls using the ExtenderControl base class
As a followup to the “Create AJAX Server Controls using the ScriptControl base class” post I wrote a couple weeks ago, I’ve decided to write on how to create ASP.NET AJAX Extender Controls using the ExtenderControl base class in ASP.NET 3.5. There is already an article titled “Adding ASP.NET AJAX Client Behaviors to Web Server Controls” in MSDN, but it doesn’t show all the code involved in creating Extender Controls, especially the JavaScript code.
What is an Extender Control?
First what exactly is an Extender Control? An Extender Control is basically a non-visual control that adds some sort of AJAX behavior to another control within the page. The majority of the controls within the AjaxControlToolkit are built as Extender Controls. It is a common misconception that you can only create Extender Controls by using the base classes within the AjaxControlToolkit, but in fact ASP.NET 3.5 contains the required ExtenderControl base class to create Extender Controls.
The base class I’m referring to in this article is the System.Web.UI.ExtenderControl base class located within the System.Web.Extensions.dll assembly.
According to MSDN the ExtenderControl class is:
The ExtenderControl class enables you to programmatically add AJAX functionality to an ASP.NET server control. The ExtenderControl inherits from the Control class and implements the IExtenderControl interface. The Control class defines the properties, methods, and events that are shared by all ASP.NET server controls. The IExtenderControl interface is an abstract class, which you cannot instantiate directly. Instead, you create a derived type.
Basics of created Extender Controls that Inherit from ExtenderControl
When first creating your custom extender control you need to inherit from the ExtenderControl base class. The first thing you’ll see is the ExtenderControl requires you to implement to methods: GetScriptDescriptors and GetScriptReferences. The GetScriptDescriptors method is used to get an IEnumerable collection of ScriptDescriptor objects that basically define any of the controls client-side AJAX properties. The GetScriptReferences method is used to get an IEnumerable collection of ECMAScript (JavaScript) files that will be loaded on the client-side once the page loads; this is used to basically define any client-side scripts the control will require to run on the client.
[TargetControlType(typeof(Control))]
public class FocusExtender : System.Web.UI.ExtenderControl
{
protected override System.Collections.Generic.IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl)
{
throw new NotImplementedException();
}
protected override System.Collections.Generic.IEnumerable<ScriptReference> GetScriptReferences()
{
throw new NotImplementedException();
}
}
You may have noticed the TargetControlType attribute applied to the FocusExtender control; this attribute is required, and is used to define the type of control that the extender explicitly works with. In the case of this example, I’m setting it to work with any thing that inherits from the Control class, which is all server controls.
Define ScriptReferences
In the GetScriptReferences method, return an IEnumerable collection of the ScriptReference objects to include in the page on the client-side.
Here’s a basic example of setting up a ScriptReference that includes a script resource that is embedded within the assembly that FocusExtender is contained in:
protected override System.Collections.Generic.IEnumerable<ScriptReference> GetScriptReferences()
{
yield return new ScriptReference("CustomExtenderControl.FocusExtender.js", "CustomExtenderControl");;
}
When inheriting from ExtenderControl you’ll always have at least one ScriptReference, because you need to reference the script that creates the client-side JavaScript representation of your extender control.
Define ScriptDescriptors
In the GetScriptDescriptors method, return an IEnumerable collection of ScriptDescriptor objects that define what properties will be passed down to the client-side JavaScript representation of your control.
The only ScriptDescriptor you need to return in the IEnumerable collection is a ScriptBehaviorDescriptor instance that contains all the descriptors for your extender control.
Here’s a basic example of returning a ScriptBehaviorDescriptor without any properties being defined:
protected override System.Collections.Generic.IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl)
{
ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor(this.GetType().FullName, targetControl.ClientID)
yield return descriptor;
}
What ECMAScript (JavaScript) code is needed?
There is a basic block of JavaScript code that you’ll need to include that will define the client-side JavaScript representation of your control. This basic block of code is the same base code that you’ll start with when creating all extender controls.
Here’s the basic JavaScript that will define the client-side JavaScript representation of the FocusExtender
control we’re using in this example. One thing to remember is that the namespaces and object name in the JavaScript file need to be the same as they are in the server-side .NET code.
In case you’re wonderin, there is a way to make the client-side and server-side namespaces different, but that’s beyond the scope of this article.
Type.registerNamespace("CustomExtenderControl");
CustomExtenderControl.FocusExtender = function(element) {
CustomExtenderControl.FocusExtender.initializeBase(this, [element]);
};
CustomExtenderControl.FocusExtender.prototype = {
initialize:function(){
CustomExtenderControl.FocusExtender.callBaseMethod(this, "initialize");
},
dispose:function(){
CustomExtenderControl.FocusExtender.callBaseMethod(this, "dispose");
}
};
CustomExtenderControl.FocusExtender.registerClass("CustomExtenderControl.FocusExtender", Sys.UI.Behavior);
Sys.Application.notifyScriptLoaded();
Also, in order for the ExtenderControl to correctly load the “CustomExtenderControl.FocusExtender.js” script reference that we’re using in this example, we must not forget to define the assemblies embedded resource as a Web Resource, like so:
[assembly: System.Web.UI.WebResource("CustomExtenderControl.FocusExtender.js", "text/javascript")]
Let’s Add a Little “Rich” AJAX Functionality to the Extender Control
Now that we have a basic ExtenderControl created, we can start adding out “rich” AJAX functionality that we will use to extend our target server control in the page.
In this article, I’m going to keep things extemely simple, and we’re going to do the following:
- Pass HighlightCssClass and NoHighlightCssClass properties down to the client, that will determin the CSS class's used to highlight out server control (in this article a TextBox).
Add the HighlightCssClass and NoHighlightCssClass Properties
public string HighlightCssClass { get; set; }
public string NoHighlightCssClass { get; set; }
Add the ScriptDescriptor for the HighlightCssClass and NoHighlightCssClass Properties
Just modify the GetScriptDescriptor method above to look like the following:
protected override System.Collections.Generic.IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl)
{
ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor(this.GetType().FullName, targetControl.ClientID)
descriptor.AddProperty("HighlightCssClass", this.HighlightCssClass);
descriptor.AddProperty("NoHighlightCssClass", this.NoHighlightCssClass);
yield return descriptor;
}
Add the Client-Side Name Property that will receive the value
To do this we need to add a “private” like variable for each CSS property and property accessor methods for each to our JavaScript.
Here’s the above JavaScript file with the “private” variables and property accessors added for the CSS properties:
Type.registerNamespace("CustomExtenderControl");
CustomExtenderControl.FocusExtender = function(element) {
CustomExtenderControl.FocusExtender.initializeBase(this, [element]);
this._HighlightCssClass = null;
this._NoHighlightCssClass = null;
};
CustomExtenderControl.FocusExtender.prototype = {
initialize:function(){
CustomExtenderControl.FocusExtender.callBaseMethod(this, "initialize");
},
dispose:function(){
CustomExtenderControl.FocusExtender.callBaseMethod(this, "dispose");
},
get_HighlightCssClass:function() {
return this._HighlightCssClass;
},
set_HighlightCssClass:function(v) {
this._HighlightCssClass = v;
},
get_NoHighlightCssClass:function() {
return this._NoHighlightCssClass;
},
set_NoHighlightCssClass:function(v) {
this._NoHighlightCssClass = v;
}
};
CustomExtenderControl.FocusExtender.registerClass("CustomExtenderControl.FocusExtender", Sys.UI.Behavior);
Sys.Application.notifyScriptLoaded();
Set the TargetControls CSS according to the Focus
In this example we’re going to attach event handlers to the TargetControl’s focus and blur events that will change the controls CSS.
Here’s the above JavaScript code with the event handlers added:
Type.registerNamespace("CustomExtenderControl");
CustomExtenderControl.FocusExtender = function(element) {
CustomExtenderControl.FocusExtender.initializeBase(this, [element]);
this._HighlightCssClass = null;
this._NoHighlightCssClass = null;
this._focusHandler = Function.createDelegate(this, this._onFocus);
this._blurHandler = Function.createDelegate(this, this._onBlur);
};
CustomExtenderControl.FocusExtender.prototype = {
initialize:function(){
CustomExtenderControl.FocusExtender.callBaseMethod(this, "initialize");
var targetElement = this.get_element();
$addHandler(targetElement, "focus", this._focusHandler);
$addHandler(targetElement, "blur", this._blurHandler);
},
dispose:function(){
var targetElement = this.get_element();
$removeHandler(targetElement, "focus", this._focusHandler);
$removeHandler(targetElement, "blur", this._blurHandler);
CustomExtenderControl.FocusExtender.callBaseMethod(this, "dispose");
},
get_HighlightCssClass:function() {
return this._HighlightCssClass;
},
set_HighlightCssClass:function(v) {
this._HighlightCssClass = v;
},
get_NoHighlightCssClass:function() {
return this._NoHighlightCssClass;
},
set_NoHighlightCssClass:function(v) {
this._NoHighlightCssClass = v;
},
//Event Handler Methods
_onFocus:function(eventArgs) {
var targetElement = this.get_element();
if (targetElement != null)
{
targetElement.className = this.get_HighlightCssClass();
}
},
_onBlur:function(eventArgs) {
var targetElement = this.get_element();
if (targetElement != null)
{
targetElement.className = this.get_NoHighlightCssClass();
}
}
};
CustomExtenderControl.FocusExtender.registerClass("CustomExtenderControl.FocusExtender", Sys.UI.Behavior);
Sys.Application.notifyScriptLoaded();
Tips and Tricks
Tip #1: Simplify Creating ExtenderControls using a custom ExtenderControlBase class
One trick that I’ve learned, that proves useful when creating custom Extender Controls, is to create your own base class that inherits from ExtenderControl and extends it to use some custom attributes to make adding ScriptReferences and ScriptDescriptors easier. This is the same trick that I learned from creating ScriptControls.
Here’s what the final server-side code for the FocusExtender object would look like when using custom attributes and our very own ExtenderControl base class to add ScriptReferences and ScriptDescriptors:
[TargetControlType(typeof(Control)),
ScriptReference("CustomExtenderControl.FocusExtender.js", "CustomExtenderControl")]
public class FocusExtender : ExtenderControlBase
{
[ExtenderControlProperty]
public string HighlightCssClass { get; set; }
[ExtenderControlProperty]
public string NoHighlightCssClass { get; set; }
}
As you can see, the new code for the FocusExtender using this ExtenderControlBase class with custom attributes is much cleaner and easier to read.
Here’s the complete code for the ExtenderControlBase, ScriptReferenceAttribute and ExtenderControlPropertyAttribute objects:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Web.UI;
public class ExtenderControlBase : ExtenderControl
{
protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl)
{
ScriptControlDescriptor descriptor = new ScriptControlDescriptor(this.GetType().FullName, targetControl.ClientID);
// Add all the ExtenderControls Client-Side Object Properties
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(this);
foreach (PropertyDescriptor prop in props)
{
ExtenderControlPropertyAttribute propAttr = prop.Attributes[typeof(ExtenderControlPropertyAttribute)] as ExtenderControlPropertyAttribute;
if (propAttr != null)
{
object value = prop.GetValue(this);
string name = (propAttr.Name != null) ? propAttr.Name : prop.Name;
if (value != null)
{
descriptor.AddProperty(name, value);
}
}
}
yield return descriptor;
}
protected override IEnumerable<ScriptReference> GetScriptReferences()
{
// Add all the ExtenderControls Client-Side JavaScript References
object[] scriptReferences = Attribute.GetCustomAttributes(this.GetType(), typeof(ScriptReferenceAttribute), false);
foreach (ScriptReferenceAttribute r in scriptReferences)
{
yield return r.GetScriptReference();
}
}
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class ExtenderControlPropertyAttribute : Attribute
{
public ExtenderControlPropertyAttribute() { }
public ExtenderControlPropertyAttribute(string name)
{
this.Name = name;
}
public string Name { get; set; }
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class ScriptReferenceAttribute : Attribute
{
public ScriptReferenceAttribute(string path)
{
this.Path = path;
}
public ScriptReferenceAttribute(string name, string assembly)
{
this.Name = name;
this.Assembly = assembly;
}
private string _path = null;
public string Path
{
get { return _path; }
set { _path = value; }
}
private string _name = null;
public string Name
{
get { return _name; }
set { _name = value; }
}
private string _assembly = null;
public string Assembly
{
get { return _assembly; }
set { _assembly = value; }
}
public ScriptReference GetScriptReference()
{
ScriptReference r = null;
if (this.Path == null)
{
r = new ScriptReference(this.Name, this.Assembly);
}
else
{
r = new ScriptReference(this.Path);
}
return r;
}
}
Tip #2: Access ExtenderControl via JavaScript
Another trick that I figured out allows you to be able to reference the custom extender control from within JavaScript. The issue this solves is the fact that the ExtenderControl base class doesn’t send the control ID down to the client, so there is no way to get a reference to it for manipulation via JavaScript.
To do this all we need to do is add a property named “id” to the ScriptBehaviorDescriptor within the GetScriptDescriptor method. Here’s an example of doing this:
protected override System.Collections.Generic.IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl)
{
ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor(this.GetType().FullName, targetControl.ClientID);
descriptor.AddProperty("id", this.ClientID);
descriptor.AddProperty("HighlightCssClass", this.HighlightCssClass);
descriptor.AddProperty("NoHighlightCssClass", this.NoHighlightCssClass);
yield return descriptor;
}
Now with the following FocusExtender control in the page:
<cec:FocusExtender runat="server" id="FocusExtender1"
TargetControlID="TextBox1"
HighlightCssClass="Highlight" NoHighlightCssClass="NoHightlight">
</cec:FocusExtender>
We can access it from JavaScript like so:
<input type="button" value="Show HighlightCssClass" onclick="ShowHighlightCssClass();" />
<script type="text/javascript">
function ShowHighlightCssClass()
{
var focusExtender = $find("<%=FocusExtender1.ClientID%>");
alert(focusExtender.get_HighlightCssClass());
}
</script>
It’s kind of strange that the ExtenderControl base class doesn’t automatically link it up to be able to access it from within JavaScript, but at least it’s pretty simple to add.
Conclusion
As you can see it’s fairly simple to get started creating your own custom extender controls that inherit from the ExtenderControl base class, especially if you use the above ExtenderControlBase class along with it’s ExtenderControlPropertyAttribute and ScriptReferenceAttribute attributes. It’s also very simple to enable the ability to access the ExtenderControl from within JavaScript to enable much richer AJAX functionality to be built.
Download Full Source Code: CreateAJAXExtenderControlUsingExtenderControlBaseClass.zip (21.36 kb)