Building jQuery and ASP.Net AJAX Enabled Controls, The jQueryCollapsiblePanelExtender Part 1 Client Control

by mosessaur| 26 October 2008| 11 Comments

At the end of the last month (September 2008) Microsoft announced that it will be shipping jQuery with Visual Studio going forward. It was a great news for all ASP.NET developers who are jQuery fans as well as jQuery and ASP.NET AJAX folks like me. Before and after that, there were many posts made around the same subject. It will take a full post to list all these posts. But it would be easy for me to list ASP.NET jQuery heroes around such Rick Strahl, Dave Ward and Matt Berseth. There are many other heroes around such as Bill Beckelman who made a great collection of ASP.NET with jQuery Demos.

For specific posts around ASP.NET AJAX and jQuery I recommend to refer to the following posts by Dave Ward's blog:

Introduction:

We all heard about and maybe worked with ASP.NET AjaxControlToolkit! A set of wonderful controls, but they are heavy, maybe not all of them but many of of them. Beside they require lots of script files which increase the response size.

I was thinking of building jQuery UI Widgets to clone some of AjaxControlToolkit controls. And after Microsoft announcement about jQuery I had another idea! I always liked how ASP.NET AJAX component model, both client and server models. It is easy to build ASP.NET AJAX Enabled Controls especially for ASP.NET control developers. ASP.NET AJAX client and server architecture really ease the development ASP.NET AJAX Enabled Controls and also the core ASP.NET AJAX client library has no massive performance issues, and it is widely used.

On the other hand, I loved the ease of using jQuery as well, how fancy it provides UI Effects and manipulation of DOM. Many many things I wished to have in ASP.NET AJAX were exist in jQuery. So I though of building an ASP.NET AJAX Enabled Control to clone the existing AjaxControlToolkit's control CollapsiblePanelExtender. And use ASP.NET AJAX client and server component model while using jQuery for UI Effects. In this case it would be collapsing (Slide Up) and expanding (Slide Down). [View Demo]

Prerequisites:

You must be familiar with both ASP.NET AJAX and jQuery before you continue on this post. I assume that you already know how to build ASP.NET AJAX Enabled Controls (Controls & Extenders). I assume that you familiar with ASP.NET AJAX client library.

Building the Client Component:

As this going to be based on the existing CollapsiblePanelExtender, I decided to have a look at the existing client component. And I end up with Copy/Paste behaviour. I noticed the that the main issue with the CollapsiblePanelExtender is the animation, and the inclosure of the animation script. The animation and effects provided by jQuery is much more faster and better than the ones provided by the AjaxControlToolkit. So, I made a clone and removed everything related to animation.

I will call this control jQueryCollapsiblePanelExtender. I'm not going to support all features of CollapsiblePanelExtender, but will support the core features. And everything else can be implemented later.

I'll start with declaring and registering the client component namespace and class. After creating an ASP.NET AJAX Server Control Extender Project a JavaScript file will be added to the project. This is where I will code my client component:

/// <reference name="MicrosoftAjax.js"/>
/// <reference path="jquery-1.2.6.min.js" name="jquery-1.2.6.min.js"/>
Type.registerNamespace("jQueryASPNetAjaxControls");
jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender = function(element) {
 jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender.initializeBase(this, [element]);
}
jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender.prototype = {
 initialize: function() {
  jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender.callBaseMethod(this, 'initialize');
  //Initialize Code
 },
 dispose: function() {
  //Dispose Code
  jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender.callBaseMethod(this, 'dispose');
 }
 //Methods and Properties (Getters and Setters)
};
//Class Registration
jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender.registerClass('jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender', Sys.UI.Behavior);
if (typeof (Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

Line 7 is the initialize function, where I will initialize the extender and attache even handlers to do collapse and expand operations. Line 12 is dispose function, where I will clean up my code like remove all event handlers. on Line 19 this is client component class registration. Note that the base class is Sys.UI.Behavior. It is true that I said I made copy and paste from CollapsiblePanelExtender, but also I made major modifications. I do not use AjaxControlToolkit server or client libraries. I am using only the Core ASP.NET AJAX Library.

Client API Properties:

Based on CollapsiblePanelExtender few properties will be needed such as:

  • ExpandControlID/CollapseControlID: The controls that will expand or collapse the panel on a click, respectively. If these values are the same, the panel will automatically toggle its state on each click
  • Collapsed: Specifies that the object should initially be collapsed or expanded.
  • ImageControlID: The ID of an Image control where an icon indicating the collapsed status of the panel will be placed. The extender will replace the source of this Image with the CollapsedImage and ExpandedImage URLs as appropriate.
  • CollapsedImage: The path to an image used by ImageControlID when the panel is collapsed.
  • ExpandedImage - The path to an image used by ImageControlID when the panel is expanded.

These properties are going to have their values set when the client control is created. And this is the job of the Server Control as I will show in next part.

Few variables were needed and were created on the client class constructor:

jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender = function(element) {
 jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender.initializeBase(this, [element]);
 this._$element = null;
 this._$expandControl = null;
 this._$collapseControl = null;
 this._$imageControl = null;
 this._$clientStateControl = null;
 this._expandControlID = null;
 this._collapseControlID = null;
 this._imageControlID = null;
 this._clientStateFieldID = null;
 this._expandedImage = null;
 this._collapsedImage = null;
 this._collapseClickHandler = null;
 this._expandClickHandler = null;
 this._suppressPostBack = null;
 this._collapsed = false;
}

All variable that is have _$ prefix are going to be jQuery objects. Below is the client getter and setter functions for one of the above mentioned properties:

get_CollapseControlID: function() {
    return this._collapseControlID;
},
set_CollapseControlID: function(value) {
    if (this._collapseControlID != value) {
        this._collapseControlID = value;
        this._$collapseControl = $('#' + value);
        this.raisePropertyChanged('CollapseControlID');
    }
}

On line 7 I am creating a jQuery object around the Collapse Control. And I also as shown below made a getter method that return this jQuery object, to be part of the component client APIs if needed:

get_CollapsejQObj: function() {
  return this._$collapseControl;
}

All properties are implemented on client APIs as the above one.

Client API Events:

Again, the CollapsiblePanelExtender support client event handling for some events such expanded and collapsed events. And part of the core features these events are needed. Nothing new here it is just as it is in the original CollapsiblePanelExtender:

add_collapsed: function(handler) {
    this.get_events().addHandler('collapsed', handler);
},
remove_collapsed: function(handler) {
    this.get_events().removeHandler('collapsed', handler);
},
raiseCollapsed: function(eventArgs) {
    var handler = this.get_events().getHandler('collapsed');
    if (handler) {
        handler(this, eventArgs);
    }
}

Initializing Client Component:

During initialization process, previous state (Expanded or Collapsed) of the CollapsiblePanel is maintained, so in case of PostBack if the Panel is collapsed it will remain like that, and if expanded it will also remain like that. Also the CollapsiblePanel is being setup, for example showing expand or collapse image. In original CollapsiblePanel it also set the appropriate collapse or expand text on a predefined label as well as images title. Finally registration of event handlers is made. Below is the code for the initialize function:

initialize: function() {
 jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender.callBaseMethod(this, 'initialize');
  var element = this.get_element();
  //Creating jQuery Object for the Target Element.
  this._$element = $(this.get_element());

  //Maintaining Client State.
  var lastState = this.get_ClientState();
  if (lastState && lastState != "") {
      var wasCollapsed = Boolean.parse(lastState);
      if (this._collapsed != wasCollapsed) {
          this._collapsed = wasCollapsed;
          this.raisePropertyChanged('Collapsed');
      }
  }

  if (this._collapsed) {
      $(element).hide();
  }
  else {
      $(element).show();
  }
  //Setup CollapsiblePanel State (Set approperiate Collapse\Expand image)
  this._setupState(this._collapsed);

  //Initializing event handlers
  if (this._collapseControlID == this._expandControlID) {
      //if the collapse cdontrol is th esame as the expand one,
      //togglePanel would the handler.
      this._collapseClickHandler = Function.createDelegate(this, this.togglePanel);
      this._expandClickHandler = null;
  } else {
      //else each control would have a separate handler
      this._collapseClickHandler = Function.createDelegate(this, this.collapsePanel);
      this._expandClickHandler = Function.createDelegate(this, this.expandPanel);
  }
  if (this._collapseControlID) {
      var collapseElement = $get(this._collapseControlID);
      if (!collapseElement) {
          throw Error.argument('CollapseControlID', this._collapseControlID);
      } else {
          //register handler for click event on the collapse control
          $addHandler(collapseElement, 'click', this._collapseClickHandler);
      }
  }
  if (this._expandControlID) {
      if (this._expandClickHandler) {
          var expandElement = $get(this._expandControlID);
          if (!expandElement) {
              throw Error.argument('ExpandControlID', this._expandControlID);
          } else {
          //register handler for click event on the expand control
              $addHandler(expandElement, 'click', this._expandClickHandler);
          }
      }
  }
}

Disposing:

Disposing is very important. Having events registered they must be cleared and removed on disposing. And that is what dispose function do as shown below:

dispose: function() {
  var element = this.get_element(); 
  if (this._collapseClickHandler) {
      var collapseElement = (this._collapseControlID ? $get(this._collapseControlID) : null);
      if (collapseElement) {
          $removeHandler(collapseElement, 'click', this._collapseClickHandler);
      }
      this._collapseClickHandler = null;
  }
  if (this._expandClickHandler) {
      var expandElement = (this._expandControlID ? $get(this._expandControlID) : null);
      if (expandElement) {
          $removeHandler(expandElement, 'click', this._expandClickHandler);
      }
      this._expandClickHandler = null;
  }
  jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender.callBaseMethod(this, 'dispose');
}
Expand\Collapse the UI Effects:

You might notice the following functions when registering event handlers: togglePanel, expandPanel and collapsePanel. All are public functions for the client API and they call other private functions that is responsible for doing the UI effect of collapsing or expanding. Below I'll show the code responsible for collapse and expand.

The private function responsible for the collapse shown below:

_doClose: function(eventObj) {
  var eventArgs = new Sys.CancelEventArgs();        
  this.raiseCollapsing(eventArgs);

  //Check if cancelling the operation
  if (eventArgs.get_cancel()) {
      return;
  }
  //Create a callback with context referencing the current component.
  //The callback function is used to raise collapseComplete and collapsed events
  var closedCallback = Function.createCallback(this.__closed, this);
        
  //Collapse Effect with closedCallback function passed as parameter
  this._$element.slideUp('normal', closedCallback);

  //Perform setup for collapse state.
  this._setupState(true);

  if (this._suppressPostBack) {
      if (eventObj && eventObj.preventDefault) {
          eventObj.preventDefault();
      } else {
          if (event) {
              event.returnValue = false;
          }
          return false;
      }
  }
}

On line 11, a Callback is created. Actually this is a tricky step. I wanted to raise CollapseCompleted and Collapsed events just after the collapse animation effect take place. The jQuery slideUp and slideDown fcunctions accept compete function callback as parameter. On the callback function, I need to access the client component to call raiseXxx functions such as raiseCollapsed to raise the required events. And this would require to made a call like this this.raiseCollapsed(...);. But this is possible sense "this" will refer to the expanded or collapsed DOM element. Thanks to createCallback function, I was able to pass the component instance as a context parameter. Below is the code for __closed private function that is used as the callback for slideUp complete:

__closed: function(context) {
  //this : refers to the collapsed element.
  context.raiseCollapseComplete();
  context.raiseCollapsed(Sys.EventArgs.Empty);
}

_doOpen
function is exactly the same as _doClose except it calls slideDown function. Comparing this code with original CollapsiblePanel code, the UI Effect is done with one line of code using jQuery, while using ASP.NET AJAX Animation script it is another story. It require to initialize animation object and calls stop/play methods when appropriate. Of course the original CollapsiblePanel has a feature for specifying collapse and expand size. But these settings can be easily applied using jQuery animate function with less code as well.

Live Demo

As I am going to discuss the Server Extender Control in next part, I will show how to use this client control through manual JavaScript coding. Basically it is required to create the client control using $create shortcut function on client application init event just as below:

Sys.Application.add_init(function() {
    var c = $create(jQueryASPNetAjaxControls.jQueryCollapsiblePanelExtender,
            { 
                ClientStateFieldID: 'parentPanelState',
                CollapseControlID: 'parentPanel',
                ExpandControlID: 'parentPanel',
                ImageControlID: 'imgExpandCollapse',
                ExpandedImage: 'images/minus.png',
                CollapsedImage: 'images/plus.png',
                Collapsed: true
             }, 
             null, null, $get("childPanel"));
});

Just a small note, in line 4, this ClientStateFieldID is a hidden field that holds the current state of the CollapsiblePanel (expanded or collapsed). This is to maintain client state through PostBacks.

You can view a live demo here. Also download the sample web to explore the code.

Conclusion:

Many more features can be added to the CollapsiblePanel, such as expand direction (vertical, horizontal) and different animation style during collapse or expand as well as collapse and expand speed. I didn't demonstrate that here to save post space as well as leaving something for you to play with. Believe me using jQuery with Core ASP.NET AJAX library is much better than using AjaxControlToolkit. Not because AjaxControlToolkit is bad, but because it includes many scripts and its performance isn't that good. You can save hundreds of KBs by using jQuery and then build your own required controls with ASP.NET AJAX Core Library. For example if you replaced Animation script made in AjaxControlToolkit with Effects provided by jQuery not only you'll save bytes and get better UI Effects performance, but also you'll have fully featured framework which is easy to use with cost of 17 KB.

Hope you liked this post and see you in next part.

Comments (11) -

Bill Beckelman
Bill Beckelman United States on 10/22/2008 3:27 PM Muhammad,

I really appreciate the mention in your post. So when is the project going to be started on codeplex Smile

Bill
mosessaur
mosessaur Egypt on 10/22/2008 3:37 PM @Bill Beckelman Well Bill, I always keep checking your blog, good stuff you post out there and they are of good usage as well.
I have no plans to start a project on CodePlex regarding building another Toolkit that combine ASP.NET AJAX and jQuery if that what you mean. Because actually I think by the release of .Net 4.0 and VS.NET 2010 there will be great change in ASP.NET Ajax to use jQuery. And then manythings might change.
Jamie Swindall
Jamie Swindall United States on 10/25/2008 5:20 PM How would I go about allowing only one panel to be expanded at a time.  
mosessaur
mosessaur Egypt on 10/25/2008 5:24 PM @Jamie Swindall I didn't understand! isn't it like that already?
Fahad
Fahad India on 10/31/2008 12:47 PM Hey mosa,

How do u use core JQuery API's like .each / .map within a control, Say i want to access the properties that i define in my asp.net client object, but the scope inside these functions do not return these properties.

this.outsideVal = 'hello';
$(something).each(s.split(''),function(){
  //how to access outsideVal inside this?
});

Any ideas let me know.

Many Thanks,
Fahad
mosessaur
mosessaur Egypt on 11/1/2008 12:14 PM @Fahad Very interesting catch! But I think I covered this issue under Expand\Collapse the UI Effects section in this post. try to do something like this:
var callback= Function.createCallback(this.__callback, this);
where this will reference the ajax client control itself.
You can do this now: $(something).each(this.__callback);

Now the each method takes a callback function that already has 2 arguyment, your context param will be the 3rd one, so the signature of your __callback function should be like this:
__callback: function(index,domElement,context) {
        //this == domElement
        alert(context.outsideVal);
    }

I hope this helps
Fahad
Fahad India on 11/2/2008 1:01 AM Yup, that should work, i was running short of ideas on that Smile.

Anyways, i find many things limiting when using jQuery with ASP.NET client library. So i planned to work out to initialize the jQuery object fully in the initialize() function, and then rather have a mapping between the two worlds to let the user listen to events, change properties etc.,

Thanks for the tip!!

- Fahad
mosessaur
mosessaur Egypt on 11/2/2008 10:46 AM @Fahad For me it is working trick which make jQuery and ASP.NET AJAX Client Library so far compatible. I guess only few catchs that cannot be resolved or might need a true workaround that is not considered a solution. So far I never faced such thing except for something mentioned in Dave's Blog.
What are the other ways you have tested and found Fahad?
Fahad
Fahad India on 11/3/2008 10:07 AM Hey Mosa,

I find jQuery to be a more promising client library than the ASP.NET client library. On my further research, I did some small implementations that makes use of jQuery library seamlessly with ASP.NET (which i feel is the best way instead of having ASP.NET + ASP.NET CL + jQuery).

I blogged out in mine,

fadsworld.wordpress.com/.../

Do let me know your thoughts on this approach Smile.

-Fahad
mosessaur
mosessaur Egypt on 11/3/2008 9:30 PM @Fahad Interesting idea indeed. I thought about it once but never put it in practice. Good stuff Fahad. You should continue on that Providing the rest of fucntionality just as the ASP.NET AJAX Server Library do. I recommend that you post it in DotNetKicks.net You should get feedback about it.
Fahad
Fahad India on 11/5/2008 10:52 AM Thanks for ur comments Mosa, i will be continuing my work on this Smile.

Pingbacks and trackbacks (2)+

Comments are closed