ASP.NET AJAX: Creating Reusable JavaScript Components (that are not Controls)

So I have been working with ASP.NET AJAX and the Toolkit for about a year and a half total - give or take a few months.  Over this period I have played around with building a handful of AJAX controls ...

... and I am generally happy with how each one of these turned out.  However I have learned over time that a new control extender or script control is not always the right fit for the job.

Take the problem Mustafa was solving the other day ...

I have recently figured out that if there is a scrollbar’ed Panel control inside an UpdatePanel, it looses its scrollbar position after any type of partial postback within that UpdatePanel. There can be a GridView, a DIV container or another similar control instead of this Panel.

Mustafa generously provides a solution to the problem.  He describes a technique he is using for hooking into the PageRequestManager's beginRequest and endRequest events to tuck away the scroll bar position before the partial page is reloaded.  He then reapplies the scroll position after the panel has been refreshed.  I look at the code he provides for this and I am thinking - Yep, that looks great.  I want it.   

image

And I want that on every page except I don't want to copy/paste everywhere and I don't want to have to edit the JavaScript every time I add a new panel to the page.  The thing is, solving this problem via a reusable solution is a little tricky.  We could create an extender control that would hook onto the Panel's using the TargetControlID, but that seems a little heavy when all we have is a touch of JavaScript.  And besides the direct reference to Panel1's ClientID in Mustafa's solution - the script is completely reusable.  So I did a little reading up on some of ASP.NET AJAX's client side components to see if there was anything I could use to pull this code together into some sort of non-visual component that I could re-use across pages that wouldn't require me to write any server side code. 

 

Hello Sys.Component

And that is when I came across this bit of documentation ...

image

This sounded promising so I decided I would try to refactor the original JavaScript into a Sys.Component and see where it takes me.  So I moved Mustafa's original code into a Sys.Component class called majax.MaintainScrollPosition.  Within the Components initialize function I grab a reference to the PageRequestManager object and wire handlers to the pageLoading and pageLoaded events.  When pageLoading fires (this happens after the async-postback has completed but before the DOM is rewritten with the resulting data) I tuck away the scroll positions of the elements I am interested in and when pageLoaded is invoked I look up those elements again and set the scroll positions to what it was before the async-postback fired.  Here is the bit of JavaScript I am using to handle this ...

   1: onPageLoading : function(sender, args) {
   2:     // get a list of the panels that are going to
   3:     // be updated
   4:     var updatedPanels = args.get_panelsUpdating();
   5:     if(updatedPanels && updatedPanels.length > 0){
   6:  
   7:         // clear the array
   8:         Array.clear(this._elements); 
   9:  
  10:         // find all elements with the 
  11:         // and remember the scroll position 
  12:         for(var i = 0; i < updatedPanels.length; i++) {
  13:             Array.forEach($majax.getElementsByClassName('maintain-scroll', null, updatedPanels[i]),
  14:             function(e){
  15:                 if(e.id) {
  16:                     Array.add(this._elements, { "id":e.id, "x": e.scrollLeft, "y":e.scrollTop });
  17:                 }
  18:             }, this); 
  19:         }
  20:     }
  21: },
  22:  
  23: onPageLoaded : function(sender, args) {
  24:  
  25:     var updatedPanels = args.get_panelsUpdated();
  26:     if(updatedPanels && updatedPanels.length > 0){
  27:         // find all elements with the 
  28:         // and remember the scroll position 
  29:         for(var i = 0; i < updatedPanels.length; i++) {
  30:             Array.forEach(this._elements, function(e){
  31:                 var element = $get(e.id, updatedPanels[i]);
  32:                 if(element) {
  33:                     element.scrollLeft = e.x;
  34:                     element.scrollTop = e.y;
  35:                 }
  36:             }, this);
  37:         }
  38:     } 
  39: }

 

getElementsByClassName

The one thing you should notice with the onPageLoading function is that I am maintaining the scroll position for all HTML elements that have the maintain-scroll class applied to them.  To fetch these elements I am using a helper function called getElementsByClassName (taken from here) that scans the panel to find all of the elements that have this maintain-scroll class applied to them.  This approach is different from the typical extender control that extends a single control that it knows the ID of. 

So for my demo page, I have a DIV contained within an UpdatePanel with a fixed width/height, and have enough content that scroll bars are being applied.  To let my component know that we want it to maintain the scroll position of this DIV across partial postbacks all we have to do is tag the DIV with the maintain-scroll class like so ...

image

And to get my majax.MaintainScrollPosition script loaded I let the ScriptManager on the page know about my scripts and it will take care of the rest (the reference to majax.js is the script that contains the getElementsByClassName function.  I planned on putting other common scripts in this file as well).

image

These Path references are pointers to the location on the webserver where my scripts reside.

image

 

Is this a one-off Solution or is it something more General?

After creating the component that maintains the scroll position, I started wondering if this was a pattern that could be applied a little more broadly.  One of the first Toolkit controls I created extended a GridView control and added a bunch of cool row and column hovering effects.  And after I got it build just the way I wanted .Net 3.5 came out and I fell head over heals for the new ListView control.  And while I could still use my original extender control, it would take a little more work (i.e. explicit calls to $create for each of the tables I want to apply the behaviors to).  So I quick like moved my old TABLE hover behavior script into this new patter to see how it fit.  So I ...

  • Created a new Sys.Component JavaScript class that uses the getElementsByClassName function to fetch all TABLE's on the page that are tagged with the tablehover class
  • After these TABLE's are located I use the $create function to apply my hover behavior to the control
  • Add a reference to my script to the ScriptManager control

image

  • Updated my GridView to include the tablehover CSS class

image

And the coolest part is that without any code changes I can render the same grid using the ListView control as well ...

image

And they both work exactly the same.

 

Finally, The Demo's

The demo page for this post contains 2 sections.  The first one demonstrates how the majax.MaintainScrollPosition can be used to persist the scroll position of elements between async postbacks.  You can move the scroll bar around, then click the post back button.  This will cause the panel to refresh and because this panel has Mustafa's script tied to it - the original scroll position will be restored.

image

And the second demo shows how to use the majax.TableHover script with both the GridView and ListView controls.  Just hover over any of the table's cells and you will see how it works.

image

And you can download the sample site here.

 

Conclusion

I am kind of impressed with how this is looking so far.  I like that ...

  • I can easily apply the same behavior to elements that match a CSS selector.  This is nothing new to the wider JavaScript community, but to those of us using ASP.NET AJAX and the Toolkit it certainly is. 
  • If I don't need to interact with the control from the server I don't need to go through the process of building a server side piece for the component
  • I thought it was cool how I could easily apply the same behavior to both a GridView as well as an HTML table rendered by the ListView without changing a single line of code
  • I can hook into the PageRequestManager and attach to the ASP.NET AJAX client side life cycle events from within the Component

All of that being said, I spent a total of 3 hours putting this together stream-of-consciousness style, so odds are that I am missing something huge or that none of this is really all that useful to anyone but me.  Either way, leave a comment and let me know what you think.

 

That's it.  Enjoy!


TrackBack

TrackBack URL for this entry:
http://mattberseth.com/blog-mt/mt-tb.fcgi/128

Comments


Really really nice article!

Posted by: Doug on June 12, 2008 12:00 AM

Dude you rock once again!
When is your book coming out?
:)

Great article!

Thank you for a very educational and inspirational article ;-)

Very nice! Great example of taking a great idea and making it even better. I love continuous improvement! Bravo!

Posted by: Josh M on June 13, 2008 12:00 AM

Very interesting information. I always love reading your blog because of the insights it gives me.

Youre the man!

Posted by: jim on June 27, 2008 12:00 AM

This does NOT seem to work with a content page.
Here is what i did.
1. place the scriptreference in masterpages scriptmanager
2. place the class="maintain-scroll" in one of my div that contains server controls, which are inside an updatepanel.

when i clicked a linkbutton inside the div, which is inside an updatepanel, it STILL scroll ALL THE WAY to the top.

Did i do something wrong? Should there be xxx.ClientID placing somewhere in content page?

Posted by: It doesnt work with Master Page on June 27, 2008 12:00 AM

I have a master page, and putting your codes in my content page DOESNT SEEM to work.

Did i do something wrong?
A) I put ScriptManagerProxy in my content page to reference the .js
B) place the class="maintain-scroll" in my textbox inside updatepanel.

Please help?

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)

Consulting Services

Yep - I also offer consulting services. And heck, I'll do just about anything. If you enjoy my blog just drop me an email describing the work you need done.

Recent Comments

  • jim wrote: This does NOT seem to work with a content page. Here is what i did. 1. place the scriptreference in ...
  • It doesnt work with Master Page wrote: I have a master page, and putting your codes in my content page DOESNT SEEM to work. Did i do somet...
  • stelios a wrote: Youre the man! ...
  • Bryan Reynolds wrote: Great article! ...
  • Joakim Westin wrote: Thank you for a very educational and inspirational article ;-) ...
  • Bill Coffman wrote: Very nice! Great example of taking a great idea and making it even better. I love continuous improve...
  • Josh M wrote: Very interesting information. I always love reading your blog because of the insights it gives me. ...
  • Doug wrote: Dude you rock once again! When is your book coming out? :) ...