CoreCoder

LANGUAGES: C# | VB.NET

ASP.NET VERSIONS: 1.x | 2.0

 

A Very Special CheckBoxList Control

Hook Up the Rendering System of List Controls

 

By Dino Esposito

 

The whole community of developers heartily welcomed the introduction of the CheckBoxList control in ASP.NET 1.0. The control is easy to use, data-bound, and, most importantly, addresses a real need of most applications displaying a checkable list of options possible with minimal effort. Since ASP.NET 1.0, I believe I ve used the control in hundreds of pages with virtually no hassle and no worries; in fact, never even wishing for additional capabilities. To reinforce this statement, consider the fact that the control in ASP.NET 2.0 is nearly the same as it was in ASP.NET 1.0.

 

So apparently nobody ever complained about the CheckBoxList control; nobody except one of my clients. But I must confess, it took me a while to realize what she was requesting. Basically, she wanted a more appealing control capable of rendering checked items according to a user-defined style. More importantly, she wanted this to occur on both the client and the server.

 

Being quite familiar with controls like DataGrid and GridView, I thought it was going to be a very simple task; easy money for just a little bit of work. Was I ever wrong!

 

Compared to DataGrid, for example, the CheckBoxList control is a sort of black-box. You insert bound data and get some markup. There s little knowledge of what happens inside the box, and, more importantly, there s no easy way to crack open the box and put your hands on the plumbing.

 

This article is the detailed summary of my wandering around the internals of list controls.

 

ASP.NET List Controls

Like all list-bound controls, the CheckBoxList control derives from ListControl. In ASP.NET, list-bound controls include DropDownList, RadioButtonList, and ListBox. All these controls have one common aspect (as far as rendering is concerned): They define a private member that represents the control to repeat, and repeat the control for each bound data item. The rendering mechanism, however, is closed, and no events are fired to the outside world to indicate the various steps. The DataGrid control has a pair of ItemCreated and ItemDataBound events that inform page developers about what is going on in the control. By hooking up these events from within a code-behind class, you can modify the style and contents of a particular item. Injecting item-specific script code is possible too, because both events are fired at the time the markup for the item is being generated.

 

With similar tools available for list controls, meeting and even exceeding the client s expectation wouldn t have been a big issue. Unfortunately, list controls don t natively provide such facilities.

However, the .NET Framework does support inheritance; with a bit of work, and some control development background, you can build your own replacement for the CheckBoxList control that provides powerful additional features.

 

Prototyping a New CheckBoxList Control

Compared to the ASP.NET built-in control, the CheckBoxList control that my client wanted to incorporate in all her applications has just one extra feature. It sports a new SelectedItemStyle property for developers to specify the style of checked items. Style properties in ASP.NET controls have a rather default implementation, as shown in Figure 1.

 

public TableItemStyle SelectedItemStyle

{

 get

 {

   if (_selectedItemStyle == null)

       _selectedItemStyle = new TableItemStyle();

   if (IsTrackingViewState)

        ((IStateManager)_selectedItemStyle).TrackViewState();

   return _selectedItemStyle;

 }

}

Public ReadOnly Property SelectedItemStyle

 As TableItemStyle

 Get

     If _selectedItemStyle Is Nothing Then

         _selectedItemStyle = New TableItemStyle()

     End If

     If IsTrackingViewState Then

        Dim sm As IStateManager

        sm = DirectCast(_selectedItemStyle, IStateManager)

        sm.TrackViewState();

     End If

     return _selectedItemStyle;

 End Get

End Property

Figure 1: Style properties in ASP.NET controls have a rather default implementation.

 

A control s style property is often an instance of the TableItemStyle class. The TableItemStyle class incorporates viewstate management and knows how to serialize and deserialize its contents to and from the viewstate. A style property also requires a couple of key attributes: PersistenceMode and DesignerSerializationVisibility.

 

The DesignerSerializationVisibility attribute specifies how the Visual Studio 2005 designer generates code for the object. The typical value is shown here:

 

[DesignerSerializationVisibility(

 DesignerSerializationVisibility.Content)]

 

The second attribute, PersistenceMode, specifies how an ASP.NET control property is persisted declaratively in an .aspx file. Typically, you d use the value PersistenceMode.InnerProperty for a style property. This means that any value a page author sets for the style in the Visual Studio 2005 designer is serialized with a child tag:

 

<x:CheckBoxList runat="server" ...>

    <SelectedItemStyle ForeColor="blue" ... />

    :

</x:CheckBoxList>

 

The InnerProperty setting doesn t work for the CheckBoxList control because all list controls feature an Items collection property decorated with the PersistenceMode.InnerDefaultProperty attribute. A control that has a default persistence property doesn t allow child tags, except the tag that identifies the property. The following is the setting that works for the new CheckBoxList control and guarantees that Visual Studio 2005 settings are saved and retrieved over postbacks:

 

[PersistenceMode(PersistenceMode.Attribute)]

 

Some changes are required to the infrastructure of the CheckBoxList control to take the SelectedItemStyle property into due account.

 

Overriding GetItemStyle

Looking under the hood of the ASP.NET CheckBoxList control, you ll see a protected virtual method that promises help. The method is named GetItemStyle and has the following signature:

 

protected virtual Style GetItemStyle(

   ListItemType itemType, int repeatIndex)

Protected Overridable Function GetItemStyle( _

   ByVal itemType As ListItemType, _

   ByVal repeatIndex As Integer) As Style

 

GetItemStyle returns the style object to be used for the specified list item. The idea is to check the selected state of each item being rendered and apply the selected-item style, if appropriate. The code is shown in Figure 2.

 

protected override Style GetItemStyle(

 ListItemType itemType, int repeatIndex)

{

   ListItem item = Items[repeatIndex];

   if (item.Selected)

       return SelectedItemStyle;

   else

       return base.GetItemStyle(itemType, repeatIndex);

}

Protected Overridable Function GetItemStyle( _

 ByVal itemType As ListItemType, _

 ByVal repeatIndex As Integer) As Style

   Dim item As ListItem = Items(repeatIndex)

   If item.Selected Then

       Return SelectedItemStyle

   Else

       Return MyBase.GetItemStyle(itemType, repeatIndex)

   End IF

End Function

Figure 2: GetItemStyle returns the style object to be used for the specified list item.

 

Figure 3 shows the rich CheckBoxList control in action. The user checks as many items as needed; when the page is refreshed, the selected-item style settings are applied:

 

<x:CheckBoxList ID="CheckBoxList1" runat="server"

 SelectedItemStyle-ForeColor="Blue"

 SelectedItemStyle-BackColor="#FFFF80">

 


Figure 3: The new CheckBoxList control in action.

 

The preceding code snippet also illustrates the effect of the PersistenceMode.Attribute setting. The values of the SelectedItemStyle property are now saved as attributes in the CheckBoxList control tag.

 

One Step Further

My client first liked this code, but then, one second later, she realized that more functionality was absolutely necessary: Is it possible to apply the style as soon as the element is checked or unchecked? For this to happen, a bit of script code is required. The problem, though, was not with the script code it required just a few relatively simple lines but with the internal structure of the CheckBoxList control that tends to hide the steps where data-bound items are rendered out. Rather, you need to handle the onclick event on individual checkboxes. But how?

 

The CheckBoxList control (as well as other list controls) implements the IRepeatInfoUser interface. The interface defines the members that must be implemented by a control that repeats a list of items. The interface has one method that is key here: the RenderItem method. The CheckBoxList control implements RenderItem using the following method:

 

protected virtual void RenderItem(

    ListItemType itemType,

    int repeatIndex,

    RepeatInfo repeatInfo,

    HtmlTextWriter writer)

Protected Overridable Sub RenderItem( _

  ByVal itemType As ListItemType, _

  ByVal repeatIndex As Integer, _

  ByVal repeatInfo As RepeatInfo, _

  ByVal writer As HtmlTextWriter)

 

As you can see, the method is virtual and, therefore, can be overridden in a derived class. Is this the key to the whole affair? Well, not exactly. To override a method, you first need to know what the method does in the base class and how it does it. Looking at the method prototype, you can guess that it accumulates the markup in the text writer object. The hunch is confirmed by .NET Reflector, the superb tool that snoops inside the decompiled source code of .NET assemblies. (If you still don t know about the tool, get it now at http://www.aisto.com/roeder/dotnet.) According to .NET Reflector, most list controls maintain a private member as the control-to-repeat. This control is a CheckBox for the CheckBoxList; it is a RadioButton control for the RadioButtonList. There s just one instance of this control that serves all bound items. RenderItem configures the control instance and then asks it to render out using RenderControl. The following line is an excerpt from the decompiled source code of RenderItem:

 

this._controlToRepeat.RenderControl(writer);

Me._controlToRepeat.RenderControl(writer)

 

Unfortunately, _controlToRepeat is a private member and, therefore, is not accessible from within a derived class. However, .NET Reflector reveals another characteristic of the RenderItem method that can be exploited to come to a solution. Look at this excerpt:

 

if (item1.HasAttributes)

{

  foreach (string text1 in item1.Attributes.Keys)

     this._controlToRepeat.Attributes[text1] =

     item1.Attributes[text1];

}

If item1.HasAttributes Then

  Dim text1 As String

  For Each text1 In item1.Attributes.Keys

     Me._controlToRepeat.Attributes(text1) =

     item1.Attributes(text1)

  Next

End If

 

The item1 member represents the nth data item. As it shows, all attributes associated with a data item (the ListItem class) are replicated on the repeated control. Let s make a quick test:

 

<x:CheckBoxList ID="CheckBoxList1" runat="server" ...>

   <asp:ListItem Text="Red" Value="1"

    onclick="alert('hello');" />

</x:CheckBoxList>

 

If you click the resulting checkbox, a message box pops up. What remains is to engineer a more general, and easy to use, programming interface.

 

The Final Step

The idea is to ensure that all bound items have an onclick attribute properly set before the control renders out. You override the OnPreRender method of the CheckBoxList control, go through all elements in the Items collection, and add an onclick attribute as appropriate. Listing One shows the full source code.

 

In the end, each checkbox is bound to a __setStyle JavaScript function that CheckBoxList itself injects in the host page. The source code for the __setStyle function is created on the server and then emitted using the methods of the ClientScript page object (see Figure 4).

 

Type t = this.GetType();

string jsFunc = BuildScript();

if (!Page.ClientScript.IsClientScriptBlockRegistered(

   t, "__setStyle"))

  Page.ClientScript.RegisterClientScriptBlock(

   t, "__setStyle", jsFunc, true);

Dim t As Type = Me.GetType()

Dim jsFunc As String = BuildScript()

If!Page.ClientScript.IsClientScriptBlockRegistered(

 t, "__setStyle") Then

  Page.ClientScript.RegisterClientScriptBlock( _

      t, "__setStyle", jsFunc, True)

End If

Figure 4: Each checkbox is bound to a __setStyle JavaScript function that CheckBoxList itself injects in the host page.

 

The __setStyle function accepts a bunch of parameters (as shown in Listing Two). The first parameter indicates the current checkbox (the this value). Next, it takes two blocks of six parameters denoting the selected and normal style for the checkbox. The six parameters refer to boldface, foreground, and background color, and color, style, and width of the border. On the server, the helper method BuildScriptForItem prepares the call to __setStyle for each item.

 

With the JavaScript in place, the control changes the style as soon as the user clicks a checkbox. The style is then maintained when the host page posts back. Only the preceding styles are set on the client. If your SelectedItemStyle object also sets, say, the horizontal alignment, that setting will be applied only after a postback.

 

Conclusion

Figure 5 shows the new CheckBoxList control live in the Visual Studio 2005 environment. The control works fine with data-bound and list-bound items; that is, whether you bind it to a data source or just to a list of static items. To provide a great design-time experience, you should create a custom designer for the control that adds a fake selected item just to show how it works. Finally, note that this rich set of features works only if the list control is rendered with a table layout. If the list control is created with a flow layout then, by design, GetItemStyle is never called out and there s no way to fix things. Thankfully, all my client s checkbox lists use the table layout.

 


Figure 5: The new CheckBoxList control in Visual Studio 2005.

 

The sample code accompanying this article is available for download.

 

Dino Esposito is a Solid Quality Learning mentor and the author of Programming Microsoft ASP.NET 2.0 Core Reference and Programming Microsoft ASP.NET 2.0 Applications-Advanced Topics, both from Microsoft Press. Based in Italy, Dino is a frequent speaker at industry events worldwide. Join the blog at http://weblogs.asp.net/despos.

 

Begin Listing One Overriding OnPreRender

protected override void OnPreRender(EventArgs e)

{

   base.OnPreRender(e);

   // Rich capabilities only supported with a table layout

   if (this.RepeatLayout == RepeatLayout.Flow)

       return;

   foreach (ListItem item in Items)

   {

       string jsItem = BuildScriptForItem(item);

       item.Attributes["onclick"] = jsItem;

   }

   // Inject the script code

   Type t = this.GetType();

   string jsFunc = BuildScript();

   if (!Page.ClientScript.IsClientScriptBlockRegistered(

       t, "__setStyle"))

   {

       Page.ClientScript.RegisterClientScriptBlock(

        t, "__setStyle", jsFunc, true);

   }

}

Protected override void OnPreRender(EventArgs e)

{

   base.OnPreRender(e);

   // Rich capabilities only supported with a table layout

   if (this.RepeatLayout == RepeatLayout.Flow)

       return;

   foreach (ListItem item in Items)

   {

       string jsItem = BuildScriptForItem();

       item.Attributes["onclick"] = jsItem;

   }

   // Inject the script code

   Type t = this.GetType();

   string jsFunc = BuildScript();

   if (!Page.ClientScript.IsClientScriptBlockRegistered(

       t, "__setStyle"))

   {

       Page.ClientScript.RegisterClientScriptBlock(

        t, "__setStyle", jsFunc, true);

   }

}

End Listing One

 

Begin Listing Two The __setStyle JavaScript function

function __setStyle(me,

   bold, fore, back, bordcolor, bordstyle, bordwidth,

   oldbold, oldfore, oldback, oldbordcolor, oldbordstyle,

   oldbordwidth)

{

  var ctl = me.parentNode;

  if (me.checked)

  {

     ctl.style.fontWeight = bold;

     ctl.style.color = fore;

     ctl.style.backgroundColor = back;

     ctl.style.borderColor = bordcolor;

     ctl.style.borderStyle = bordstyle;

     ctl.style.borderWidth = bordwidth;

  }

  else

  {

     ctl.style.fontWeight = oldbold;

     ctl.style.color = oldfore;

     ctl.style.backgroundColor = oldback;

     ctl.style.borderColor = oldbordcolor;

     ctl.style.borderStyle = oldbordstyle;

     ctl.style.borderWidth = oldbordwidth;

  }

}

End Listing Two