Introduction
Like Evyatar Ben-Shitrit says in the introduction to his article The ScrollableListBox Custom Control for ASP.NET 2.0, I too started out searching for an ASP.NET ListBox
control that supports horizontal scrollbars. Like him, I too found several solutions and code snippets (including his) which, as helpful as they were, did not quite suit my requirements.
At first I thought I just wanted a ListBox
that could scroll horizontally. Then I wanted a ListBox
that would also adjust its scroll position when the up and down arrow keys were used to cycle through the items. Then I wanted a ListBox
that would adjust its scroll position when someone used other keys (like letters and numbers) to cycle through the items. Once I had those requirements down, I wanted a ListBox
that would remember its vertical and horizontal scroll position and readjust itself after a postback (asynchronous or otherwise). Finally, I wanted this ListBox
to be able to exist as a normal Microsoft System.Web.UI.WebControls.ListBox
by default, so that the control itself would determine its own rendering strategy using its Width
, BorderXyz
, and Style
properties. The resulting control is what this article is about.
Background
I started out searching and immediately found lintom's article Finally a Horizontal Scroll Bar List Box in ASP.NET! Through that article I learned about the strategy of wrapping the ListBox inside a DIV
and setting its OVERFLOW
style to AUTO
. This made it possible to get the horizontal scrollbars needed. However, when using the keyboard to traverse the items in the ListBox
, the DIV
did not automatically scroll. When a user selected an item that was not in the scroll window, they were out of luck. My next search yielded Shiby Chacko's article Horizontal scroll bar in a combo box or a listbox with up/down arrow key press functionality. Shiby was helpful in showing how client script can be used to overcome the problem in lintom's article. The best part of Shiby's article though was a comment left at the bottom of it by a user named zivros. It contained client script that could be used to make the DIV
adjust its scroll position after any keypress event (not just the up & down arrow keys), even if the DIV
element received the keypress event in lieu of the ListBox
. That was a great contribution zivros, thanks for posting it. Finally, Evyatar's article addressed the idea of passing Width
, BorderXyz
, and other attributes from the ListBox
to its containing DIV
by overriding rendering methods in a custom server control.
What I can bring to this series of improvements upon a horizontally-scrollable ListBox
is my familiarity with using the ASP.NET AJAX Extensions to add client functionality to server controls. If you've never implemented the System.Web.UI.IScriptControl
interface or created client script prototypes for ASP.NET 2.0 server controls, don't worry, we're going to cover that. If you have a hard time following along, these 2 articles helped me get a good grasp on the core concepts:
A Comprehensive Solution
The first thing we need to do is create a new server control that extends Microsoft's System.Web.UI.WebControls.ListBox
control and implements the System.Web.UI.IScriptControl
interface. This is simple enough:
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
{
}
}
As for configurable properties, our ListBox
control will have three: HorizontalScrollEnabled
, ScrollTop
, and ScrollLeft
. Notice that the default value of the HorizontalScrollEnabled
property is false
, meaning it must be explicitly enabled in an ASPX page in order to take advantage of the new features we will create. The ScrollTop
and ScrollLeft
properties are here to provide simplified wiring between the server control and the corresponding client script so that the ListBox
will remember its scroll position between postbacks. Also note that if EnableViewState
is false on the control or one of its parents, the scroll properties won't be persisted between postbacks. I've also added a get
accessor to provide uniform access to the ScriptManager
, which will be needed later.
protected virtual ScriptManager ScriptManager
{
get { return ScriptManager.GetCurrent(Page); }
}
public virtual bool HorizontalScrollEnabled
{
set { this.ViewState["HorizontalScrollEnabled"] = value; }
get
{
object output = this.ViewState["HorizontalScrollEnabled"];
if (output == null)
output = false;
return (bool)output;
}
}
public virtual int ScrollTop
{
set { this.ViewState["ScrollTop"] = value; }
get
{
object output = this.ViewState["ScrollTop"];
if (output == null)
output = 0;
return (int)output;
}
}
public virtual int ScrollLeft
{
set { this.ViewState["ScrollLeft"] = value; }
get
{
object output = this.ViewState["ScrollLeft"];
if (output == null)
output = 0;
return (int)output;
}
}
At this point, most other AJAX Extensions primer articles I've read discuss how to implement the System.Web.UI.IScriptControl
interface in the extended control. This article will instead discuss the client script file first. If you're following along, you've already created a ListBox.cs file in your App_Code directory. In the root of your web site project, create a JScript file called ListBox.js. This file will automatically contain all the client script we need for the control when the ASPX page is rendered, as long as it contains a ScriptManager
. There will be no need to manually include any script tags in the ASPX page or write any ASPX codebehind whatsoever. We do have to carefully follow Microsoft's specification though, which entails the following major steps:
- Register the client control's namespace (1 line of code).
- Define the client control's class.
- Define the class prototype.
- Override / implement the
initialize
method of Sys.UI.Control
. - Override / implement the
dispose
method of Sys.UI.Control
. - Define the event handlers.
- Define the property
get
and set
methods.
- Optionally enable JSON Serialization (not required).
- Register the client control as a type that inherits from
Sys.UI.Control
(1 line of code). - Notify the ScriptManager that this script is loaded (1 line of code).
Type.registerNamespace('DanLudwig.Controls.Client');
DanLudwig.Controls.Client.ListBox = function(element)
{
DanLudwig.Controls.Client.ListBox.initializeBase(this, [element]);
this._horizontalScrollEnabled = null;
this._scrollTop = null;
this._scrollLeft = null;
}
DanLudwig.Controls.Client.ListBox.prototype =
{
initialize : function()
{
DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'initialize');
}
,
dispose : function()
{
DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'dispose');
}
,
,
set_horizontalScrollEnabled : function(value)
{
if (this._horizontalScrollEnabled !== value)
{
this._horizontalScrollEnabled = value;
this.raisePropertyChanged('_horizontalScrollEnabled');
}
}
,
get_horizontalScrollEnabled : function()
{
return this._horizontalScrollEnabled;
}
,
set_scrollTop : function(value)
{
if (this._scrollTop !== value)
{
this._scrollTop = value;
this.raisePropertyChanged('_scrollTop');
}
}
,
get_scrollTop : function()
{
return this._scrollTop;
}
,
set_scrollLeft : function(value)
{
if (this._scrollLeft !== value)
{
this._scrollLeft = value;
this.raisePropertyChanged('_scrollLeft');
}
}
,
get_scrollLeft : function()
{
return this._scrollLeft;
}
}
DanLudwig.Controls.Client.ListBox.descriptor =
{
properties: [
{name: '_horizontalScrollEnabled', type: Boolean}
,{name: '_scrollTop', type: Number }
,{name: '_scrollLeft', type: Number }
]
}
DanLudwig.Controls.Client.ListBox.registerClass(
'DanLudwig.Controls.Client.ListBox', Sys.UI.Control);
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
This file doesn't give us any real functionality yet, but it defines the necessary parts so that the client control (the client JavaScript code specific only to our ListBox
control) can interface with the ScriptManager
. Now, if we implement the System.Web.UI.IScriptControl
methods on the server control, and override both its OnPreRender
and Render
methods, we can finally wire up our server control properties to their corresponding client control properties.
protected virtual IEnumerable<ScriptReference> GetScriptReferences()
{
ScriptReference reference = new ScriptReference();
reference.Path = ResolveClientUrl("ListBox.js");
return new ScriptReference[] { reference };
}
IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
{
return GetScriptReferences();
}
protected virtual IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
ScriptControlDescriptor descriptor = new ScriptControlDescriptor(
"DanLudwig.Controls.Client.ListBox", this.ClientID);
descriptor.AddProperty("horizontalScrollEnabled",
this.HorizontalScrollEnabled);
descriptor.AddProperty("scrollTop", this.ScrollTop);
descriptor.AddProperty("scrollLeft", this.ScrollLeft);
return new ScriptDescriptor[] { descriptor };
}
IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
{
return GetScriptDescriptors();
}
protected override void OnPreRender(EventArgs e)
{
if (!this.DesignMode)
{
if (ScriptManager == null)
throw new HttpException(
"A ScriptManager control must exist on the current page.");
ScriptManager.RegisterScriptControl(this);
}
base.OnPreRender(e);
}
protected override void Render(HtmlTextWriter writer)
{
if (!this.DesignMode)
ScriptManager.RegisterScriptDescriptors(this);
base.Render(writer);
}
This should get rid of those pesky compiler errors telling you to implement the interface you declared for the class earlier. If you're following along with our own web site project, the code discussed so far should compile and render fine at this point (though there isn't any real functionality yet). The server control will set properties on the client control, but we still have to put them to use. Be warned though, from here forward, testing the control in an ASPX page may produce funky results until we reach the end of the article.
Render a DIV around the ListBox... or not
So far we've done the common things you'll need to do every time you create a new AJAX-enabled server control with client events. The client events are what will be specific to your control. In the case of our ListBox
, the client-specific events we want to handle and respond to are when users scroll the DIV
element that will contain the ListBox
, or when they use keystrokes to navigate the items. But where is this DIV
container? Do we need to include a DIV
element or an ASP Panel
control around our ListBox
in the ASPX page? Ideally, the control should use its HorizontalScrollEnabled
property to determine whether or not it should render a DIV
around itself. We also need to render a hidden Form field to pass client state data back to the server control on postbacks (to maintain scroll state).
Another thing we should do while we're rendering is pass certain characteristics of the ListBox
to the DIV
container. For example, if width or border styles are applied to our ListBox
from the ASPX page, we want to pass those up to the DIV
element. This can be a little tricky, and I might not have it exactly right, but the following code gets the job done for all of the styles I apply to this ListBox
in my applications. Read the code comments to see exactly what's going on while we're rendering the control.
public const string ContainerClientIdSuffix = "__LISTBOXHSCROLLCONTAINER";
public const string ScrollStateClientIdSuffix = "__LISTBOXHSCROLLSTATE";
protected override void Render(HtmlTextWriter writer)
{
if (!this.DesignMode)
ScriptManager.RegisterScriptDescriptors(this);
if (this.HorizontalScrollEnabled)
{
this.AddContainerAttributesToRender(writer);
writer.RenderBeginTag(HtmlTextWriterTag.Div);
}
base.Render(writer);
if (this.HorizontalScrollEnabled)
{
this.AddScrollStateAttributesToRender(writer);
writer.RenderBeginTag(HtmlTextWriterTag.Input);
writer.RenderEndTag();
writer.RenderEndTag();
}
}
protected virtual void AddContainerAttributesToRender(HtmlTextWriter writer)
{
if (HorizontalScrollEnabled)
{
writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID
+ ContainerClientIdSuffix);
writer.AddStyleAttribute(HtmlTextWriterStyle.Overflow, "auto");
writer.AddStyleAttribute(HtmlTextWriterStyle.Width,
this.Width.ToString());
Color borderColor = this.BorderColor;
if (!borderColor.Equals(Color.Empty))
writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor,
string.Format("#{0}", borderColor.ToArgb().ToString("x")
.Substring(2)));
BorderStyle borderStyle = this.BorderStyle;
if (!borderStyle.Equals(BorderStyle.NotSet))
writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle,
borderStyle.ToString());
Unit borderWidth = this.BorderWidth;
if (!borderWidth.Equals(Unit.Empty))
writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth,
borderWidth.ToString());
foreach (string key in this.Style.Keys)
{
writer.AddStyleAttribute(key, this.Style[key]);
}
this.Style.Remove(HtmlTextWriterStyle.Width);
this.Style.Remove("width");
this.Style.Remove(HtmlTextWriterStyle.BorderWidth);
this.Style.Remove(HtmlTextWriterStyle.BorderStyle);
this.Style.Remove(HtmlTextWriterStyle.BorderColor);
this.Style.Remove("border");
}
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
if (HorizontalScrollEnabled)
{
Unit originalWidth = this.Width;
Unit originalBorderWidth = this.BorderWidth;
BorderStyle originalBorderStyle = this.BorderStyle;
Color originalBorderColor = this.BorderColor;
this.Width = Unit.Empty;
this.BorderWidth = Unit.Empty;
this.BorderStyle = BorderStyle.NotSet;
this.BorderColor = Color.Empty;
base.AddAttributesToRender(writer);
writer.AddStyleAttribute("border", "0px none");
this.Width = originalWidth;
this.BorderWidth = originalBorderWidth;
this.BorderStyle = originalBorderStyle;
this.BorderColor = originalBorderColor;
}
else
{
base.AddAttributesToRender(writer);
}
}
protected virtual void AddScrollStateAttributesToRender(
HtmlTextWriter writer)
{
string fieldId = this.ClientID + ScrollStateClientIdSuffix;
writer.AddAttribute(HtmlTextWriterAttribute.Id, fieldId);
writer.AddAttribute(HtmlTextWriterAttribute.Name, fieldId);
writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
writer.AddAttribute(HtmlTextWriterAttribute.Value, string.Format(
"{0}:{1}", this.ScrollTop, this.ScrollLeft));
}
This is almost all of the code we need in the server control. The only thing that's missing is what the server control does when it receives new scroll position information from the client during a postback. As it turns out, all we need to do is extract the data passed by our hidden field and apply its values to our respective ScrollTop
and ScrollLeft
properties in the server control. Then, when IScriptControl.GetScriptDescriptors()
is executed, it will pass these values back to the client control when it's rendered again after the postback. I handled this by overriding the server control's OnLoad
event, since it happens at a point in the lifecycle where there is access to the Request.Form
collection, but before the IScriptControl.GetScriptDescriptors()
is called.
public static readonly char[] ClientStateSeparator = new char[] { ':' };
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
string scrollStateClientId = this.ClientID + ScrollStateClientIdSuffix;
object state = Page.Request.Form[scrollStateClientId];
if (state != null)
{
string[] scrollState = state.ToString().Split(
ClientStateSeparator,2);
int scrollTop = 0;
if (scrollState[0] != null && int.TryParse(scrollState[0],
out scrollTop))
this.ScrollTop = scrollTop;
int scrollLeft = 0;
if (scrollState[1] != null && int.TryParse(scrollState[1],
out scrollLeft))
this.ScrollLeft = scrollLeft;
}
}
Handle Client Events to Add ListBox Functionality... or not
The rest of the code we need to write is all in the ListBox.js file. All of the user interaction with the ListBox
will be handled in the client. This is where the code zivros posted comes in handy. The 3 client events that his code handled were the DIV
container's onkeydown
event, and the ListBox
's onkeydown
and onchange
events. We're going to handle these 3 events, but we're also going to handle the container's onscoll
event in order to update the hidden field we rendered.
In order to handle these events, we first have to add some code to our overridden initialize
and dispose
functions. (Note that from within the prototype definition, all functions are separated by commas.)
DanLudwig.Controls.Client.ListBox.prototype =
{
initialize : function()
{
DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'initialize');
if (this.get_horizontalScrollEnabled())
{
this._onkeydownHandler = Function.createDelegate(
this, this._onKeyDown);
this._onchangeHandler = Function.createDelegate(
this, this._onChange);
this._onkeydownContainerHandler = Function.createDelegate(
this, this._onContainerKeyDown);
this._onscrollContainerHandler = Function.createDelegate(
this, this._onContainerScroll);
$addHandlers(this.get_element(),
{
'keydown' : this._onKeyDown
,'change' : this._onChange
}, this);
$addHandlers(this.get_elementContainer(),
{
'keydown' : this._onContainerKeyDown
,'scroll' : this._onContainerScroll
}, this);
}
if (this.get_horizontalScrollEnabled())
{
var listBox = this.get_element();
var container = this.get_elementContainer();
container.originalListBoxSize = listBox.size;
if (this.get_element().options.length > this.get_element().size)
{
listBox.size = listBox.options.length;
container.style.height
= ((container.scrollHeight / listBox.size)
* (container.originalListBoxSize)) + 2 + 'px';
}
container.scrollTop = this.get_scrollTop();
container.scrollLeft = this.get_scrollLeft();
if (container.scrollWidth <= parseInt(container.style.width))
{
listBox.style.width = '100%';
listBox.style.height = container.scrollHeight + 'px';
container.style.height
= ((container.scrollHeight / listBox.size)
* (container.originalListBoxSize)) + 'px';
}
}
}
,
dispose : function()
{
$clearHandlers(this.get_element());
if (this.get_elementContainer() != null)
{
$clearHandlers(this.get_elementContainer());
}
DanLudwig.Controls.Client.ListBox.callBaseMethod(this, 'dispose');
}
,
get_elementContainer : function()
{
if (this.get_horizontalScrollEnabled()
&& this.get_element().parentNode != null)
{
return this.get_element().parentNode;
}
else
{
return null;
}
}
,
In the initialize
method, we created and added 4 new event handlers. Because the ListBox
is the server control that is directly connected to this client control, we access it using this.get_element()
. I've created a helper method called get_elementContainer()
that conveniently retrieves the DIV
client element for us as well. After creating the handlers, we add them respectively to the ListBox
and the DIV
using the $addHandlers
shortcut. The rest of the code follows zivros' pattern of initializing the client-side elements, with a few of my own tweaks. After the page is loaded, when any of our 4 client events are triggered, the client will execute the event-specific code defined below:
_onKeyDown : function(e)
{
if (this.get_element() && !this.get_element().disabled)
{
event.cancelBubble = true;
return true;
}
}
,
_onChange : function(e)
{
if (this.get_element() && !this.get_element().disabled)
{
updateListBoxScrollPosition(this.get_elementContainer(),
this.get_element(), null);
event.cancelBubble = true;
return true;
}
}
,
_onContainerKeyDown : function(e)
{
this.get_element().focus();
setTimeout("updateListBoxScrollPosition("
+ "document.getElementById('"
+ this.get_elementContainer().id
+ "'), document.getElementById('"
+ this.get_element().id
+ "'), "
+ this.get_elementContainer().scrollTop
+ ")", "5");
}
,
_onContainerScroll : function(e)
{
this.set_scrollState();
}
,
set_scrollState : function()
{
this.set_scrollTop(this.get_elementContainer().scrollTop);
this.set_scrollLeft(this.get_elementContainer().scrollLeft);
var stateValue = this.get_scrollTop() + ':' + this.get_scrollLeft();
this.get_elementState().value = stateValue;
this.raisePropertyChanged('_scrollState');
}
,
get_elementState : function()
{
if (this.get_horizontalScrollEnabled()
&& this.get_element().parentNode != null)
{
var childNodeIndex = -1;
for (i = 0; i<this.get_elementContainer().childNodes.length; i++)
{
if (this.get_elementContainer().childNodes[i].value != null)
{
childNodeIndex++;
if (childNodeIndex > 0)
{
childNodeIndex = i;
break;
}
}
}
return this.get_elementContainer().childNodes[childNodeIndex];
}
else
{
return null;
}
}
,
If you've been trying to get these functions to work before coming this far in the article, you're probably pretty frustrated. There's one more client-side function we need to add, and it's probably the most important of all. Again, thanks to zivros for the final piece that ties everything together and actually updates the DIV
element's scroll position. Place the updateListBoxScrollPosition
function just before the final line of code where Sys.Application.notifyScriptLoaded()
is called:
DanLudwig.Controls.Client.ListBox.registerClass(
'DanLudwig.Controls.Client.ListBox', Sys.UI.Control);
function updateListBoxScrollPosition(container, listBox, realScrollTop)
{
if (realScrollTop == null)
realScrollTop = container.scrollTop;
var scrollStepHeight = container.scrollHeight / listBox.size;
var minVisibleIdx = Math.round(realScrollTop / scrollStepHeight);
var maxVisibleIdx = minVisibleIdx + container.originalListBoxSize - 2;
if (listBox.selectedIndex >= maxVisibleIdx)
{
container.scrollTop
= (listBox.selectedIndex - container.originalListBoxSize + 2)
* scrollStepHeight;
}
else if (listBox.selectedIndex < minVisibleIdx)
{
container.scrollTop = listBox.selectedIndex * scrollStepHeight;
}
else
{
container.scrollTop = realScrollTop;
}
}
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
Voila
There is a lot of overlapping code here, so if you're copying and pasting it straight from this article into your own files, you may be running into the dreaded "DanLudwig is undefined" client error message. That error message, like so many others, can be misleading. I assure you, I am very well defined. However, the slightest syntax error in the ListBox.js file will cause the DanLudwig namespace to not be successfully registered. Sometimes it's because there aren't the required commas separating the different parts of the prototype declaration, and other times it's because of more obscure errors in the client script file. If you're having trouble copying and pasting, use the source files provided along with this article (but remember to change the reference in the GetScriptReferences()
method if you'll be testing it in a web site project).
Perfectionism is a Disease
Now we have a fairly intuitive ListBox
that can accommodate text too wide for our available screen real estate. As an added feature, it will also remember its last vertical and horizontal scroll positions and restore them after postbacks to preserve client state. But there are still more features to be added! For example, if your ListBox
's SelectionMode
is set to Multiple
, users should be able to use the CONTROL and SHIFT keys to select multiple items simultaneously. Try this: select an item, then scroll away so that it isn't visible anymore. now, try to use either the CONTROL or SHIFT key to select more than one item. As soon as you hit the key, the ListBox
scrolls back to the original item you selected. I'm not a user, but if I was, that would probably annoy me.
Another shortcoming of this control can be demonstrated by viewing it in IE7 and FireFox 1.5. In FireFox, when the mouse is positioned over the ListBox
, the mouse's wheel can be used to scroll through the items. In IE7 though, this only works if the mouse is hovered over one of the scrollbars. Again, were I a user, I might complain about this too. What's even more interesting is what happens when you view the control in IE6 and FireFox 1.0... you get the opposite behavior! In the older versions of these browsers, IE scrolls by default whereas FireFox does not. Ideally, we should code for the mouse wheel such that we can turn this type of behavior on and off from the ASPX page. Some users might prefer using the mouse wheel whereas others might think it interferes too much with page scrolling. If we make it configurable, we can at least provide ourselves with the possibility of letting users turn this feature on and off for themselves using ProfileCommon
.
You might also have noticed that our ListBox
doesn't remember scroll position if HorizontalScrollingEnabled
is set to false
(or not set at all). The two should be configured separately and operate independently from one another. That, and we can also remodel how we respond to events to use fewer CPU cycles on the client machine. This will be especially useful since FireFox 1.0 doesn't respond to the onscroll
event we attached to the DIV
container. There are probably more things that I haven't even thought of yet, but this article is already way too long. In my next, much shorter article, I'll begin discussing how to accommodate these additional requirements.
Points of Interest
This is only the second code article I've ever written. The first, now quite obsolete More Scrolling Function in Flash 5, I wrote way back in 2000. Some of the same mathematics needed to calculate the scroll position is similar, but even I find it odd that the only two articles I've ever written have to do with this rather mundane, yet UI-critical necessity. Perhaps this ListBox
control will also become obsolete if browsers ever evolve to the level of maturity where they can scroll ListBox
es sideways natively.
Exercises for You
I know this control isn't perfect, though I do think it's highly reusable when packaged correctly, and we're going to make it even better in the next article. Here are some considerations for you to tackle if you find this control falling short of your requirements:
- Even on asynchronous postbacks, there is sometimes screen flicker on the
ListBox
between the time the browser renders the ListBox
and the time the client code initializes the saved scroll position. Because its height is determined by its "size
" attribute (which corresponds to the "Rows
" attribute / property in the server control) by default, it's difficult to set the DIV
's scroll position before the ListBox
's size is programmatically set by the JavaScript. For my requirements the screen flicker is not a 100% reproducible problem, is barely noticeable, and for the most part, can be ignored. But can you figure out how to lose it altogether? - Although this control is compatible with FireFox, it always renders a vertical scrollbar even when the
ListBox
is bigger than the number of items it contains. Can you figure out how to get rid of this? - Create server properties to add CSS styles to the
DIV
scrollbars. - Create other properties to pass more CSS styling control from the
ListBox
to its DIV
container. - Clean up my C# code (I'm actually certified in Java and J2EE, not ASP.NET or C#
- I can't think of anything else. What could I be missing?