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

The ScrollableListBox Custom Control for ASP.NET 2.0

4.96/5 (51 votes)
13 Jun 20067 min read 2   2K  
A ListBox control with horizontal scrolling capability.

ScrollableListbox

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.

C#
[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:

C#
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:

C#
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:

ScenarioCurrent behaviorExpected 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.

JavaScript
function __ScrollableListBoxRefineHeightAndWidth(list_id)
{
    var list = document.getElementById(list_id);
    var div = document.getElementById(list_id + '_div');
    if (div.style.height) 
    {
        // div has height we will expand the list to fit the div's height.
        // In addition we will make sure the vertical
        // scroll bar of the list will not
        // displayed
        list.size = list.options.length;
        while (list.clientHeight < div.clientHeight)
            list.size = list.size + 1;
    }
    if (div.style.width)
    {   
        if (div.clientWidth > list.clientWidth)
        {
            // div is wider than list. We will
            // wide the list to fill the div area
            list.style.width = div.clientWidth;
        }
        if (!div.style.height)
        {
            // div have no height this is mean the height is defined by list rows.
            // We will set the div height to be like the list height.
            div.style.height = list.offsetHeight + 
                     (list.offsetHeight - div.clientHeight);
             
            // now after we set the div height,
            // we need to remove the vertical scroll bar of the list 
            // (it will exists when the size of the list is less than the number 
            // the amount of list's items).
            // We are doing it by set the list rows 
            if (list.size < list.options.length)
            {
                list.size = list.options.length;
            }
            else
            {
                // here we will not need the vertical
                // scroll bar. therefore we will hide it.
                div.style.overflowY = 'hidden';
            }
        }
    }
    else 
    {
        // div has no width. This is mean the width will be set 
        // by the longest list's item. Therefore we will
        // set the div width like the list offset.
        div.style.width = list.offsetWidth;
        // here we will not need the horizontal scroll bar.
        // therefor we will hide it.
        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:

C#
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.

C#
[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:

C#
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here