To read the article online, visit http://www.4GuysFromRolla.com/articles/091405-1.aspx

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
All of these controls are fundamentally the same - they all derive from the 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 ListItems. 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 ListItems. 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 RepeatInfo class rendering the "appropriate" <table> cell. I use this terminology because the Web control using the RepeatInfo class can specify how many items per row to render and other such formatting options through the RepeatDirection, RepeatLayout, and RepeatColumns properties. These property settings affect whether the RepeatInfo class is rendering a new table row for the item or not.

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 ListItems 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:

'Assuming items in DropDownList are: Red, Green, and Blue (in that order)
DropDownListID.Items(0).Attributes("style") = "background-color:red;"
DropDownListID.Items(1).Attributes("style") = "background-color:green;"
DropDownListID.Items(2).Attributes("style") = "background-color:blue;"

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 ListItems 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():

public void RenderItem(System.Web.UI.WebControls.ListItemType itemType, 
                       int repeatIndex, RepeatInfo repeatInfo, 
                       HtmlTextWriter writer)
{
	controlToRepeat.ID = repeatIndex.ToString(NumberFormatInfo.InvariantInfo);
	controlToRepeat.Text = this.Items[repeatIndex].Text;
	controlToRepeat.TextAlign = this.TextAlign;
	controlToRepeat.Checked = this.Items[repeatIndex].Selected;
	controlToRepeat.Enabled = this.Enabled;	
	
	controlToRepeat.Attributes.Clear();
	foreach (string key in Items[repeatIndex].Attributes.Keys)
		controlToRepeat.Attributes.Add(key, Items[repeatIndex].Attributes[key]);
		
	controlToRepeat.RenderControl(writer);
}

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:

HTML Portion:
<%@ Register TagPrefix="cc1" Namespace="skmExtendedControls" 
                Assembly="skmExtendedControls" %>

<form runat="server">
  Select the Peripherals You'd Like to Include in Your Purchase:<br />
  <cc1:skmCheckBoxList id="addOns" runat="server" />
</form>

Source Code Portion
Private Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) _
                                                   Handles MyBase.Load
   If Not Page.IsPostBack Then
      'Populate CheckBoxList from database
      'Also be sure to add on "None" option
   End If

    'Add none client-side script for CheckBoxList
    AddNoneScriptToCheckBoxList(addOns, 0)
End Sub

Private Sub AddNoneScriptToCheckBoxList(ByVal cbl As CheckBoxList, _
                                            ByVal noneIndex As Integer)
    'Now, Add client-side actions
    Dim i As Integer
    For i = 0 To cbl.Items.Count - 1
        If i = noneIndex Then
            cbl.Items(i).Attributes("onclick") = _
                 String.Format("skm_Uncheck('{0}', {1}, {2}, true);", _
                             cbl.ClientID, noneIndex, cbl.Items.Count)
        Else
            cbl.Items(i).Attributes("onclick") = _
                 String.Format("skm_Uncheck('{0}', {1}, {2}, false);", _
                             cbl.ClientID, noneIndex, cbl.Items.Count)
        End If
    Next

    'Finally, add the skm_Uncheck client-side function
    Const SKM_UNCHECK_KEY As String = "skm_Uncheck"
    If Not Page.IsClientScriptBlockRegistered(SKM_UNCHECK_KEY) Then
        Page.RegisterClientScriptBlock(SKM_UNCHECK_KEY, _
"<script language=""JavaScript"">" & vbCrLf & _
"function skm_Uncheck(cbID, offset, total, uncheckAllButThisOne) {" & _vbCrLf & _
"  if (uncheckAllButThisOne)" & vbCrLf & _
"    for (var i = 0; i < total; i++) { " & vbCrLf & _
"      var cb = document.getElementById(cbID + '_' + i);" & vbCrLf & _
"      if (cb && offset != i) cb.checked = false;" & vbCrLf & _
"    }" & vbCrLf & _
"  else {" & vbCrLf & _
"    var cb = document.getElementById(cbID + '_' + offset);" & vbCrLf & _
"    if (cb) cb.checked = false;" & vbCrLf & _
"  }" & vbCrLf & _
"} </script>")
    End If
End Sub
[View a Live Demo!]

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 ListItems 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 ListItems 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!

  • By Scott Mitchell


    Attachments:


  • Download the complete code (ZIP format)
  • View a Live Demo!
  • A Follow-Up Article: ListControl Items, Attributes, and ViewState

  • Article Information
    Article Title: ASP.NET.List Control Items and Attributes
    Article Author: Scott Mitchell
    Published Date: September 14, 2005
    Article URL: http://www.4GuysFromRolla.com/articles/091405-1.aspx


    Copyright 2014 QuinStreet Inc. All Rights Reserved.
    Legal Notices, Licensing, Permissions, Privacy Policy.
    Advertise | Newsletters | E-mail Offers