AspAlliance.com LogoASPAlliance: Articles, reviews, and samples for .NET Developers
URL:
http://aspalliance.com/articleViewer.aspx?aId=1694&pId=-1
AJAX Extender Example: Button Enabled Changes
page
by Brian Mains
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 32388/ 81

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 and 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, the control can simply target itself, disabling itself from clicking the button twice and preventing 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 cannot demystify the process completely; I will 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 are not familiar with the subject. There are some introductory tutorials on the official web site of ASP.NET.

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 supplies 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 will take a look at these lifecycle methods in code. To that end, it is not worth discussing much more without seeing it first-hand.

Server Extender

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

Listing 1

[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 are 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 the controls 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 how easy the describing process is, the following properties and event handlers are illustrated below.  I will start by illustrating the properties.

Listing 2

[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.

Listing 3

[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 is all from the server component. That is all the code that is needed at the moment to setup our ButtonEnabledExtender control.  It is that simple; the only item remaining is the client component.

Client Component

As I mentioned before, classes go through a registration process that we will 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 are not 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 will 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 do not 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 us get into the code. The shell of the component looks like the following.

Listing 4

Type.registerNamespace("Nucleo.Web.ButtonControls");
//Constructor
Nucleo.Web.ButtonControls.ButtonEnabledExtender = function(associatedElement)
{
  Nucleo.Web.ButtonControls.ButtonEnabledExtender.initializeBase(this,
    [associatedElement]);
 
  this._isEnabledInitially = null;
  this._receiverControlID = null;
 
  this._clickHandler = null;
}
 
//Class definition
Nucleo.Web.ButtonControls.ButtonEnabledExtender.prototype =
{
  …
}
 
//Descriptor
Nucleo.Web.ButtonControls.ButtonEnabledExtender.descriptor =
{
  properties: [..], events: [..]
}
 
//Class registration, registerClass's second parameter is the base class
Nucleo.Web.ButtonControls.ButtonEnabledExtender.registerClass
  ('Nucleo.Web.ButtonControls.ButtonEnabledExtender',
  AjaxControlToolkit.BehaviorBase);
 
if (typeof(Sys) !=  = 'undefined')
  Sys.Application.notifyScriptLoaded();

In the example above, every script registers the namespace it belongs to. Do not worry if you define this multiple times amongst your various scripts; the AJAX framework does not 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.

Listing 5

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 the 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 is pretty long, I am 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.

Listing 6

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

Remember that previously I said 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 initialize 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 am returning false and clearing the onclick code.

Listing 7

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 will discuss clickCallback in a moment; for now, it simply handles the click event.

Listing 8

this._clickHandler = Function.createDelegate(thisthis.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 initialize 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.

Listing 9

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 be toggled.

Listing 10

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.

Listing 11

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 does not.

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 have 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 is clicked, the disabled status flips its state accordingly.

Listing 12

<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.

Figure 1

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

Listing 13

<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.

Figure 2

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 are some differences between the two approaches. But in the JavaScript client components, there is not.  It is the same approach, and it does not really change. In the future, as new additions come out from Microsoft, it still will not 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 was not 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 is not that difficult at all; most of the difficulty comes from figuring out the complex processing logic and state management aspects.

Conclusion

We have 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 some of the new objects now available in the client library.



©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-04-25 11:56:27 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search