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

Posted on June 11, 2008 by matt@mattberseth.com.
Categories: ASP.NET, Contributors.
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 ...
onPageLoading : function(sender, args) {
    // get a list of the panels that are going to
    // be updated
    var updatedPanels = args.get_panelsUpdating();
    if(updatedPanels && updatedPanels.length > 0){
        // clear the array
        Array.clear(this._elements); 
        // find all elements with the 
        // and remember the scroll position 
        for(var i = 0; i < updatedPanels.length; i++) {
            Array.forEach($majax.getElementsByClassName('maintain-scroll', null, updatedPanels[i]),
            function(e){
                if(e.id) {
                    Array.add(this._elements, { "id":e.id, "x": e.scrollLeft, "y":e.scrollTop });
                }
            }, this); 
        }
    }
},
 
onPageLoaded : function(sender, args) {
     var updatedPanels = args.get_panelsUpdated();
    if(updatedPanels && updatedPanels.length > 0){
        // find all elements with the 
        // and remember the scroll position 
        for(var i = 0; i < updatedPanels.length; i++) {
            Array.forEach(this._elements, function(e){
                var element = $get(e.id, updatedPanels[i]);
                if(element) {
                    element.scrollLeft = e.x;
                    element.scrollTop = e.y;
                }
            }, this);
        }
    } 
}

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!

no comments yet.

Leave a comment

Names and email addresses are required (email addresses aren't displayed), url's are optional.

Comments may contain the following xhtml tags:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>