ASP.NET 2.0 Unleashed

When you build more complex controls, you often need to represent a collection of items. For example, the standard ASP.NET DropDownList control contains one or more ListItem controls that represent individual options in the DropDownList. The GridView control can contain one or more DataBoundField controls that represent particular columns to display.

In this section, we build several controls that represent a collection of items. We build multiple content rotator controls that randomly display HTML content, as well as a server-side tab control that renders a tabbed view of content.

Using the ParseChildren Attribute

When building a control that contains a collection of child controls, you need to be aware of an attribute named the ParseChildren attribute. This attribute determines how the content contained in a control is parsed.

When the ParseChildren attribute has the value TRue, then content contained in the control is parsed as properties of the containing control. If the control contains child controls, then the child controls are parsed as properties of the containing control. (The attribute really should have been named the ParseChildrenAsProperties attribute.)

When the ParseChildren attribute has the value False, then no attempt is made to parse a control's child controls as properties. The content contained in the control is left alone.

The default value of the ParseChildren attribute is False. However, the WebControl class overrides this default value and sets the ParseChildren attribute to the value to true. Therefore, you should assume that ParseChildren is False when used with a control that inherits directly from the System.Web.UI.Control class, but assume that ParseChildren is true when used with a control that inherits from the System.Web.UI.WebControls.WebControl class.

Imagine, for example, that you need to create a content rotator control that randomly displays content in a page. There are two ways of creating this control, depending on whether ParseChildren has the value true or False.

The control in Listing 31.24 illustrates how you can create a content rotator control when ParseChildren has the value False.

Listing 31.24. ContentRotator.vb

Imports System Imports System.Web.UI Imports System.Web.UI.WebControls Namespace myControls <ParseChildren(False)> _ Public Class ContentRotator Inherits WebControl Protected Overrides Sub AddParsedSubObject(ByVal obj As Object) If TypeOf obj Is Content Then MyBase.AddParsedSubObject(obj) End If End Sub Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) Dim rnd As Random = New Random() Dim index As Integer = rnd.Next(Me.Controls.Count) Me.Controls(index).RenderControl(writer) End Sub End Class Public Class Content Inherits Control End Class End Namespace

The file in Listing 31.24 actually contains two controls: a ContentRotator control and a Content control. The ContentRotator control randomly selects a single Content control from its child controls and renders the Content control to the browser. This all happens in the control's RenderContents() method.

Notice that the ParseChildren attribute has the value False in Listing 31.24. If you neglected to add this attribute, then the Content controls would be parsed as properties of the ContentRotator control and you would get an exception.

Note

The AddParsedSubObject() method is discussed in the next section.

The page in Listing 31.25 illustrates how you can use the ContentRotator and Content controls (see Figure 31.11).

Listing 31.25. ShowContentRotator.aspx

[View full width]

<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR /xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Show ContentRotator</title> </head> <body> <form runat="server"> <div> <custom:ContentRotator Runat="server"> <custom:Content Runat="server"> First Content Item </custom:Content> <custom:Content Runat="server"> Second Content Item <asp:Calendar Runat="server" /> </custom:Content> <custom:Content Runat="server"> Third Content Item </custom:Content> </custom:ContentRotator> </div> </form> </body> </html>

Figure 31.11. Randomly displaying content with the ContentRotator control.

If ParseChildren is not set to the value False, then you need to add a property to your control that corresponds to the child controls contained in the control. For example, the control in Listing 31.26 includes an Items property that represents the Item controls contained in the control.

Listing 31.26. ItemRotator.vb

Imports System Imports System.Collections Imports System.Web.UI Imports System.Web.UI.WebControls Imports System.ComponentModel Namespace myControls <ParseChildren(True, "Items")> _ Public Class ItemRotator Inherits CompositeControl Private _items As ArrayList = New ArrayList() <Browsable(False)> _ Public ReadOnly Property Items() As ArrayList Get Return _items End Get End Property Protected Overrides Sub CreateChildControls() Dim rnd As Random = New Random() Dim index As Integer = rnd.Next(_items.Count) Dim item As Control = CType(_items(index), Control) Me.Controls.Add(item) End Sub End Class Public Class Item Inherits Control End Class End Namespace

In Listing 31.26, the second value passed to the ParseChildren attribute is the name of a control property. The contents of the ItemRotator are parsed as items of the collection represented by the specified property.

Unlike the ContentRotator control, the controls contained in the ItemRotator control are not automatically parsed into child controls. After the CreateChildControls() method executes, the ItemRotator control contains only one child control (the randomly selected Item control).

The page in Listing 31.27 illustrates how you can use the ItemRotator control to randomly display page content.

Listing 31.27. ShowItemRotator.aspx

[View full width]

<%@ Page Language="VB" Trace="true" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR /xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Show ItemRotator</title> </head> <body> <form runat="server"> <div> <custom:ItemRotator Runat="server"> <custom:item runat="server"> First Item </custom:item> <custom:item runat="server"> Second Item <asp:Calendar Runat="server" /> </custom:item> <custom:item runat="server"> Third Item </custom:item> </custom:ItemRotator> </div> </form> </body> </html>

There is no requirement that the contents of a control must be parsed as controls. When building a control that represents a collection of items, you can also represent the items as objects. For example, the ImageRotator control in Listing 31.28 contains ImageItem objects. The ImageItem class does not represent a control.

Listing 31.28. ImageRotator.vb

Imports System Imports System.Collections Imports System.Web.UI Imports System.Web.UI.WebControls Imports System.ComponentModel Namespace myControls <ParseChildren(True, "ImageItems")> _ Public Class ImageRotator Inherits WebControl Private _imageItems As ArrayList = New ArrayList() Public ReadOnly Property ImageItems() As ArrayList Get Return _imageItems End Get End Property Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) Dim rnd As Random = New Random() Dim img As ImageItem = CType(_imageItems(rnd.Next(_imageItems.Count)), ImageItem) writer.AddAttribute(HtmlTextWriterAttribute.Src, img.ImageUrl) writer.AddAttribute(HtmlTextWriterAttribute.Alt, img.AlternateText) writer.RenderBeginTag(HtmlTextWriterTag.Img) writer.RenderEndTag() End Sub End Class Public Class ImageItem Private _imageUrl As String Private _alternateText As String Public Property ImageUrl() As String Get Return _imageUrl End Get Set(ByVal Value As String) _imageUrl = value End Set End Property Public Property AlternateText() As String Get Return _alternateText End Get Set(ByVal Value As String) _alternateText = value End Set End Property End Class End Namespace

Notice that the ImageItem class is just a class. It does not derive from the base Control class. Because the ImageItem class does nothing more than represent a couple of properties, there is no reason to make it a full-blown control.

The page in Listing 31.29 illustrates how you can use the ImageRotator control to display different images randomly.

Listing 31.29. ShowImageRotator.aspx

[View full width]

<%@ Page Language="VB" Trace="true" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR /xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Show ImageRotator</title> </head> <body> <form runat="server"> <div> <custom:ImageRotator Runat="server"> <custom:ImageItem ImageUrl="Image1.gif" AlternateText="Image 1" /> <custom:ImageItem ImageUrl="Image2.gif" AlternateText="Image 2" /> <custom:ImageItem ImageUrl="Image3.gif" AlternateText="Image 3" /> </custom:ImageRotator> </div> </form> </body> </html>

The page in Listing 31.29 has tracing enabled. If you look in the Control Tree section, you'll see that the ImageRotator control does not contain any child controls (see Figure 31.12).

Figure 31.12. The ShowImageRotator.aspx page control tree.

Using the AddParsedSubObject() Method

When the ParseChildren attribute has the value false, the contents of a control are automatically added to the control's collection of child controls (represented by the Controls property). It is important to understand that all content contained in the control, even carriage returns and spaces, are added to the controls collection.

Any content contained in a control that does not represent a server-side control is parsed into a Literal control. In some cases, you might want to allow only a certain type of control to be added to the Controls collection.

The AddParsedSubObject() method is called as each control is added to the Controls collection. By overriding the AddParsedSubObject() method, you can block certain types of controlssuch as Literal controlsfrom being added to the Controls collection.

For example, the ContentRotator control in Listing 31.20 overrides the base AddParsedSubObject() method and prevents anything that is not a Content control from being added to the ContentRotator Controls collection. If you removed the AddParsedSubObject() method from this control, then all of the carriage returns and spaces between the Content controls would be added to the Controls collection as Literal controls.

Using a ControlBuilder

The AddParsedSubObject() method enables you to specify which parsed controls get added to a Controls collection. Sometimes, you must take even more control over the parsing of a control.

When the ASP.NET Framework parses a page, the Framework uses a special type of class called a ControlBuilder class. You can modify the way in which the content of a control is parsed by associating a custom ControlBuilder with a control.

Here's a list of the most useful methods supported by the ControlBuilder class:

  • AllowWhiteSpaceLiterals() Enables you to trim white space from the contents of a control.

  • AppendLiteralString() Enables you trim all literal content from the contents of a control.

  • GetChildControlType() Enables you to specify how a particular tag gets parsed into a control.

The GetChildControlType() method is the most useful method. It enables you to map tags to controls. You can use the GetChildControlType() method to map any tag to any control.

For example, the file in Listing 31.30 contains a ServerTabs control that renders multiple tabs (see Figure 31.13). Each tab is represented by a Tab control.

Figure 31.13. Using the ServerTabs control.

Listing 31.30. ServerTabs.vb

[View full width]

Imports System Imports System.Collections Imports System.Web.UI Imports System.Web.UI.WebControls Namespace myControls <ControlBuilder(GetType(ServerTabsBuilder))> _ <ParseChildren(False)> _ Public Class ServerTabs Inherits WebControl Implements IPostBackEventHandler Public Property SelectedTabIndex() As Integer Get If ViewState("SelectedTabIndex") Is Nothing Then Return 0 Else Return CType(ViewState("SelectedTabIndex"), Integer) End If End Get Set(ByVal Value As Integer) ViewState("SelectedTabIndex") = Value End Set End Property Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) Dim i As Integer For i = 0 To Me.Controls.Count - 1 Step i + 1 Dim tab As ServerTab = CType(Me.Controls(i), ServerTab) Dim eRef As String = Page.ClientScript.GetPostBackClientHyperlink(Me, i .ToString()) If SelectedTabIndex = i Then writer.AddAttribute(HtmlTextWriterAttribute.Class, "tab selectedTab") Else writer.AddAttribute(HtmlTextWriterAttribute.Class, "tab") End If writer.RenderBeginTag(HtmlTextWriterTag.Div) writer.AddAttribute(HtmlTextWriterAttribute.Href, eRef) writer.RenderBeginTag(HtmlTextWriterTag.A) writer.Write(tab.Text) writer.RenderEndTag() ' A writer.RenderEndTag() ' Tab Div Next writer.Write("<br style='clear:both' />") writer.AddAttribute(HtmlTextWriterAttribute.Class, "tabContents") writer.RenderBeginTag(HtmlTextWriterTag.Div) Me.Controls(SelectedTabIndex).RenderControl(writer) writer.RenderEndTag() ' Tab Contents DIV End Sub Protected Overrides Sub AddParsedSubObject(ByVal obj As Object) If TypeOf obj Is ServerTab Then MyBase.AddParsedSubObject(obj) End If End Sub Protected Overrides ReadOnly Property TagKey() As HtmlTextWriterTag Get Return HtmlTextWriterTag.Div End Get End Property Public Sub RaisePostBackEvent(ByVal eventArgument As String) Implements IPostBackEventHandler.RaisePostBackEvent SelectedTabIndex = Int32.Parse(eventArgument) End Sub End Class Public Class ServerTabsBuilder Inherits ControlBuilder Public Overrides Function GetChildControlType(ByVal tagName As String, ByVal attribs As IDictionary) As Type If String.Compare(tagName, "tab", True) = 0 Then Return GetType(ServerTab) Else Return Nothing End If End Function End Class Public Class ServerTab Inherits Control Private _Text As String Public Property Text() As String Get Return _Text End Get Set(ByVal Value As String) _Text = value End Set End Property End Class End Namespace

Notice that the ServerTabs class is decorated with a ControlBuilder attribute. This attribute associates the ServerTabs control with a ControlBuilder class named ServerTabsBuilder.

The ServerTabsBuilder class overrides the base ControlBuilder GetChildControlType() method. The overridden method maps the <tab> tag to the Tab control. Because of this mapping, you do not need to use a prefix or use the runat="server" attribute when declaring a tab within the ServerTabs control.

The page in Listing 31.31 illustrates how you can use the ServerTabs control.

Listing 31.31. ShowServerTabs.aspx

[View full width]

<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR /xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <style type="text/css"> html { background-color:silver; } .tab { float:left; position:relative; top:1px; background-color:#eeeeee; border:solid 1px black; padding:0px 15px; margin-left:5px; } .tab a { text-decoration:none; } .selectedTab { background-color:white; border-bottom:solid 1px white; } .tabContents { border:solid 1px black; background-color:white; padding:10px; height:200px; } </style> <title>Show ServerTabs</title> </head> <body> <form runat="server"> <div> <custom:ServerTabs Runat="Server"> <tab Text="First Tab"> Contents of the first tab </tab> <tab Text="Second Tab"> Contents of the second tab </tab> <tab Text="Third Tab"> Contents of the third tab </tab> </custom:ServerTabs> </div> </form> </body> </html>

The ControlBuilder enables you to declare instances of the Tab control by using the <tab> tag instead of using a <custom:Tab runat="server"> tab.

Категории