Introduction
I have been searching the web looking for a list box control which supports horizontal scroll and behaves exactly like the ASP.NET ListBox
. I found lintom's article which causes us to handle each ListBox
separately without providing reuse capabilities. I also found the XList control, which is a reusable control but it doesn't behave exactly like the ordinary ASP.NET ListBox
control. After a while, I decided to develop my own control. In this article, I'll present the ScrollableListBox
custom control I've created using C# and ASP.NET 2.0 technologies. The ScrollableListBox
, which derives from ListBox
, supports a horizontal scroll bar, and yet behaves like the ASP.NET ListBox
control.
[ToolboxBitmap(typeof(Evyatar.Web.Controls.ScrollableListBox),
"Evyatar.Web.Controls.ScrollableListBox.bmp")]
public class ScrollableListBox : ListBox
{
...
}
Adding a horizontal scroll bar
The main idea behind adding a horizontal scroll bar is to wrap the <SELECT>
HTML element (that represent the ListBox
) with a <DIV>
HTML element and let the <DIV>
to deal with the scroll bars. This approach leads us to put the Height
and the Width
attributes of the control on the <DIV>
element. To keep it behave like the ASP.NET ListBox
, we need to drill down and ask ourselves how browsers define the measurements of the ListBox
or the <SELECT>
HTML element? In the case of Internet Explorer, the answer to this question is: the width is defined by the longest ListItem
or <OPTION>
HTML element, unless there is an explicit definition of width. The height is defined by the Rows
property of the ListBox
which renders to the size
attribute of the <SELECT>
HTML element, unless there is an explicit definition of height.
In order to eliminate the Height
and the Width
attributes from the <SELECT>
HTML element, we should override the Height
and the Width
properties as follows:
public override Unit Width
{
get
{
object o = ViewState["Width"];
if (null == o)
return Unit.Empty;
return (Unit)o;
}
set
{
if (value.Value < 0)
{
throw new ArgumentOutOfRangeException("value");
}
ViewState["Width"] = value;
}
}
public override Unit Height
{
get
{
object o = ViewState["Height"];
if (null == o)
return Unit.Empty;
return (Unit)o;
}
set
{
if (value.Value < 0)
{
throw new ArgumentOutOfRangeException("value");
}
ViewState["Height"] = value;
}
}
In order to wrap the <SELECT>
HTML element with the <DIV>
HTML element, we should override the Render
method as follows:
protected override void Render(HtmlTextWriter writer)
{
writer.Write(string.Format("<div style='OVERFLOW-X:" +
" auto;OVERFLOW-Y: auto; {0}' id='{1}'>",
AddStyleAttributesToRender(), ClientID + "_div"));
base.Render(writer);
writer.Write("</div>");
}
private string AddStyleAttributesToRender()
{
StringBuilder sb = new StringBuilder("", 16);
if (!Width.IsEmpty)
sb.Append(string.Format("WIDTH: {0};",
Width.ToString(CultureInfo.InvariantCulture)));
if (!Height.IsEmpty)
sb.Append(string.Format("HEIGHT: {0};",
Height.ToString(CultureInfo.InvariantCulture)));
return sb.ToString();
}
The Render
method firstly renders the start tag of the <DIV>
HTML element, indicating the height and the width of the <DIV>
and its horizontal and vertical scrollbars behavior when the <DIV>
content overflows. Then, we render the <SELECT>
HTML element by calling the Render
method of the ListBox
control (which is the base class). Here, we should be aware that the ListBox
's Render
method will not render the Height
and the Width
attributes. The reason for that is while we are setting a value to the Height
or the Width
properties, we aren't passing this value to the base class. Finally, we are rendering the end tag of the <DIV>
HTML element.
We are almost done, but we still need to deal with the browser definition of the control measurements:
Scenario | Current behavior | Expected behavior |
There is no explicit definition of the control Width . | The <SELECT> width is determined by the longest <OPTION> element. But, the <DIV> width is determined to be 100% (the default behavior of Internet Explorer). | The <DIV> width should be determined like the <SELECT> HTML element. |
There is explicit definition of the control Width . | The <SELECT> width is determined by the longest <OPTION> element. The <DIV> width is determined according to the control Width definition. In case the longest <OPTION> element is wider than the explicit definition of the control Width , a horizontal scroll bar will appear. | In case the longest <OPTION> element is wider than the <DIV> element, the current behavior is also the expected behavior. Otherwise, the <SELECT> element should be stretched to fill the <DIV> area. |
There is no explicit definition of the control Height . | The <SELECT> height is determined by the Rows property of the control. The <DIV> height is determined by its content. In case the Rows property is less than the number of list items, a vertical scroll bar will appear on the <SELECT> HTML element. | In case the amount of list items is less than the Rows definition, no vertical scroll bar should appear. Otherwise, a vertical scroll bar should appear on the <DIV> element instead of the <SELECT> element. The amount of visible items should be according to the Rows property even though there is a horizontal scroll bar (that may appear and consume area). |
There is explicit definition of the control Height . | The <SELECT> height is determined by the Rows property of the control. The <DIV> height is determined by the Height property of the control. In case the Rows property is less than the number of list items, a vertical scroll bar will appear on the <SELECT> HTML element. In case the <DIV> height is exceeding the <SELECT> height, there is a gap between the <DIV> and its contents. | The <SELECT> element shouldn't contain vertical scroll bars. In addition, no gap should be there between the <DIV> element and its contents. |
To solve the above issues, we need to know what the client width and the client height of the rendered <SELECT>
HTML element is. That information is only available in the client side after the control is rendered. So, in order to solve this issue, we are going to inject a JavaScript code that is invoked right after the control is rendered.
First of all, let's look at the JavaScript code that will invoked. The __ScrollableListBoxRefineHeightAndWidth
function tests the above scenarios, and makes sure the the control will behave as expected.
function __ScrollableListBoxRefineHeightAndWidth(list_id)
{
var list = document.getElementById(list_id);
var div = document.getElementById(list_id + '_div');
if (div.style.height)
{
list.size = list.options.length;
while (list.clientHeight < div.clientHeight)
list.size = list.size + 1;
}
if (div.style.width)
{
if (div.clientWidth > list.clientWidth)
{
list.style.width = div.clientWidth;
}
if (!div.style.height)
{
div.style.height = list.offsetHeight +
(list.offsetHeight - div.clientHeight);
if (list.size < list.options.length)
{
list.size = list.options.length;
}
else
{
div.style.overflowY = 'hidden';
}
}
}
else
{
div.style.width = list.offsetWidth;
div.style.overflowX = 'hidden';
}
}
Now, we should inject the above code into the client browser, and inject the call to the __ScrollableListBoxRefineHeightAndWidth
function right after the control rendering. In order to keep a clean separation between the server side code and the client side code, I put the __ScrollableListBoxRefineHeightAndWidth
on a separate file called ScrollableListBox.js. That file is compiled as an embedded resource in my control library project. Now, all we have to do is to register the include script to the ScrollableListBox.js file, and to register the startup script that calls to the __ScrollableListBoxRefineHeightAndWidth
function:
protected override void OnInit(EventArgs e)
{
ClientScriptManager cs = Page.ClientScript;
Type rsType = this.GetType();
if (!cs.IsClientScriptIncludeRegistered("ScrollableListBox"))
{
cs.RegisterClientScriptInclude("ScrollableListBox",
cs.GetWebResourceUrl(rsType,
"Evyatar.Web.Controls.JavaScript.ScrollableListBox.js"));
}
cs.RegisterStartupScript(rsType, "ScrollableListBoxStartup" + this.UniqueID,
string.Format("__ScrollableListBoxRefineHeightAndWidth('{0}');",
this.ClientID), true);
base.OnInit(e);
}
Resuscitation of appearance properties
When I examined the ListBox
and its rendering on Internet Explorer, I noticed that the BorderColor
, BorderStyle
, and BorderWidth
properties have no effect on the appearance of the ListBox
control, even though they still render within the STYLE
attribute of the <SELECT>
HTML element. This is the reason why those properties are not supported at design time.
Because the ScrollableListBox
is rendered to the <SELECT>
HTML element wrapped by the <DIV>
HTML element, we want to render the appearance properties within the STYLE
attribute of the <DIV>
HTML element. In addition, we want to add design time support to those properties.
[Browsable(true)]
[Category("Appearance")]
[Description("Color of the border around the control"),
DefaultValue(typeof(Color),"")]
[TypeConverter(typeof(WebColorConverter))]
public override Color BorderColor
{
get
{
object o = ViewState["BorderColor"];
if (null == o)
return Color.Empty;
return (Color)o;
}
set
{
ViewState["BorderColor"] = value;
}
}
[Browsable(true)]
[DefaultValue(BorderStyle.NotSet)]
[Category("Appearance")]
[Description("Style of the border around the control")]
public override BorderStyle BorderStyle
{
get
{
object o = ViewState["BorderStyle"];
if (null == o)
return System.Web.UI.WebControls.BorderStyle.NotSet;
return (BorderStyle)o;
}
set
{
ViewState["BorderStyle"] = value;
}
}
[Browsable(true)]
[Description("Width of the border around the control")]
[Category("Appearance")]
[DefaultValue(typeof(Unit), "")]
public override Unit BorderWidth
{
get
{
object o = ViewState["BorderWidth"];
if (null == o)
return Unit.Empty;
return (Unit)o;
}
set
{
if (value.Value < 0)
{
throw new ArgumentOutOfRangeException("value");
}
ViewState["BorderWidth"] = value;
}
}
Now, let's fix the AddStyleAttributesToRender
method in order to support the appearance properties as well:
private string AddStyleAttributesToRender()
{
StringBuilder sb = new StringBuilder("", 16);
if (!Width.IsEmpty)
sb.Append(string.Format("WIDTH: {0};",
Width.ToString(CultureInfo.InvariantCulture)));
if (!Height.IsEmpty)
sb.Append(string.Format("HEIGHT: {0};",
Height.ToString(CultureInfo.InvariantCulture)));
if (! BorderColor.IsEmpty)
sb.Append(string.Format("BORDER-COLOR: {0};",
ColorTranslator.ToHtml(BorderColor)));
if (!BorderWidth.IsEmpty)
sb.Append(string.Format("BORDER-WIDTH: {0};",
BorderWidth.ToString(CultureInfo.InvariantCulture)));
if (BorderStyle != BorderStyle.NotSet)
sb.Append(string.Format("BORDER-STYLE: {0};", BorderStyle.ToString()));
return sb.ToString();
}
Using the code
In case you are an ASP.NET programmer, you should be familiar with the ListBox
control. You will probably be happy to notice that the ScrollableListBox
control is derived from the ListBox
control. Therefore, all the capabilities of ListBox
, such as data binding, ViewState, event notifications, etc., will remain as is when you are using the ScrollableListBox
control. All you need to do in order to use the ScrollableListBox
control is to add it into the toolbox and drag it into the page. From now on, you can use it the same way you use the ListBox
control.
About the demo project
The demo project lets you examine and compare the behavior of the ListBox
control and the ScrolllableListBox
control. Run the project, and use the demo project page to define the control properties such as Height
, Width
, Rows
, BorderColor
, BorderStyle
, and BorderWidth
. In addition, try to add long items to the lists when you are defining an explicit Height
and when you are not doing that.
Summary
The ScrollableListBox
, which derives from ListBox
, supports a horizontal scroll bar, and yet behave like the ASP.NET ListBox
. In addition, it resuscitates a few appearance properties which are not supported by the ASP.NET ListBox
control.