Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Advanced AJAX ListBox Component v0.6

2.33/5 (3 votes)
9 Apr 2008CPOL6 min read 1   131  
Final article on a horizontally-scrollable listbox component.

Introduction

In my last article, we added a server control property that allows us to take control over how the ListBox responds to mouse wheel events. In this article, we're going to take care of some anomalies, and fully enforce browser compatibility for the mouse wheel.

Background

Once again, let's review the requirements checklist we drew out in the second article. Now that we're getting down to the nitty gritty, we're going to break #4 out into two separate requirements:

  1. It is difficult or impossible to select multiple disparate items at once by using the SHIFT and CONTROL keys because their onkeydown events trigger the scrollbars to be repositioned based on the first item(s) selected. We should read the keyCode of the event and bypass our scrolling code for "meta" key presses like this.
  2. The ability to scroll through the items using a mouse wheel depends on both the browser and the version used to load the page. We would like to make the mouse scrolling a configurable option supported on all target browsers.
  3. The vertical and horizontal scrollbar positions are only preserved across postbacks when the control's HorizontalScrollEnabled property is explicitly set to true. We should be able to preserve scroll state event when HorizontalScrollEnabled is false.
  4. Our event handling strategy does not fully support Firefox 1.0 because some user interactions which change the scroll position do not execute the _onContainerScroll event handler.
  5. If you ever try debugging the _onContainerScroll handler, you'll notice IE can execute it up to four times for a single mouse click or keystroke. Dragging a scrollbar causes my CPU to briefly jump to 30% or higher, depending on how fast I scroll. Updating the hidden field this many times places an unnecessary load on the client computer's processor.

Although it may seem that we've squashed #2, those of you who are keen on Firefox may have noticed the following quirks:

  • In FF1, when HorizontalScrollEnabled == true and MouseWheelScroll == Enforce, the ListBox does not respond to the mouse wheel while the cursor is positioned over the scrollbars.
  • In FF1.5 and FF2, when HorizontalScrollEnabled == true and MouseWheelScroll == Prevent, the ListBox does respond to the mouse wheel while the cursor is positioned over the scrollbars.
  • In FF1, when the mouse wheel is used to scroll, the scroll state is not saved to the hidden field. Incidentally, this is the last outstanding issue for requirement #4.

The first two quirks surfaced because we didn't handle the DIV container's DOMMouseScroll event. We should tackle that issue first because it will ultimately affect the third.

Copy and Paste

Here is the reason why we gave the DIV a _thisPrototype field pointing to the prototype, in the last article. Now, we already have access to the client control's fields that are needed to compute the new scroll position when the DIV receives the onscroll event. Granted, I could've written the _onContainerMouseWheel code differently than the _onMouseWheel code, since the this reference will always point to the DIV. However, writing it this way gives us an opportunity to consolidate the code into a helper function later.

JavaScript
// find this snippet inside if _initializeEvents()
// FF doesn't have an onmousewheel event
if (this.get_element().onmousewheel === undefined)
{
    this.get_element().addEventListener(
         'DOMMouseScroll', this._onMouseWheel, false);

    // register the container's wheel with a different handler
    if (this.get_requiresContainerScroll())
    {
        this.get_elementContainer().addEventListener(
            'DOMMouseScroll', this._onContainerMouseWheel, false);
    }
}
    
    // 3c)
    // Define the event handlers
    //
    _onContainerMouseWheel : function(e)
    {
        var _this = this._thisPrototype;
        
        // stop the mouse wheel from scrolling
        if (_this.get_mouseWheelScroll() == false)
        {
            e.preventDefault();
            return false;
        }

        // enforce mouse wheel scrolling
        else if (_this.get_mouseWheelScroll() == true)
        {
            var listBox = _this.get_element();
            var container = this;
            var scrollingElement = this;
            var direction = (e.detail > 1) ? 1 : -1;
            
            // scroll the ListBox by the height of one item in the correct direction.
            var stepSize = scrollingElement.scrollHeight / listBox.options.length;
            var newScrollTop = scrollingElement.scrollTop + (stepSize * direction);
            scrollingElement.scrollTop = newScrollTop;
            
            // tell the browser we're taking care of the mouse wheel.
            e.preventDefault();
            return false;
        }
    }
,

All of our target Firefox versions will now fully comply with the control's MouseWheelScroll setting when the cursor is positioned over the scrollbars. Congratulations, we can now check off requirement #2!

Home Stretch

Since that was so easy, let's do something a little more challenging. By default, Firefox 1.0 does not fire an onscroll event for either the ListBox or the DIV container when the mouse wheel is used to scroll. This means the hidden field isn't updated with the latest positioning information. We can coax it into doing this, but first, let's examine the scenarios where it's necessary.

Remember, the default mouse wheel behavior for FF1 is to allow it to scroll the ListBox when horizontal scrolling is disabled and the ListBox has focus. This means that we'll have to register the _onMouseWheel handler even when MouseWheelScroll is NotSet (a.k.a. _mouseWheelScroll == null).

JavaScript
_initializeEvents : function()
{
    // handle mouse wheel events separately from all others
    if (this.get_mouseWheelScroll() != null)
    {
        // ...
    }
    
    // MouseWheelScroll == NotSet, hack FF1
    else if (!this.get_requiresContainerScroll() 
        && this.get_scrollStateEnabled()
        && this.get_element().onmousewheel === undefined)
    {
        this.get_element().addEventListener(
            'DOMMouseScroll', this._onMouseWheel, false);
    }

    // ...
}
,

Now, in the _onMouseWheel handler, we can try to handle the case where we need to update the scroll state in Firefox when horizontal scrolling is disabled, scroll state is enabled, and MouseWheelScroll is NotSet:

JavaScript
_onMouseWheel : function(e)
{
    var _this = this._thisPrototype
    if (_this === undefined)
        _this = this;

    // stop the mouse wheel from scrolling
    if (_this.get_mouseWheelScroll() == false)
    {
        // ...
    }
    
    // enforce mouse wheel scrolling
    else if (_this.get_mouseWheelScroll() == true)
    {
        // ...
    }
    
    // MouseWheelScroll == NotSet, hack FF1
    else if (!_this.get_requiresContainerScroll() 
        && _this.get_scrollStateEnabled()
        && _this.get_element().onmousewheel === undefined)
    {
        _this._updateScrollState();
    }
}
,

I have to admit, it took me a little while to figure out why the above code wouldn't work. I kicked myself when I found out that this._thisPrototype was returning undefined. I forgot that in _initializeUI(), _thisPrototype is only defined when MouseWheelScroll is either Enforce or Prevent. We have to remove the conditional check in that function so that we can have access to the DIV's _thisPrototype when MouseWheelScroll is NotSet. However, in the cases where this.get_elementContainer() returns null, we can't add a field to it.

JavaScript
_initializeUI : function()
{
    var listBox = this.get_element();
    var container = this.get_elementContainer();
    
    // hack to support mouse wheel scrolling
    listBox._thisPrototype = this;
    if (container != null)
    {
        container._thisPrototype = this;
    }
        
    // ...
}
,

Slide On In

The other case where we want to jimmy FF1 into doing the same thing is when MouseWheelScroll equals Enforce, but we have to do it in both the DIV's and in the ListBox's mouse wheel handlers. The hidden field must be updated near the end of the conditional block, just after the scrollTop property is set but before the return false line:

JavaScript
_onContainerMouseWheel : function(e)
{
    var _this = this._thisPrototype;
    
    // stop the mouse wheel from scrolling
    if (_this.get_mouseWheelScroll() == false)
    {
        // ...
    }

    // enforce mouse wheel scrolling
    else if (_this.get_mouseWheelScroll() == true)
    {
        // ...
        scrollingElement.scrollTop = newScrollTop;
        
        // hack FF1 into saving scroll state
        if (_this.get_scrollStateEnabled())
        {
            _this._updateScrollState();
        }

        // tell the browser we're taking care of the mouse wheel.
        e.preventDefault();
        return false;
    }
}
,
_onMouseWheel : function(e)
{
    var _this = this._thisPrototype
    if (_this === undefined)
        _this = this;

    // stop the mouse wheel from scrolling
    if (_this.get_mouseWheelScroll() == false)
    {
        // ...
    }
    
    // enforce mouse wheel scrolling
    else if (_this.get_mouseWheelScroll() == true)
    {
        // ...
        scrollingElement.scrollTop = newScrollTop;
        
        // hack FF1 into saving scroll state
        if (this._thisPrototype != undefined 
            && _this.get_scrollStateEnabled())
        {
            _this._updateScrollState();
        }
        
        // tell the browser we're taking care of the mouse wheel.
        e.preventDefault();
        return false;
    }
    
    // MouseWheelScroll == NotSet, hack FF1
    else if (!_this.get_requiresContainerScroll() 
        && _this.get_scrollStateEnabled()
        && _this.get_element().onmousewheel === undefined)
    {
        // ...
    }
}
,

...And for the first time in six articles, we can now say that our ListBox fully supports Firefox 1.0.

The Big Picture

No, I'm not going to write an article on how to deal with the fifth requirement. It would be boring, and no matter how inefficient the code currently is, it at least exhibits the behaviors we set out to achieve. Some of you may be sitting there, reading this, going through the source and demo, thinking to yourself, "Man, this guy wrote over a thousand lines of code for a control that I'll only use on one or two of my forms tops." And, you're right. My only answer is, if you think this is a lot of code, you should check out the ASP.NET AJAX Control Toolkit source. I actually added even more code to satisfy the fifth requirement for my production version of this control, but I'll leave that as an exercise for you to figure out on your own.

As an application specialist, my top priority is the users of the systems I create. Making things easier for them to do is what I do. Whenever possible though, I want to make things easier for myself as well. ASP.NET WebControls are great, and even though ASP.NET 2.0 added a lot of client functionalities to validators, buttons, etc., they're still, first and foremost, server controls. Meeting and exceeding user demands in web applications requires us to go beyond the server, into the client capabilities of the browsers they ultimately use.

Like I said in my first article, I'm Java and J2EE certified... but I must applaud Microsoft for this framework. Integrating a server control with configurable, cross-browser client capabilities opens a lot of opportunities to improve web application user interfaces quite rapidly. I wanted to learn even more about it, and they say the best way to learn something is to teach it to someone else. Hopefully, you learned as much as I did :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)