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.
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;
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
.
protected virtual void AddContainerAttributesToRender(HtmlTextWriter writer)
{
if (this.HorizontalScrollEnabled || this.ScrollStateEnabled)
{
writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID
+ ContainerClientIdSuffix);
if (this.HorizontalScrollEnabled)
{
writer.AddStyleAttribute(HtmlTextWriterStyle.Overflow, "auto");
}
else if (this.RequiresContainerScroll)
{
writer.AddStyleAttribute(HtmlTextWriterStyle.Overflow, "auto");
writer.AddStyleAttribute(HtmlTextWriterStyle.OverflowX, "hidden");
}
if (this.RequiresContainerScroll)
{
writer.AddStyleAttribute(HtmlTextWriterStyle.Width,
this.Width.ToString());
}
}
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
if (RequiresContainerScroll)
{
}
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.
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 };
}
DanLudwig.Controls.Client.ListBox = function(element)
{
DanLudwig.Controls.Client.ListBox.initializeBase(this, [element]);
this._requiresContainerScroll = null;
this._scrollStateEnabled = null;
this._horizontalScrollEnabled = null;
this._scrollTop = null;
this._scrollLeft = null;
}
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:
_initializeEvents : function()
{
if (this.get_requiresContainerScroll())
{
}
}
,
_initializeUI : function()
{
var listBox = this.get_element();
var container = this.get_elementContainer();
if (this.get_requiresContainerScroll())
{
}
if (this.get_scrollStateEnabled())
{
this._restoreScrollState();
}
if (this.get_requiresContainerScroll())
{
}
}
,
_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();
}
,
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.