Getting jQuery Goodness into ASP.NET AJAX

If you have been following along with me over the past few days you have seen that I am looking into ways to incorporate some of the goodness of jQuery into ASP.NET AJAX.  Of course the obvious question is why don't I just switch over to jQuery completely?  Well ...

The team I work on is not switching from WebForms to MVC any time soon. And I am fine with that - we have a bunch of good dev's all with plenty of years of WebForms experience so we understand all of the quirks and can get past any of the weirdness that sometimes pops up.

But I don't really want to use 3 ajax libraries (jQuery, ASP.NET AJAX and the Toolkit). But I like how easy it is to author and distribute jQuery plugins and I wish there was a similar pattern for ASP.NET AJAX.

I posted that bit as a follow up to comment George left to my previous post.  If I was starting from scratch using ASP.NET's MVC engine I would definitely just use jQuery as my client side library and be done with it.  But that just doesn't make sense for my project.  So in the mean time, I would like to try to get some jQuery-like features into my WebForms application.  Specifically what I want is a better selector function, a more fluent programming model and a simple framework for authoring and distributing plug-ins.

Sound interesting?  Read on to find out how I am going to get there ...

Live Demo | Download

 

What's on my Wish List

This is some of the stuff I want ...

 

1. A Better Selector

$get fetches elements by ID.  And that's OK, but it would be great if there was a more robust selector method that allowed me to find elements by any CSS selector.  So I could do stuff like this (see below). 

   1: //  Find me the alternating rows of the #orders table
   2: var alternatingRows = $select('#orders TR.alt');
   3:  
   4: //  ... or even better
   5: var alternatingRows = $select('#orders TR:odd');   

 

2. Programming Model - Events

Next, I would want a slightly different programming model for the DOM elements that match a selector.  I would like to be able to do things like this (see below).  So much of AJAX is navigating the DOM then doing something with the resulting elements (like attaching event handlers).  I think it would be nice if the programming model had better support for these common tasks.

   1: //  Find me the rows of the #orders table
   2: $select('#orders TR').each(function(e) {
   3:     //  add the over class while the mouse is hovering
   4:     e.mouseover(function() { this.addCssClass('over'); });
   5:     //  remove the class when the mouse leaves
   6:     e.mouseout(function() { this.removeCssClass('over'); });
   7: }

 

3. Programming Model - Extend Elements without using the TargetControlID

ExtenderControl's use the TargetControlID property to wire the extension logic to a specific control via the extender control's markup.  This works well, but in many cases I would actually prefer doing something like this instead ...

   1: $select('watermark').each(function(e){
   2:     //  add the watermark to all of the elements tagged
   3:     //  with the watermark CSS class
   4:     e.watermark());
   5: }); 

 

I use the TextBoxWatermarkExtender in a lot of the pages I build, but I have never once needed to access the WatermarkText or WatermarkCssClass from the server.  For my tastes I would really prefer to use the above syntax to apply watermarking.  I would rather fetch the elements I want watermarked using a selector and than provide these elements to the watermarking extender using the above syntax.  I think watermarking falls nicely into the 'find some DOM elements and do something useful with the resulting elements'.   

 

4. JavaScript only plug-ins

And finally, I would like to see a simple way to author and distribute JavaScript only plug-ins.  There are plenty of Toolkit users, but not too many people are building Toolkit controls.  I can't help but think that part of this is because there is a bit of a learning curve for creating Toolkit controls.  I think it would be neat if we could create JavaScript only plug-ins for ...

  • Some of the cool jQuery, DoJo, MooTools, Prototype, [Pick your favorite JavaScript library] stuff that is already out there.  I realize that because of the WebForms programming model some of this stuff won't port directly, but I would think much of it will.  It would be cool if we could create a simple JavaScript plug-in framework that so us WebForms developers could make use of some of these AJAX widgets.
  • The existing Toolkit extender controls that fit naturally into plug-in programming model.  For starters I think the following controls would make good candidates ...
    • RoundedCorners, MutuallyExclusiveCheckBox, ToggleButton, DropShadow and DragPanel

 

Sounds Good ... Now What?

So with all of this in the back of my mind, last week I stumbled onto Mustafa's post about remembering the scroll position of an element that is contained within an UpdatePanel across partial page loads.  I wanted to use Mustafa's script, but I didn't like that I would have to copy/paste this logic around from page to page.  So I looked into the ASP.NET client side documentation and wrote a Component that handles this task for me.  And I was happy with how this works, but it was still a bit clunky.  So this weekend I took some time and revised things a bit.  In the process I also tried out addressing some of the items mentioned on my wish list above 

  • I added a new shortcut function called $select that finds DOM elements by ID or by CSS class.  Below is a quick example.  The $select function returns all elements with the ID of #someElement or that have a CSS class of dragpanel.  I know this is very simple compared with what jQuery supports, but it is also a bit better than what the $get ASP.NET AJAX function provides.
   1: var elements = $select('#someElement, dragPanel');
  • Next I added an each function to the Array prototype.  Maybe it is just me, but I find this incredibly readable.
   1: $select('#someElement, dragPanel').each(function(e){
   2:     //  do something interesting with the elements that
   3:     //  matched this selector ...
   4: });
  • I updated the getElementsByClassName function (the new $select calls this) and I wrap the resulting DOM elements in the Sys.UI.Control instance.  This is useful for the next two items ...
  • I created a very simple programming model for JavaScript only plug-ins.  To create a ASP.NET AJAX JavaScript plug-in, I create a Sys.Component class that does the following
    • Defines a function that specifies the plug-ins API.  Below is the sample for the RoundedCorners plug-in and the _round function is the plug-ins interface.  It accepts a single parameter - the radius in pixels for the rounded corners.
    • In the plug-in's initialize function I am adding a round function to the Sys.UI.Control's prototype that will allow this plug-in to be called from any instance of the Control class. 
   1: Type.registerNamespace("majax");
   2:  
   3: majax.RoundedCorners = function() {
   4:     majax.RoundedCorners.initializeBase(this);
   5: }
   6:  
   7: majax.RoundedCorners.prototype = {
   8:  
   9:     initialize : function() {
  10:         majax.RoundedCorners.callBaseMethod(this, 'initialize');
  11:  
  12:         //  attach the plugin
  13:         Sys.UI.Control.prototype.round = this._round;
  14:     },
  15:  
  16:  
  17:     dispose : function() {
  18:  
  19:         majax.RoundedCorners.callBaseMethod(this, 'dispose');
  20:     },
  21:  
  22:     _round : function(radius) {
  23:         $create(AjaxControlToolkit.RoundedCornersBehavior, {"Radius":radius}, null, null, this.get_element());        
  24:     }
  25: }
  26:  
  27: //  register the class
  28: majax.RoundedCorners.registerClass('majax.RoundedCorners', Sys.Component);
  29:  
  30: //  create the singleton
  31: $create(majax.RoundedCorners, null, null, null);
  32:  
  33: // Since this script is not loaded by System.Web.Handlers.ScriptResourceHandler
  34: // invoke Sys.Application.notifyScriptLoaded to notify ScriptManager 
  35: // that this is the end of the script.
  36: if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();

 

And because I am wrapping the DOM elements returned from $select in instances of this class it allows us to do cool things like this ...

   1: $select('#roundedPanel1, #roundedPanel2').each(function(e){
   2:     //  add the rounded corners (6 pixel radius) to the panels
   3:     e.round(6);
   4: });

 

  • And finally, I plan on adding extending the Sys.UI.Control prototype to include functions for adding event handlers like click, mouseover, mouseout, focus, etc ... that would make stuff like this possible.
   1: //  Find me the rows of the #orders table
   2: $select('#orders TR').each(function(e) {
   3:     //  add the over class while the mouse is hovering
   4:     e.mouseover(function() { this.addCssClass('over'); });
   5:     //  remove the class when the mouse leaves
   6:     e.mouseout(function() { this.removeCssClass('over'); });
   7: }

 

Here Come the Plug-ins

And to test this stuff out, I created a 6 ASP.NET AJAX plug-ins.  Five of these are just like the RoundedCorners one I showed previously - they just take the parameters and pass them through to the corresponding Toolkit extender control's JavaScript Behavior.  But I did also create a brand new plug-in that isn't based on the Toolkit.  Here is the rundown of these plug-ins ...

DropShadow

Snippet below shows how to add a drop shadow to all elements on the page that have the dropShadow CSS class applied to them.

   1: //  add the drop shadow plugin to all elements with the dropShadow class
   2: $select('dropShadow').each(function(e){
   3:     //  opacity of 0.75 and a width of 4px
   4:     e.shadow(0.75, 4);
   5: });  

Here is the markup for some elements that will match this selector

   1: <div class="dropShadow">
   2:     <div style="padding:10px">  
   3:         <table>
   4:             <tr><th>First Name </th><td><asp:TextBox ID="TextBox1" runat="server"></asp:TextBox></td></tr>
   5:             <tr><th>Last Name </th><td><asp:TextBox ID="TextBox2" runat="server"></asp:TextBox></td></tr>
   6:         </table>
   7:     </div>
   8: </div>
   9: <br />
  10: <div class="dropShadow">
  11:     <div style="padding:10px">  
  12:         <table>
  13:             <tr><th>First Name </th><td><asp:TextBox ID="TextBox3" runat="server"></asp:TextBox></td></tr>
  14:             <tr><th>Last Name </th><td><asp:TextBox ID="TextBox4" runat="server"></asp:TextBox></td></tr>
  15:         </table>
  16:     </div>
  17: </div> 

And here is a screen shot of what is displayed

image

MutexCheckBox

Snippet below shows how to create checkboxes whose values are mutually exclusive.

   1: //  hook up the mutually exclusive checkboxes
   2: $select('#haveGuestBedroom, #notHaveGuestBedroom').each(function(e){
   3:     e.mutexCheckBox('guestBedroom');
   4: });
   5: $select('#haveSplitPlan, #notHaveSplitPlan').each(function(e){
   6:     e.mutexCheckBox('splitPlan');
   7: });
   8: $select('#haveCoveredPatio, #notHaveCoveredPatio').each(function(e){
   9:     e.mutexCheckBox('coveredPatio');
  10: });
  11: $select('#haveGatedCommunity, #notHaveGatedCommunity').each(function(e){
  12:     e.mutexCheckBox('gatedCommunity');
  13: });

Here is the markup for some elements that will match this selector

   1: <table>
   2:     <tr>
   3:         <td>
   4:             <b>Must Have</b><br />
   5:             <asp:Checkbox runat="server" ID="haveGuestBedroom" Text="Guest Bed Downstairs" /><br />
   6:             <asp:Checkbox runat="server" ID="haveSplitPlan" Text="Split Plan" /><br />
   7:             <asp:Checkbox runat="server" ID="haveCoveredPatio" Text="Covered Patio" /><br />
   8:             <asp:Checkbox runat="server" ID="haveGatedCommunity" Text="Gated Community" /><br />
   9:         </td>
  10:         <td>
  11:             <b>Must Not Have</b><br />
  12:             <asp:Checkbox runat="server" id="notHaveGuestBedroom" Text="Guest Bed Downstairs" /><br />
  13:             <asp:Checkbox runat="server" id="notHaveSplitPlan" Text="Split Plan" /><br />
  14:             <asp:Checkbox runat="server" id="notHaveCoveredPatio" Text="Covered Patio" /><br />
  15:             <asp:Checkbox runat="server" id="notHaveGatedCommunity" Text="Gated Community" /><br />
  16:         </td>            
  17:     </tr>
  18: </table>

And here is a screen shot of what is displayed

image

RoundedCorners

Snippet below shows how to create elements with rounded corners.

   1: $select('#roundedPanel1, #roundedPanel2').each(function(e){
   2:     //  add the rounded corners (6 pixel radius) to the panels
   3:     e.round(6);
   4: });

Here is the markup for some elements that will match this selector

   1: <div id="roundedPanel1" class="rounded" style="height:200px">
   2:     <div style="padding: 10px; text-align: center">
   3:         Rounded Panel #1
   4:     </div>
   5: </div>
   6: <br />
   7: <div id="roundedPanel2" class="rounded" style="height:200px">
   8:     <div style="padding: 10px; text-align: center">
   9:         Rounded Panel #2
  10:     </div>
  11: </div>  

And here is a screen shot of what is displayed

image

ToggleButton

Snippet below shows how to turn a checkbox into a toggle button

   1: $select('toggleButton').each(function(e){
   2:     //  it would have been cool if the
   3:     //  toggle button extender applied the images
   4:     //  with CSS classes instead ;)
   5:     e.toggleButton(
   6:     {
   7:         "CheckedImageAlternateText":"Check",
   8:         "CheckedImageUrl":"_assets/img/ToggleButton_Checked.gif",
   9:         "ImageHeight":19,
  10:         "ImageWidth":19,
  11:         "UncheckedImageAlternateText":"UnCheck",
  12:         "UncheckedImageUrl":"_assets/img/ToggleButton_Unchecked.gif"
  13:     });
  14: });

Here is the markup for some elements that will match this selector

   1: <asp:CheckBox ID="CheckBox1" CssClass="toggleButton" Checked="true" Text="I like ASP.NET" runat="server"/><br />
   2: <asp:CheckBox ID="CheckBox2" CssClass="toggleButton" Checked="true" Text='I like ASP.NET AJAX' runat="server"/><br /><br />
   3: <asp:Button ID="Button1" runat="server" Text="Submit" OnClick="Button1_Click" />
   4: <br /><br />
   5: <asp:Label ID="Label1" runat="server" Text="[No response provided yet]" />

And here is a screen shot of what is displayed

image

CheckGroup

Snippet below shows how to create a check all checkbox that is automatically kept in sync with a group of checkboxes.  This would useful in TABLE where the header row has a check box that checks or unchecks all items in row

   1: $select('checkall').each(function(e){
   2:     //  add checkgroup plugin to the groupclass elements
   3:     e.checkGroup($select('groupclass'));
   4: }); 

Here is the markup for some elements that will match this selector

   1: <table style="border:solid 1px #444;">
   2:     <tr><th><input type="checkbox" class="checkall" /></th><th>Description</th><th>Comments</th><th>More Comments</th></tr>
   3:     <tr><td><input class="groupclass" name="group" type="checkbox"/></td><td>Some Text</td><td>Some Text</td><td>Some Text</td></tr>
   4:     <tr><td><input class="groupclass" name="group" type="checkbox"/></td><td>Some Text</td><td>Some Text</td><td>Some Text</td></tr>
   5:     <tr><td><input class="groupclass" name="group" type="checkbox"/></td><td>Some Text</td><td>Some Text</td><td>Some Text</td></tr>
   6:     <tr><td><input class="groupclass" name="group" type="checkbox"/></td><td>Some Text</td><td>Some Text</td><td>Some Text</td></tr>
   7:     <tr