List Control Items and Attributes
By Scott Mitchell
Introduction
ASP.NET 1.x provides four Web controls that serve as list controls:
- The DropDownList,
- The CheckBoxList,
- The RadioButtonList, and
- The ListBox
System.Web.UI.WebControls.ListControl
class and all display a variable number of ListItem
instances. The four controls differ in how they render
themselves and their associated ListItem
s. The CheckBoxList, for example, renders itself as an HTML
<table>
with a CheckBox Web control inside of each table cell. The DropDownList renders itself as an
HTML <select>
element, with each ListItem
rendered as an <option>
element.
One nuisance shared among all controls is the fact that their items don't render attributes. For example, imagine that you
wanted to display a CheckBoxList with particular CheckBoxes in the list displayed using a certain CSS class; or maybe when
a particular RadioButton in a RadioButtonList control is selected, you want to run some client-side JavaScript. These
are features that would be typically set using the CheckBox or RadioButton Web control's Attributes
collection.
Unfortunately, when a list control is rendered it does not render the attributes of the items.
In this article we'll look at how, exactly, these list controls are rendered. We'll then see how to extend the list control
classes to enable attributes of the control's ListItem
instances. The article concludes with a real-world demo
that illustrates how to have a CheckBoxList with a "None" checkbox option that, if checked, uses client-side script to
automatically uncheck all other CheckBoxes in the list. Read on to learn more!
How List Controls are Rendered
In ASP.NET 1.x the RadioButtonList and CheckBoxList render a bit differently than the ListBox and DropDownList. Both the RadioButtonList and CheckBoxList use the
System.Web.UI.WebControls.RepeatInfo
class to render their ListItem
s.
The RepeatInfo
class renders a set of items in an HTML <table>
either vertically or
horizontally. Understand that the RepeatInfo
class just provides the rendering for the outter HTML
<table>
; the rendering for the inner elements is handled by a passed-in object that implements
the IRepeatInfoUser
interface. The graphic below illustrates the workflow of rendering data with
the RepeatInfo
class:

As you can see, the RepeatInfo
class delegates many aspects of rendering to the supplied IRepeatInfoUser
object passed in to its RenderRepeater()
method. The IRepeatInfoUser
interface defines
properties like HasHeader
, HasFooter
, and RepeatedItemCount
, and a method to render
the actual item, RenderItem()
.
The CheckBoxList and RadioButtonList implement the IRepeatInfoUser
interface themselves. Therefore,
what these Web controls actually pass in to the RepeatInfo
class's
RenderRepeater()
method is an instance of themselves. That is, both the CheckBoxList and RadioButtonList
implement the needed IRepeatInfoUser
properties and methods. When calling the RenderRepeater()
method, they pass a copy of themselves in. Therefore, during the rendering of the list control's items, the
the list control's own RenderItem()
method is called to render the particular item. Specifically,
the RenderItem()
adds a CheckBox or RadioButton instance for the CheckBoxList or RadioButtonList, respectively.
Don't Forget the DataList! |
---|
In addition to the RadioButtonList and CheckBoxList,
the only other built-in ASP.NET Web control that implements the IRepeatInfoUser interface is the DataList.
All three of these controls use the RepeatInfo class interally for rendering. Rather than rendering a CheckBox
or RadioButton, the DataList renders the corresponding DataListItem .
Additionally, in the diagram above I refer to the |
The DropDownList and ListBox are not rendered using the RepeatInfo
class. Rather, their Items
are emitted as <option>
elements directly within their RenderContents()
method. Specifically,
the Items
property is enumerated, and for each ListItem
in the collection an <option>
tag is emitted with the appropriate text and value attributes. The main difference between the DropDownList and ListBox is
that the ListBox adds the multiple
attribute to the rendered <select>
tag, which indicates that
the end user may select multiple items from the list.
Why Attributes Cannot Be Applied to ListItem
s of a List Control
If you have ever wanted to apply attributes to an item in a list control, you will have no doubt been thoroughly disappointed. For example, imagine that you wanted to create a DropDownList with three items: Red, Green, and Blue. Furthermore, you wanted the background color of each of those three items to be, respectively, red, green, and blue. You might try to use code like the following:
|
However, if you attempt to view this page through a browser all list items will have their white background color. A View/Source
reveals that the style
attribute is not even rendered to the <option>
element. Similarly,
if you try to specify attributed through the declarative syntax, those, too, are lost. What gives?
Unfortunately none of the list controls render their items' attributes. This is clearly a known bug, as there are plenty
of discussions on this topic on the
newsgroups. If you use Reflector to poke around the source code of
the list controls you'll find that they simply omit any sort of writing of the Attributes
. Part of this is probably
due to the fact that the ListItem
class, which is the class that represents each instance of a list control prior
to being rendered. The Attributes
collection is defined in the System.Web.UI.WebControls.WebControl
class and automatically is persisted to view state, so that it's values persist across postbacks. Part of the problem lies
in that the ListItem
class does not derive from WebControl
. Furthermore, while it does
have an Attributes
property, the values are not persisted to view state.
Fixing this Bug - Extending the List Controls to Render their Item's Attributes
There are really two bugs here - the first is that the list controls don't render their items' attributes. The second is that the
ListItem
class does not save its values in ViewState (hence they are lost across postback).
In this article I am only going to talk about overcoming the first such bug; perhaps a future article will detail cracking
the second one! (UPDATE: There is now an article on this very topic!
See ListControl Items, Attributes, and ViewState for more information.) Additionally, this article looks only at fixing the first bug in the CheckBoxList. Due to their similarities,
you should be able to fairly easily move the logic/code over to work with the RadioButtonList. If you need to set attributes
for the items in a DropDownList or ListBox, check out Victor Garcia Aprea's
newsgroup post on the code necessary to fix this issue with the DropDownList.
The solution we'll examine in this article is to extend the list control class, updating the code that renders the item so that it includes rendering of the attributes. (This is the approach Victor uses in his newsgroup post.) An alternative workaround is offered from Microsoft's Knowledgebase article Q309338, which is to simply ditch the list controls and use HTML controls instead when you need to set attributes on a list item-by-list item basis.
My approach for the CheckBoxList, I'm afraid, is pretty ungraceful and ugly. The actual CheckBoxList class has a private member variable
of type CheckBox named controlToRepeat
. It's this member variable that's used to render each item in the CheckBox list.
In the RenderItem()
method the controlToRepeat
variable's ID
property is set to the
current index of the repeater item being rendered, the Text
and Checked
properties are set based
on the corresponding ListItem
s Text
and Selected
properties, and the
TextAlign
and Enabled
properties are set based on the CheckBoxList's
TextAlign
and Enabled
property values. We need to tap into this method and also assign the attributes
of the current ListItem
to controlToRepeat
.
Unfortunately the CheckBoxList's RenderItem()
method is a private, non-overridable method, meaning we can't
simply override the method in our derived class. Instead, our derived class must implement IRepeatInfoUser
and provide the code for all of the IRepeatInfoUser
members. (Fortunately all of these methods, save
the RenderItem()
method, are simple one-liners.) This means, too, that we'll have to have our extended class
include its own copy of controlToRepeat
, since our instance of RenderItem()
can't access the
base class's private instance of controlToRepeat
. This carries with it another tidbit of bad news: now we
have to also override all methods in CheckBoxList that use to the private member controlToRepeat
, since
we want those implementations to use our extended class's instance of controlToRepeat
as opposed to the
base class's. These methods, though they must be included, can have their code simply cut and pasted
from the code listing provided by Reflector. (I told you this would be ungraceful and ugly!)
Rather than dump the entire code here, I'll leave that for the download, which can be found at the end of this article. Here's
the most germane part, the implementation of the RenderItem()
method. The bold text is the code that
I added that wasn't found in the original implementation of RenderItem()
:
|
As you can see, my added code clears out the Attributes
collection of controlToRepeat
and then
repopulates it with the attributes of the current ListItem
(if any).
An Example of Using ListItem
-Level Attributes
This whole investigation into attributes at the
ListItem
level started when my wife was having difficulty
creating a CheckBoxList that, in addition to a number of checkboxes bound from the database, included a "None" option.
The client-side behavior she wanted was that when the "None" checkbox was clicked all selected checkboxes would be unchecked;
similarly, if the "None" checkbox was checked and a different checkbox was checked, the "None" checkbox would automatically
be unchecked. This exercise would have been a trivial one for her if it wasn't for the fact that her attempts at adding
a client-side onclick
attribute failed because of a CheckBoxList not rendering its items' attributes.
After she did a bit of searching online she quickly realized that this was a known bug, at which point she approached me and said, "I have a great idea for a 4Guys article." With this enhanced CheckBoxList control my wife (and you!) can now add attributes to the CheckBoxList's items. Here's a sample that illustrates how to exhibit the client-side behavior just discussed:
|
The Page_Load
event handler populates the items in the CheckBoxList on the first page visit. This will typically
be done through some call to the database. Just be sure to add a "None" option to the CheckBoxList, which can be accomplished
by Insert()
ing a new ListItem()
into the Items
collection; see
Adding a Default ListItem in a Databound Listbox in ASP.NET for
more information on programmatically adding ListItem
s to a list control.
Next, a call to AddNoneScriptToCheckBoxList()
is made, passing in the CheckBoxList instance and the
offset of the "None" checkbox within the CheckBoxList. (For this example, I added the "None" CheckBox to the start of the
list, so it's offset is 0.) Note that this call is outside of the If Not Page.IsPostBack
conditional. That's
because the attribute settings on the CheckBoxList's items are not serialized to view state, meaning that they're
lost on each postback. Hence, the client-side script needs to be re-added on each and every postback.
The AddNoneScriptToCheckBoxList()
method is where the needed client-side script is added to each of the
CheckBoxList's items. The first loop iterates through each of the ListItem
s in the CheckBoxList, setting
the client-side onclick
attribute to a call to the JavaScript function skm_Uncheck
. This function
is injected into the page in the RegisterClientScriptBlock()
call further down. It takes in the ID
of the CheckBoxList along with the offset of the checkbox that was checked, the total number of checkboxes in the CheckBoxList,
and whether or not the selected checkbox should be unchecked or if all other checkboxes should be unchecked. For more
information on creating client-side script from an ASP.NET page's server-side source code portion, be sure to read
Working
with Client-Side Script.
Conclusion
In this article we saw why ASP.NET 1.x's list controls don't render their items' attributes. This is a bug, a problem with the build-in ASP.NET controls. While the simplest workaround is to simply use HTML controls when you need to tinker with a list control's items, you can create your own Web control class that does successfully render the attributes, as we saw in this article.
This article showed, specifically, how to extend the CheckBoxList Web control so that its items' attributes are rendered.
While we accomplished this there still is the annoyance of how the items' attributes aren't persisted to view state. This is
a topic that will, perhaps, be touched on in a future article. While annoying, the view state issue can be worked around - just
make sure to write to the items' Attributes
collection on every page load, not just the first one.
Armed with this enhanced CheckBoxList control we were able to build a demo that utilizes client-side events on a checkbox-by-checkbox basis in the CheckBoxList. Specifically, we saw how to add a "None" button to a CheckBoxList that, when clicked, will automatically unselect all other CheckBoxList CheckBoxes. Be sure to check out the live demo.
Happy Programming!
Attachments: