Click here to Skip to main content
5,786,882 members and growing! (20,726 online)
Email Password   helpLost your password?
Web Development » Ajax and Atlas » Controls     Intermediate License: The Code Project Open License (CPOL)

Advanced AJAX ListBox Component v0.5

By danludwig

Enforcing mouse scroll wheel behavior across target browsers
C#, .NET (.NET 2.0, .NET), ASP.NET, Dev, Design

Posted: 9 Apr 2008
Updated: 9 Apr 2008
Views: 5,896
Bookmarked: 10 times
Note: This is an unedited reader contribution
Announcements
Loading...



Search    
Advanced Search
Sitemap
votes for this Article.
Popularity: 0.00 Rating: 0.00 out of 5
Note: This is an unedited contribution. If this article is inappropriate, needs attention or copies someone else's work without reference then please Report This Article

Introduction

In my last article we modified our ListBox to allow meta keys (SHIFT and CONTROL) to be used to select multiple items in the ListBox. In this article we're going to keep our attention focused on the user and handle a different input scenario: the almighty mouse wheel.

Background

Once again, let's review the requirements checklist we drew out in the second article:

  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" keypresses like this.
  2. The ability to scroll through the items using a mouse wheel depends on both the browser and version used to load the page. We would like to make 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. Additionally, if you ever try debugging this 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.

It should be obvious which requirement to tackle next. It wouldn't be a good idea to optimize the event code when we haven't even handled all of the required events yet!

Compatibility Shmemadability

The default effect of mouse wheel events depends on the derived RequiresContainerScroll property, browser, and version you are using. Here's a breakdown of how it currently behaves.

DEFAULT BEHAVIOR RequiresContainerScroll == true RequiresContainerScroll == false
IE7 Only scrolls when mouse is positioned over scrollbars. Scrolls the ListBox by default.
IE6 Scrolls the DIV container by default. n/a
FF1 Scrolling is prevented by default. Scrolls the ListBox by default when the ListBox has focus.
FF1.5 Scrolls the ListBox by default. Scrolls the ListBox by default.
FF2 Scrolls the ListBox by default. Scrolls the ListBox by default.
Opera9 Scrolls the ListBox by default. n/a

It would be nice to be able to control the mouse wheel, so that we can either default to the above behavior matrix, prevent wheel scrolling, or enforce it. It would be even nicer if this property could be set on the server control, either declaratively or programmatically. I like to have nice things, so let's do it like that.

Mouse Wheel 101

The good news is, we can support this for the 6 browsers we've targeted to this point (which, with the exception of Safari 2 thus far, encompasses the majority browsers supported by Microsoft's ASP.NET AJAX Extensions framework). There is a consistency problem that we have to hack around though.

Both Opera and IE have an onmousewheel client event that can be handled using the event framework we're taking advantage of. Firefox, however, does not. What it does have is a DOMMouseScroll event that we can listen for. Among the many differences between the two, the most significant is that the onmousewheel event will be attached to the control prototype, whereas the DOMMouseScroll event will be attached to the ListBox (or DIV) element. The end result is that certain client control properties can't be referenced during the event handler, because for FF the this reference will point to the ListBox, not the prototype. Perhaps there is a way to attach it to the prototype, but if there is, I sure couldn't figure out how to do it... so I wrote another hack :P

Painfully Familiar

// 2.)
// Define the client control's class
//
DanLudwig.Controls.Client.ListBox = function(element)
{
    // initialize base (Sys.UI.Control)
    DanLudwig.Controls.Client.ListBox.initializeBase(this, [element]);

    // declare fields for use by properties
    this._mouseWheelScroll = null;
    this._requiresContainerScroll = null;
    this._scrollStateEnabled = null;
    this._horizontalScrollEnabled = null;
    this._scrollTop = null;
    this._scrollLeft = null;
}
    
    // 3d) 
    // Define the property get and set methods.
    //    
    set_mouseWheelScroll : function(value) 
    {
        if (this._mouseWheelScroll !== value)
        {
            this._mouseWheelScroll = value;
            this.raisePropertyChanged('_mouseWheelScroll');
        }
    }
,
    get_mouseWheelScroll : function()
    {
        return this._mouseWheelScroll;
    }
,
    

You should be able to add a property like that in your sleep by now. We're going to write the server control property later, but let me warn you that it's not going to be a boolean. The client control's _mouseWheelScroll field can be equal to true, false, or null. Null values will not attempt to handle any responses to the mouse wheel. When false, wheel scrolling should be prevented. When true, wheel scrolling should be enforced. Knowing this, we have enough information to register the correct events.

Ready... Set... HACK!

    _initializeEvents : function()
    {
        // handle mouse wheel events separately from all others
        if (this.get_mouseWheelScroll() != null)
        {
            // IE and Opera have an onmousewheel event
            this._onmousewheelHandler = Function.createDelegate(
                this, this._onMouseWheel);
            $addHandlers(this.get_element(), 
            {
                 'mousewheel' : this._onMouseWheel
            }, this);
            
            // also register the container's mouse wheel event
            if (this.get_requiresContainerScroll())
            {
                $addHandlers(this.get_elementContainer(), 
                {
                     'mousewheel' : this._onMouseWheel
                }, this);
            }
            
            // FF doesn't have an onmousewheel event
            if (this.get_element().onmousewheel === undefined)
                this.get_element().addEventListener('DOMMouseScroll', 
                this._onMouseWheel, false);
        }

        // rest of the event initialization code stays the same
    }
,
    
    // 3c)
    // Define the event handlers
    //
    _onMouseWheel : function(e)
    {
        if (this._mouseWheelScroll == false)
        {
            e.preventDefault();
            return false;
        }
    }
,
    

This code is sufficient enough to prevent mouse wheel scrolling when _mouseWheelScroll is false... in IE and Opera. Remember though, since FF registered the _onMouseWheel handler using DOMMouseScroll, it was attached to the ListBox instead of the prototype. So, this._mouseWheelScroll is not defined for FF. The good news is, we can add this property to the ListBox just like we added an originalListBoxSize property to the DIV in _initializeUI().

Before we do that though, let's stop and think. Are there any other properties we'll need to access? Perhaps. Instead of just adding the _mouseWheelScroll field, why don't we just add the whole prototype as a field. We're going to give the DIV the same field while we're at it because we're going to need it in the next article.

    _initializeUI : function()
    {
        var listBox = this.get_element();
        var container = this.get_elementContainer();
        
        // hack to support mouse wheel scrolling
        if (this.get_mouseWheelScroll() != null)
        {
            listBox._thisPrototype = this;
            container._thisPrototype = this;
        }
            
        // rest of code stays the same
    }
,
    

Which One is Backwards?

Now we have everything we need to handle the case where _mouseWheelScroll is equal to true. In the case of IE and Opera, the e parameter contains a wheelDelta property inside its rawEvent, which indicates the direction and magnitude of the mouse wheel movement. We're not concerned with the magnitude, so let's focus on direction. Moving the mouse wheel forward yields a positive integer, whereas moving the wheel toward you yields a negative one. This means that for positive values we want to decrease the scrollTop, and for negative values, we want to increase it. Because of this, we set our direction equal to 1 when wheelDelta is negative, and -1 when wheelDelta is positive.

Firefox, of course, exhibits the opposite behavior. When using the DOMMouseWheel event, we can access a wheelDelta-like variable using e.detail. Rotating the wheel towards the front produces a negative value, whereas moving it down produces a positive value. So, we want to handle the direction in the opposite way we handled it for wheelDelta. With all of this information, it's a piece of cake to set the scrollTop property of the correct element and override the default browser behavior.

    // 3c)
    // Define the event handlers
    //
    _onMouseWheel : function(e)
    {
        var _this = this._thisPrototype
        if (_this === undefined)
            _this = this;
    
        // 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.get_elementContainer();
            var direction, scrollingElement;
            
            if (this._thisPrototype === undefined) // IE & Opera
            {
                // negative wheelDelta should increase scrollTop,
                // positive wheelDelta should decrease the scrollTop.
                direction = (e.rawEvent.wheelDelta > 1) ? -1 : 1;
            }
            else
            {
                // detail's direction is opposite of wheelDelta
                direction = (e.detail > 1) ? 1 : -1;
            }
            
            // scroll the correct element
            if (_this.get_requiresContainerScroll())
                scrollingElement = container;
            else
                scrollingElement = listBox;
            
            // 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;
        }
    }
,
    

Stick a Fork in It

Currently, the only way to test this is by manually changing the _mouseWheelScroll field in the client control's constructor. The next step is to wire this client control property to a configurable server control property. We'll have an extra step involved this time though, because _mouseWheelScroll is not a normal boolean property. Ideally, we'd like to have an enumeration that gives more meaning to these three values.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace DanLudwig.Controls.Web
{
    public class ListBox : System.Web.UI.WebControls.ListBox, 
        System.Web.UI.IScriptControl
    {
        // all the server code we need will go right here
    }

    public enum ListBoxMouseWheelScrollSetting
    {
        NotSet,
        Enforce,
        Prevent
    }
}

Our client control property won't recognize this enumeration though. So, we need two server control properties: one to set the value, and another to translate it into a value that the client control can work with:

public virtual ListBoxMouseWheelScrollSetting MouseWheelScroll
{
    set { this.ViewState["MouseWheelScroll"] = value; }
    get
    {
        object output = this.ViewState["MouseWheelScroll"];
        if (output == null)
            output = ListBoxMouseWheelScrollSetting.NotSet;
        return (ListBoxMouseWheelScrollSetting)output;
    }
}

protected virtual bool? MouseWheelScrollClientValue
{
    get
    {
        if (MouseWheelScroll.Equals(
            ListBoxMouseWheelScrollSetting.Enforce))
            return true;
        else if (MouseWheelScroll.Equals(
            ListBoxMouseWheelScrollSetting.Prevent))
            return false;
        return null;
    }
}
    

We can now configure the server control's MouseWheelScroll property either declaratively in the ASPX page or programmatically in the codebehind (or other server-side code). However, during GetScriptDescriptors(), we want to pass the derived property (a nullable bool) to the client control, not the enumeration value:

protected virtual IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
    ScriptControlDescriptor descriptor = new ScriptControlDescriptor(
        "DanLudwig.Controls.Client.ListBox", this.ClientID);
    descriptor.AddProperty("mouseWheelScroll",
        this.MouseWheelScrollClientValue);
    descriptor.AddProperty("requiresContainerScroll", this.RequiresContainerScroll);
    descriptor.AddProperty("scrollStateEnabled", this.ScrollStateEnabled);
    descriptor.AddProperty("horizontalScrollEnabled", this.HorizontalScrollEnabled);
    descriptor.AddProperty("scrollTop", this.ScrollTop);
    descriptor.AddProperty("scrollLeft", this.ScrollLeft);
    return new ScriptDescriptor[] { descriptor };
}

Oh No, Not Again!

This is enough to satisfy most user-input scenarios in most browsers, but there are still a couple of inconsistencies. See if you can figure out what there is to fix next time...

License

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

About the Author

danludwig



Location: United States United States

Other popular Ajax and Atlas articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 1 of 1 (Total in Forum: 1) (Refresh)FirstPrevNext
GeneralViewing all articles in this seriesmemberdanludwig7:32 9 Apr '08  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

PermaLink | Privacy | Terms of Use
Last Updated: 9 Apr 2008
Editor:
Copyright 2008 by danludwig
Everything else Copyright © CodeProject, 1999-2009
Web10 | Advertise on the Code Project