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

Advanced AJAX ListBox Component v0.3

4.00/5 (1 vote)
30 Jun 2007CPOL4 min read 1   364  
Enforcing browser compatibility for horizontal scrolling and scroll state preservation.

ListBoxComponent0.3.gif

Introduction

In my last article, we updated our ASP.NET AJAX-enabled ListBox control, and separated client scroll state preservation from horizontal scrolling, so that the two could be configured separately. In this article, we're going to build upon that code and further enforce cross-browser functionality.

Background

We've come a long way, but we still have more features to add to our ListBox control. We're going to keep the biggest ones on hold until the next article though, and I'll tell you why. The main problem we left off with in the last article was that scroll state is only correctly saved in IE6 when HorizontalScrollEnabled is set to true. This is because IE6 does not execute the ListBox's onscroll event and will always return 0 as the ListBox's scrollTop value. Furthermore, it still wouldn't work without those two issues because setting the ListBox's scrollTop in IE6 doesn't even do anything. As far as I'm concerned, it is unacceptable to require HorizontalScrollEnabled be true in order to achieve the intended functionality in as common a browser as IE6... especially since we can solve it with changes in the code.

We want to have as few "hacks" as possible, but at the same time, good programmers have to write hacks from time to time... and I personally feel a little better about them when they're justified. In this case, IE6's behavior differs so much from that of Firefox (and IE7, for that matter), I can justify writing a browser hack. The scrollTop property of our ListBox is completely impotent in IE6. But, scrollTop of our containing DIV is fully operational. We can achieve our desired results by using the DIV container's scrollbars for IE6 even when HorizontalScrollEnabled is false.

If You Can't Keep It in Your Pants, Keep It on the Server

What I cannot justify is any client-side browser-sniffing code. We can get consistent and useful browser information on the server from the Page's Request.Browser object. The tricky part is how to refactor the code so that the control is rendered properly in all of our target browsers according to the HorizontalScrollEnabled and ScrollStateEnabled properties. In the last article, we extracted the ScrollStateEnabled property out of the single HorizontalScrollEnabled setting we started with in version 0.1. What we have to do here isn't much different... we have to separate out a RequiresScrollContainer property from the HorizontalScrollEnabled property. Again, let's start by adding the property.

C#
protected virtual bool RequiresContainerScroll
{
    get
    {
        if (HorizontalScrollEnabled)
            return true;

        else if (ScrollStateEnabled
            && Page.Request.Browser.Browser.Equals("IE")
            && Page.Request.Browser.MajorVersion < 7)
            return true;

            // Opera exhibits the same behavior as IE6 when

            // scrolling inside and outside of a DIV container

        else if (ScrollStateEnabled 
            && Page.Request.Browser.Browser.Equals("Opera"))
            return true;

        return false;
    }
}

What we're saying here is that whether or not a ListBox requires scrolling to be handled by a DIV container depends on more than just the HorizontalScrollEnabled property. Sure, if HorizontalScrollEnabled is true, of course, we need horizontal scrolling to be handled by the DIV. But, even when it's false, we still need the DIV to handle scrolling in IE6 (and, as it turns out, Opera) when ScrollStateEnabled is true.

Using this new property, we can pretty easily refactor the rendering code. Instead of just delegating certain style properties to the container when HorizontalScrollEnabled is true, we now want to delegate them when RequiresContainerScroll is true.

C#
protected virtual void AddContainerAttributesToRender(HtmlTextWriter writer)
{
    // the container now depends on 3 different property values

    if (this.HorizontalScrollEnabled || this.ScrollStateEnabled)
    {
        // add required container attributes

        writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID
            + ContainerClientIdSuffix);

        // add conditional container attributes

        if (this.HorizontalScrollEnabled)
        {
            writer.AddStyleAttribute(HtmlTextWriterStyle.Overflow, "auto");
        }
        else if (this.RequiresContainerScroll)
        {
            // Opera doesn't support overflow-x or overflow-y

            writer.AddStyleAttribute(HtmlTextWriterStyle.Overflow, "auto");
            writer.AddStyleAttribute(HtmlTextWriterStyle.OverflowX, "hidden");
        }

        if (this.RequiresContainerScroll)
        {
            writer.AddStyleAttribute(HtmlTextWriterStyle.Width,
                this.Width.ToString());

            // add other optional container attributes

            // move style declarations from the Style attribute 

            // into the DIV container.

        }
    }
}

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
    if (RequiresContainerScroll)
    {
        // the code inside this if block stays the same.

        // only the condition is changed.

    }
    else
    {
        base.AddAttributesToRender(writer);
    }
}

Because Opera doesn't support overflow-x or overflow-y, it will display horizontal scrollbars even when HorizontalScrollEnabled is false. So that we don't have to deal with this issue, let's just pretend Opera's such a great, advanced browser, it shows horizontal scrollbars without our help :P

Pass the Server Control Property to the Client Control

Now, we need to tell the client script whether the control requires container scrolling. This part should be getting easy by now.

C#
protected virtual IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
    ScriptControlDescriptor descriptor = new ScriptControlDescriptor(
        "DanLudwig.Controls.Client.ListBox", this.ClientID);
    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 };
}
// 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._requiresContainerScroll = null;
    this._scrollStateEnabled = null;
    this._horizontalScrollEnabled = null;
    this._scrollTop = null;
    this._scrollLeft = null;
}
    // 3d) 

    // Define the property get and set methods.

    //    

    set_requiresContainerScroll : function(value) 
    {
        if (this._requiresContainerScroll !== value)
        {
            this._requiresContainerScroll = value;
            this.raisePropertyChanged('_requiresContainerScroll');
        }
    }
,
    get_requiresContainerScroll : function()
    {
        return this._requiresContainerScroll;
    }
,

This is how we keep the browser hack on the server.

Use the Server Control Property in the Client Control

Now, we have to use this new property in the client code to register the correct events, initialize the UI, store the correct scroll state, and then restore it after postback. All this involves is replacing certain instances of this.get_horizontalScrollEnabled() with this.get_requiresContainerScroll(), like we did in the server code. Here's what it will look like:

JavaScript
    _initializeEvents : function()
    {
        // when horizontal scroll is enabled, use 3 zivros events

        if (this.get_requiresContainerScroll())
        {
            // same code that previously fell under

            // if (this.get_horizontalScrollEnabled())

        }
        
        // the rest of this method stays the same

    }
,
    _initializeUI : function()
    {
        var listBox = this.get_element();
        var container = this.get_elementContainer();
        
        if (this.get_requiresContainerScroll())
        {
            // same code that previously fell under

            // if (this.get_horizontalScrollEnabled())

        }
        
        if (this.get_scrollStateEnabled())
        {
            this._restoreScrollState();
        }
        
        if (this.get_requiresContainerScroll())
        {
            // same code that previously fell under

            // if (this.get_horizontalScrollEnabled())

        }
    }
,
    _restoreScrollState : function()
    {
        var scrollingElement = this.get_elementContainer();
        if (!this.get_requiresContainerScroll())
            scrollingElement = this.get_element();
        
        scrollingElement.scrollTop = this.get_scrollTop();
        scrollingElement.scrollLeft = this.get_scrollLeft();
    }
,
    // return the client scroll state data that will go in the hidden field

    get_scrollState : function()
    {
        var scrollingElement = this.get_elementContainer();
        if (!this.get_requiresContainerScroll())
            scrollingElement = this.get_element();
        
        return scrollingElement.scrollTop + ':' + scrollingElement.scrollLeft;
    }
,

...And there you have it. We can now support both IE6 and Opera (tested only in Opera 9.21 so far) when horizontal scrolling is disabled and scroll state preservation is enabled. Again, the only quirk is that Opera will show horizontal scroll bars even when horizontal scrolling is disabled. As far as I can tell, this is a necessity to support that browser at all. The good news is, if Opera ever decides to support the overflow-x and overflow-y CSS properties, this code should accommodate for it.

License

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