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

Advanced AJAX ListBox Component v0.4

0.00/5 (No votes)
9 Apr 2008CPOL4 min read 1   221  
Responding to special keyboard events to improve behavior.

Image 1

Introduction

In my last article, we modified our ListBox to enforce a browser compatibility issue introduced by separating out horizontal scrolling from scroll state preservation. In this article, we're going to turn our attention to the interface user, and finally make this control beta-worthy.

Background

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" 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 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 the scroll state even 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 that 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.

The biggest issue on our plate from a usability standpoint is #1. We'll start this out intuitively, then see why we need to do some tricks in order to mimic the normal ListBox behavior.

Shock and Awe

JavaScript
    isMetaKeyPress : function(e)
    {
        if (e.keyCode == 16 // SHIFT key
            || e.keyCode == 17 // CONTROL key
        )
            return true;
        return false;
    }
,
    _onContainerKeyDown : function(e) 
    {
        if (!this.isMetaKeyPress(e))         {
            // previous code all goes inside this conditional block
        }
    }
,

This alleviates part of the CONTROL and SHIFT key problem. This will add some flavor to the control, but now let's kick it up a notch...

BAM!

JavaScript
    isMetaKeyPress : function(e)
    {
        if (e.keyCode == 8 // BACKSPACE key
            || e.keyCode == 9 // TAB key
            || e.keyCode == 16 // SHIFT key
            || e.keyCode == 17 // CONTROL key
            || e.keyCode == 18 // ALT key
            || e.keyCode == 19 // PAUSEBREAK key
            || e.keyCode == 20 // CAPSLOCK key
            || e.keyCode == 27 // ESC key
            || e.keyCode == 45 // INSERT key
            || e.keyCode == 91 // WINDOWS key
            || e.keyCode == 93 // CONTEXT key
            || e.keyCode == 112 // F1 key
            || e.keyCode == 113 // F2 key
            || e.keyCode == 114 // F3 key
            || e.keyCode == 115 // F4 key
            // skip F5 key, since it reloads the page
            || e.keyCode == 117 // F6 key
            || e.keyCode == 118 // F7 key
            || e.keyCode == 119 // F8 key
            || e.keyCode == 120 // F9 key
            || e.keyCode == 121 // F10 key
            || e.keyCode == 122 // F11 key
            || e.keyCode == 123 // F12 key
            || e.keyCode == 127 // DELETE key
            || e.keyCode == 144 // NUMLOCK key
            || e.keyCode == 145 // SCROLLLOCK key
        )
            return true;
        return false;
    }
,

Because a normal ASP ListBox won't scroll during these key-presses, this is a better match for those data-entry clerks with long, slippery fingernails.

Be on Your Best Behavior

With this code, we still have some problems. When selecting a large block of items from top to bottom (using the SHIFT key), the DIV jumps back up to the top item selected when you click the mouse. This is because the mouse click triggers the onchange event, and the SHIFT keyCode is not captured in that event. We can get around this by adding additional cases where the updateListBoxScrollPosition() function should not be called.

JavaScript
    supressAutoScroll : function(e)
    {
        if (this.isMetaKeyPress(e))
            return true;
        
        var selectedItems = this.getSelectedItemCount(true);
        if (selectedItems > 1)
            return true;
        
        return false;
    }
,
    getSelectedItemCount : function(breakOnMultiple)
    {
        var selectedItems = 0;
        for (i = 0; i<this.get_element().options.length; i++)
        {
            if (this.get_element().options[i].selected == true)
                selectedItems++;
            
            if (breakOnMultiple && selectedItems > 1)
                break;
        }
        return selectedItems;
    }
,
    _onChange : function(e) 
    {
        if (this.get_element() && !this.get_element().disabled) 
        {
            if (!this.supressAutoScroll(e))             {
                updateListBoxScrollPosition(this.get_elementContainer(), 
                    this.get_element(), null);
            }
            
            // rest of the code stays the same
        }
    }
,
    _onContainerKeyDown : function(e) 
    {
        if (!this.supressAutoScroll(e))         {
            // change condition from isMetaKeyPress 
            // to supressAutoScroll above
        }
    }
,

What we did here was add two other helper functions. We've written the getSelectedItemCount function in a way that it can be reused by a real get_selectedItemCount property later, if need be. For the purposes of this requirement though, we only need to know if there's more than one item selected in the ListBox. Because of this, we pass a parameter that will cause the counting to stop and return 2 when there are multiple items selected. We do this from supressAutoScroll so that we can combine the two cases when automatic DIV scrolling should be suppressed:

  1. When _onContainerKeyDown is executed in response to a meta key-press, and
  2. When _onChange or _onContainerKeyDown are executed while there are multiple items selected.

All that's left is to use the supressAutoScroll function to perform a condition check before calling the updateListBoxScrollPosition method that causes the auto-scroll behavior in the two event handlers.

Splitting Hairs

One thing about the normal ListBox is that users can select an item, hold the SHIFT key, then hit another key to select multiple items. Our ListBox does this too, but the normal ListBox will jump to show the newly selected item, whereas ours does not. Also, the PAGEUP and PAGEDOWN keys don't work because the ListBox has no internal scrollbars. They act just like the HOME and END keys (which do work as intended).

Though I'm sure it's possible to imitate these behaviors, we're getting to a point of diminishing returns here. Like I said in my first article, perfectionism is a disease. The current control will be intuitive enough for the vast majority of users. It is now possible to use the SHIFT and CONTROL keys to click-select multiple items without having the scroll position go all haywire, which is good enough to check this requirement off of the list.

License

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