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:
:
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:
SelectedItemStyle-ForeColor="Blue" SelectedItemStyle-BackColor="#FFFF80"> 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. 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: onclick="alert('hello');" />
Figure 3: The new CheckBoxList
control in action. One Step Further
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