default heading
Starting at $4.95/month - ASP.NET Hosting! Get Up to 10 FREE MS SQL & MySQL Databases
See Why Thousands of Customers Have Switched to WinHost! Host UNLIMITED Web Sites with Reliable Windows/ASP.NET Web Hosting. Click Here and Sign Up Today!
Google


 

Article Rating:  4.25

A Button Enabled Extender...


ASP.NET AJAX extenders are really functional; they use a managed JavaScript development approach.
By: Brian Mains Spacer Date: October 25, 2009Spacer Spacer Spacer Printer Friendly Version

Introduction

ASP.NET AJAX extenders are really functional; not only do they use a managed JavaScript development approach; they spice up the UI like Emeril in the kitchen. The following extender is an example of the ASP.NET AJAX approach to developing extenders like the one in this example.

The extender we are going to build is an extender that toggles the enabled status of a control based on the button clicks. The extender maps to both a button and a receiver control; the receiver changes its disabled status based on the clicks of the button. The first click disabled the targeted control; the second click re-enables it. This trend continues on for the life of the page.

However, the extender also works in another mode. Rather than solely targeting another control always, the control can simply target itself, disabling itself from clicking the button twice and prevent double-clicks.

AJAX Control Toolkit Introduction

The subject of AJAX control toolkit custom development, as well as ASP.NET AJAX development, is a long complicated task. This article alone can't demystify the process completely; I'll briefly cover the concept of script development related to extender controls, which use the AJAX control toolkit. However, I would highly recommend reading up on the general process of developing custom AJAX component if you aren't familiar with the subject. There are some introductory tutorials on the http://asp.net web site.

Custom AJAX development makes use of a server component and client component that work in-tandem with each other. In order for the server to work with the client, it requires describing the component using a ScriptDescriptor object, which has several derivatives. The script describes the client properties and events and supply initial values or event handlers to them.

To describe components, the AJAX control toolkit uses customized attributes that describe the component's properties, methods, and events, which does make it easy to develop and use. At runtime, the base class component extracts this metadata using reflection, creating the script descriptor in a more automated way.

On the client-side, the ASP.NET AJAX framework has added many new features, while reusing what is already there in the JavaScript language. The approach is to make JavaScript code look more like the .NET framework, setting up namespaces, classes, interfaces, properties, events, and other constructs. The client component is like a hybrid in that it makes use of all the existing JavaScript notations, while adding new capabilities and new features to existing JavaScript objects.

The client-side framework has a built-in lifecycle, which the client component can make use of. It also includes two important lifecycle methods: initialize and dispose. Obviously, initialize fires at the beginning of the lifecycle, and dispose at the end. We'll take a look at these lifecycle methods in code. To that end, it's not worth discussing much more without seeing it first-hand.

Server Extender

The server extender's definition looks like the following skeleton below. Note the various uses of attributes.

 [assembly:System.Web.UI.WebResource(
    "Nucleo.Web.ButtonControls.ButtonEnabledExtender.js", 
    "text/javascript")]
 namespace Nucleo.Web.ButtonControls
 {
     [
     TargetControlType(typeof(IButtonControl)),
     ClientScriptResource("Nucleo.Web.ButtonControls.ButtonEnabledExtender",
                "Nucleo.Web.ButtonControls.ButtonEnabledExtender.js")
     ]
     public class ButtonEnabledExtender : ExtenderControlBase
     {
         ..
     }
 }

In the AJAX Control Toolkit API, there are two main base classes for developing custom controls: ExtenderControlBase and ScriptControlBase. The ExtenderControlBase class is used in this example because we're building an extender. For all extenders, these controls have a TargetControlID property; this property is linking the extender to the server control (and eventually underlying HTML element when it is rendered) that the extender control extends through JavaScript. The TargetControlType attribute is used to limit what type of controls that can be.

Each server component has a server class (.cs or .vb) file and a JavaScript (.js) file. The WebResource and ClientScriptResource attributes map the server class to the javascript file and link the two together. The deployment model with custom AJAX controls is that JavaScript files are marked as embedded resources in Visual Studio to the client class that it represents. These files are later extracted using the framework components; no work is necessary for this on your part.

Moving along, to see how easy the describing process is, the following properties and event handlers are illustrated below. I'll start by illustrating the properties.

 [
 ExtenderControlProperty,
 ClientPropertyName("isEnabledInitially")
 ]
 public bool IsEnabledInitially
 {
     get { return base.GetPropertyValue<bool>("IsEnabledInitially", true); }
     set { base.SetPropertyValue<bool>("IsEnabledInitially", value); }
 }
 
 [
 ExtenderControlProperty,
 ClientPropertyName("receiverControlID"),
 IDReferenceProperty(typeof(Control))
 ]
 public string ReceiverControlID
 {
     get { return base.GetPropertyValue<string>("ReceiverControlID", null); }
     set { base.SetPropertyValue<string>("ReceiverControlID", value); }
 }

The ExtenderControlProperty attribute maps the server control properties to a property in the client component; otherwise, a client-side error will be thrown if there is an invalid mapping. The ClientPropertyName attribute specified the name of the property on the client side if different than the property on the server-side. At runtime, the server will push down the initial values it defines to the client component.

One other important attribute to note is if you have any properties that reference another control (for both extenders and controls), the IDReferenceProperty allows for proper retrieval of the ClientID at description time.

For this extender, only one event is needed.

 [
 ExtenderControlEvent,
 ClientPropertyName("enabledStatusChanged")
 ]
 public string OnClientEnabledStatusChanged
 {
     get { return base.GetPropertyValue<string>("OnClientEnabledStatusChanged", 
             null); }
     set { base.SetPropertyValue<string>("OnClientEnabledStatusChanged", value); }
 }

This event allows an ASPX page to provide the name of a JavaScript method as an event handler, which gets called whenever an event is raised on the client side (more on this soon). At runtime, the name of this method has to be a valid JavaScript routine name, which will get called when the event is fired from the client component.

One other note: the GetPropertyValue and SetPropertyValue generic methods read/write data to/from ViewState; they are simply helper methods for this process. That's all from the server component. That is all the code that's needed at the moment to setup our ButtonEnabledExtender control. It's that simple; the only item remaining is the client component.

Client Component

As I mentioned before, classes go through a registration process that we'll see later on. Registration of a class allows for inheritance; in this example, the client base class that is used with ExtenderControlBase objects is the AjaxControlToolkit.BehaviorBase class. This class inherits from Sys.UI.Behavior defined in the ASP.NET AJAX library, which itself inherits from Sys.Component.

The Sys.Component base class defines some properties that can be used in our client development, most notably:

  • id - Gets the ID of the component; although the ID could be something other than the server control's client ID, the two are usually the same value. This is accessible through get_id().
  • events - Gets the events of the components; events aren't defined as you see in the .NET framework; rather, the JavaScript library uses a dictionary-based approach to define events. The object representing events is an object of type EventHandlerList, and we'll see how to use it later.
  • raisePropertyChanged - This method, used in conjunction with the INotifyPropertyChanged interface, fires an event, notifying the consumers of the class that the property value changed.

The AJAX Control Toolkit's BehaviorBase class provides additional properties. The clientState property, accessible through get_clientState and set_clientState, represents the value stored in a hidden field, which represents the state of the control (similar to a control's viewstate). Because AJAX components don't have ViewState on the client side (because ViewState is strictly a server mechanism), a hidden field is used as a ViewState mechanism on the client.

Let's get into the code. The shell of the component looks like the following:

 [
 ExtenderControlEvent,
 ClientPropertyName("enabledStatusChanged")
 ]
 public string OnClientEnabledStatusChanged
 {
     get { return base.GetPropertyValue<string>("OnClientEnabledStatusChanged", 
             null); }
     set { base.SetPropertyValue<string>("OnClientEnabledStatusChanged", value); }
 }

In the example above, every script registers the namespace it belongs to. Don't worry if you define this multiple times amongst your various scripts; the AJAX framework doesn't have issues with this. The next definition is the constructor that defines a single parameter: the underlying HTML element being extended. This element is passed to the base class and will be accessible through get_element(). All variables used in the script are also defined in the constructor.

At the end of the script, always register your scripts using the fully quantified name of the class, along with the registerClass method. The registerClass method registers with the system that the ButtonEnabledExtender type is a class that inherits from AjaxControlToolkit.BehaviorBase.

The next section, the prototype, defines the definition of the class. This is where the properties, events and handlers, methods, and lifecycle methods are defined. Remember the properties and events exposed in the server component? Those property values are mapped to the following properties/events below.

 get_isEnabledInitially : function()
 {
     return this._isEnabledInitially;
 },
 
 set_isEnabledInitially : function(value)
 {
     if (this._isEnabledInitially != value)
     {
         this._isEnabledInitially = value;
         this.raisePropertyChanged("isEnabledInitially");
     }
 },
 
 get_receiverControlID : function()
 {
     return this._receiverControlID;
 },
 
 set_receiverControlID : function(value)
 {
     if (this._receiverControlID != value)
     {
         this._receiverControlID = value;
         this.raisePropertyChanged("receiverControlID");
     }
 },
 
 add_enabledStatusChanged : function(handler)
 {
     this.get_events().addHandler('enabledStatusChanged', handler);
 },
 
 remove_enabledStatusChanged : function(handler)
 {
     this.get_events().removeHandler('enabledStatusChanged', handler);
 },
 
 raise_enabledStatusChanged : function()
 {
     var eventHandler = this.get_events().getHandler('enabledStatusChanged');
     
     if (eventHandler != null)
         eventHandler(this, Sys.EventArgs.Empty);
 }

When the application runs, the IsEnabledInitially and the ReceiverControlID properties on the server side are passed to the set_isEnabledInitially and set_receiverControlID operations on the client-side (because the server component needs to set the values). This happens through a $create static method, but $create is out of scope of this article.

I mentioned before that there are two lifecycle methods. The initialize method is a key method in this script. Because it's pretty long, I'm going to break it up into sections. First, the method calls the initialize method on the base class, as you would using MyBase.New() in VB.NET.

 initialize : function()
 {
     Nucleo.Web.ButtonControls.ButtonEnabledExtender.callBaseMethod(this, "initialize");
 
     .
     .
 }

Remember previously I said that the extender works in two modes. If the ReceiverControlID property is not null, then the extender toggles the receiving control's disabled status. The first statement of the initalize script processes toggling requests. Normally, the button control contains a script to post back to the server. This needs prevented below; for the time being, I'm returning false and clearing the onclick code.

 if (this.get_receiverControlID() != null)
 {
     if (this.get_element().onclick != null)
         this.get_element().onclick = "return false";
 }

The button instance has a click handler, which we want to map to. The click action requires the use of a delegate, which is the purpose of Function.createDelegate below. The delegate is a pointer that maps the JavaScript method clickCallback to the click event of the button. I'll discuss clickCallback in a moment; for now, it simply handles the click event.

this._clickHandler = Function.createDelegate(this, this.clickCallback); $addHandler(this.get_element(), 'click', this._clickHandler);

Think of a delegate as an intermediary; it simply points the HTML button click event to the clickCallback method. The last step of the intialize method is to retrieve and compare the value stored in client state. If the extender is protecting double-clicking, no processing of client state is needed. Otherwise, client state is used to store the current disabled status of the button.

If client state is present, the current value is passed to the changeStatus method; otherwise, the current status used is pulled from the isEnabledInitially property. Passing in a boolean value signifies that the button should be setting a direct value, and not toggle the value.

 if (this.get_receiverControlID() != null)
 {        
     var clientState = Nucleo.Web.ButtonControls.ButtonEnabledExtender.callBaseMethod(this, "get_ClientState");
     if (clientState != null)
         this.changeStatus(clientState);
     else
         this.changeStatus(this.get_isEnabledInitially());
 }

Another method also uses the changeStatus method: clickCallback. This event fires when the button is clicked, passing in a null value to changeStatus. Null signifies that the button should toggled.

 clickCallback : function(domEvent)
 {
     this.changeStatus(null);
 }

The changeStatus method is where the core of the work is performed in this script. Depending on the value passed into the method determines the action to take. If null is passed in, then the enabled status is toggled. If a value is passed in, that value is assigned to the control.

 changeStatus : function(isDisabled)
 {
     //If no receiver is defined, disable the button after changing the status
     if (this.get_receiverControlID() == null)
     {
         this.get_element().disabled = false;
         return;
     }
     
     var receiverElement = $get(this.get_receiverControlID());
     if (receiverElement.disabled === "undefined")
         throw new Error.invalidOperation("The receiver control doesn't support disabling");        
     
     //If null, toggle the current disabled state
     if (isDisabled == null)
     {
         receiverElement.disabled = !receiverElement.disabled;
         this.raise_enabledStatusChanged();
     }
     else
         receiverElement.disabled = isDisabled;
     
     Nucleo.Web.ButtonControls.ButtonEnabledExtender.callBaseMethod(this, 'set_ClientState',
                [receiverElement.disabled]);
 }
,

Initially, if the receiver control is null (which means the extender is preventing double clicks), the button is disabled and processing stops. Otherwise, the receiver control is retrieved using the $get helper method (simply a shortcut to document.getElementById). Not every element has a disabled property necessarily, so an error is thrown if it doesn't.

Notice that the enabledStatusChanged event is fired only when the toggle happens; this occurs when the parameter passed in is null. Otherwise, a direct assignment occurs. The value assigned to the disabled field is stored in client state by using the callBaseMethod method to set the client state.

Consumer

I've developed a simplified test page that consumes this new extender. There are two tests; the first test targets a button and sets up the text box as the receiver of the enabled status toggle. Every time the button click, disabled status flips its state accordingly.

 <asp:TextBox ID="txt1" runat="server" />
 <asp:Button ID="btn1" runat="server" Text="First Test" CausesValidation="false" UseSubmitBehavior="false" />
 <n:ButtonEnabledExtender ID="ext1" runat="server" TargetControlID="btn1" ReceiverControlID="txt1" />

This scenario produces the screenshot below. I produced these screenshots with the tool SnagIt; this is a very popular and very productive tool.

 <img src="images/Extender1.png" alt="" />

In the second scenario, the extender doesn't target a textbox as a receiver; it simply defines the button as the target. Notice the ReceiverControlID is not specified.

 <asp:TextBox ID="txt2" runat="server" />
 <asp:Button ID="btn2" runat="server" Text="Second Test" />
 <n:ButtonEnabledExtender ID="ext2" runat="server" TargetControlID="btn2" />

Rather than toggling the textbox enabled status, this test prevents a double-click, as shown below:

In both cases, the script changes the state of some UI element; in the first case, it works with the textbox control, whereas the latter case the button itself. All of this happens on the client side and works seamlessly with JavaScript. Deployment is seamless also because this custom script is stored as an embedded resource in the DLL, and retrieved at runtime. No code is necessary to perform this action.

A Word about Custom Extenders

Custom extenders, like custom controls, will have more volatility toward change on the server-side than on the client side. When developing components using the ASP.NET AJAX framework (not shown here) or the AJAXControlToolkit, there is some differences between the two approaches. But in the JavaScript client components, there isn't. It's the same approach, and it doesn't really change. In the future, as new additions come out from Microsoft, it still won't change as much as the server-code can, which is a good thing.

Developing these scripts by hand can be more difficult though, simply because of the sheer volume of code. I use CodeSmith Studio to generate my ASP.NET AJAX code, which I custom developed a script for my need. It wasn't overtly difficult to setup, and the benefits paid off immediately.

Once you learn the constructs to ASP.NET AJAX, it stays the same, so the major challenge is the initial learning curve. After that, the scripting portion isn't that difficult at all; most of the difficulty comes from figuring out the complex processing logic and state management aspects.

Conclusion

We've used AJAX control toolkit components to create a custom extender, which enables or disables a control based on a button click. This component uses the AJAX control toolkit server and client base classes, making use of the some of the new objects now available in the client library.

 
Please Rate This Article Poor           Excellent
 
 
 
 
 
   © Copyright 2002-2010 DotNetJohn.com LLC
Terms of Use Privacy Policy