Programming ASP.NET: Custom and User Controls, Part 2
Pages: 1, 2, 3, 4, 5
Creating the BookInquiryList composite control
Each of the BookCounter objects is contained within the Controls collection of the BookInquiryList. This control has no properties or state. Its only method is Render, as shown in C# in Example 14-22 and in VB.NET in Example 14-23.
Example 14-22: BookInquiryList source in C#
[ControlBuilderAttribute(typeof(BookCounterBuilder)),ParseChildren(false)]
public class BookInquiryList : System.Web.UI.WebControls.WebControl, INamingContainer
{
protected override void Render(HtmlTextWriter output)
{
int totalInquiries = 0;
BookCounter current;
// Write the header
output.Write("<Table border='1' width='90%' cellpadding='1'" +
"cellspacing='1' align = 'center' >");
output.Write("<TR><TD colspan = '2' align='center'>");
output.Write("<B> Inquiries </B></TD></TR>");
// if you have no contained controls, write the default msg.
if (Controls.Count == 0)
{
output.Write("<TR><TD colspan = '2'> align='center'");
output.Write("<B> No books listed </B></TD></TR>");
}
// otherwise render each of the contained controls
else
{
// iterate over the controls colelction and
// display the book name for each
// then tell each contained control to render itself
for (int i = 0; i < Controls.Count; i++)
{
current = (BookCounter) Controls[i];
totalInquiries += current.Count;
output.Write("<TR><TD align='left'>" +
current.BookName + "</TD>");
output.RenderBeginTag("TD");
current.RenderControl(output);
output.RenderEndTag( ); // end td
output.Write("</tr>");
}
output.Write("<TR><TD colspan='2' align='center'> " +
" Total Inquiries: " +
totalInquiries + "</TD></TR>");
}
output.Write("</TABLE>");
}
}
Example 14-23: BookInquiryList source in VB.NET
Imports System.ComponentModel
Imports System.Web.UI
<ControlBuilder(GetType(BookCounterBuilder)), ParseChildren(False)> _
Public Class BookInquiryList
Inherits System.Web.UI.WebControls.WebControl
Implements INamingContainer
Protected Overrides Sub Render(ByVal output As HtmlTextWriter)
Dim totalInquiries As Integer = 0
' Write the header
output.Write("<Table border='1' width='90%' cellpadding='1'" & _
"cellspacing='1' align = 'center' >")
output.Write("<TR><TD colspan = '2' align='center'>")
output.Write("<B> Inquiries </B></TD></TR>")
' if you have no contained controls, write the default msg.
If Controls.Count = 0 Then
output.Write("<TR><TD colspan = '2'> align='center'")
output.Write("<B> No books listed </B></TD></TR>")
' otherwise render each of the contained controls
Else
' iterate over the controls colelction and
' display the book name for each
' then tell each contained control to render itself
Dim current As BookCounter
For Each current In Controls
totalInquiries += current.Count
output.Write("<TR><TD align='left'>" & _
current.BookName + "</TD>")
output.RenderBeginTag("TD")
current.RenderControl(output)
output.RenderEndTag() ' end td
output.Write("</tr>")
Next
Dim strTotalInquiries As String
strTotalInquiries = totalInquiries.ToString
output.Write("<TR><TD colspan='2' align='center'> " & _
" Total Inquiries: " & _
CStr(strTotalInquiries) & "</TD></TR>")
End If
output.Write("</TABLE>")
End Sub
End Class
Friend Class BookCounterBuilder
Inherits ControlBuilder
Public Overrides Function GetChildControlType( _
ByVal tagName As String, ByVal attributes As IDictionary) As Type
If tagName = "BookCounter" Then
Dim x As BookCounter
Return x.GetType
Else
Return Nothing
End If
End Function
Public Overrides Sub AppendLiteralString(ByVal s As String)
End Sub
End Class
ControlBuilder and ParseChildren attributes
The BookCounter class must be associated with the BookInquiryClass so ASP.NET can translate the elements in the .aspx page into the appropriate code. This is accomplished using the ControlBuilder attribute:
[ControlBuilderAttribute(typeof(BookCounterBuilder)),ParseChildren(false)]
The argument to the ControlBuilderAttribute is a Type object that you obtain by passing in BookCounterBuilder, a class you will define to return the type of the BookCounter class given a tag named BookCounter. The code for the BookCounterBuilder is shown in C# in Example 14-24 and in VB.NET in Example 14-25.
Example 14-24: C# version of BookCounterBuilder
internal class BookCounterBuilder : ControlBuilder
{
public override Type GetChildControlType(
string tagName, IDictionary attributes)
{
if (tagName == "BookCounter")
return typeof(BookCounter);
else
return null;
}
public override void AppendLiteralString(string s)
{
}
}
Example 14-25: VB.NET version of BookCounterBuilder
Friend Class BookCounterBuilder
Inherits ControlBuilder
Public Overrides Function GetChildControlType(_
ByVal tagName As String, ByVal attributes As Idictionary) As Type
If tagName = "BookCounter" Then
Dim x As BookCounter
Return x.GetType
Else
Return Nothing
End If
End Function
Public Overrides Sub AppendLiteralString(ByVal s As String)
End Sub
End Class
ASP.NET will use this BookCounterBuilder, which derives from ControlBuilder, to determine the type of the object indicated by the BookCounter tag. Through this association, each of the BookCounter objects will be instantiated and added to the Controls collection of the BookInquiryClass.
The second attribute, ParseChildren, must be set to false to tell ASP.NET that you have handled the children attributes and no further parsing is required. A value of false indicates that the nested child attributes are not properties of the outer object, but rather are child controls.
Render
The only method of the BookInquiryClass is the override of Render. The purpose of Render is to draw the table shown earlier in Figure 14-15, using the data managed by each of the BookCounter child controls.
The BookInquiryClass provides a count of the total number of inquiries, as shown in Figure 14-16.
The code tallies inquiries by initializing an integer variable, totalInquiries, to zero and then iterating over each control in turn, asking the control for its Count property. The statement is the same in C# and VB.NET, except for the closing semicolon in C#:
totalInquiries += current.Count;
The Count property of the control delegates to the CountedButton's count property, as you can see if you step through this code in a debugger, as illustrated in Figure 14-17.
Rendering the output
That same loop renders each of the child controls by iterating over each of the controls. In C#, this is done using:
for (int i = 0; i < Controls.Count; i++)
{
current = (BookCounter) Controls[i];
totalInquiries += current.Count;
output.Write("<TR><TD align='left'>" +
current.BookName + "</TD>");
output.RenderBeginTag("TD");
current.RenderControl(output);
output.RenderEndTag( ); // end td
output.Write("</tr>");
}
In VB.NET, the code is:
For Each current in Controls
totalInquiries += current.Count
output.Write("<TR><TD align='left'>" & _
current.BookName + "</TD>")
output.RenderBeginTag("TD")
current.RenderControl(output)
output.RenderEndTag( ) ' end td
output.Write("</tr>")
Next
The local BookCounter object, current, is assigned to each object in the Controls collection in succession:
for (int i = 0; i < Controls.Count; i++)
{
current = (BookCounter) Controls[i];
With that object, you are able to get the Count, as described previously:
totalInquiries += current.Count;
and then you proceed to render the object. The HtmlTextWriter is used first to create a row and to display the name of the book, using the BookName property of the current BookCounter object:
output.Write("<TR><TD align='left'>" +current.BookName + "</TD>");
You then render a TD tag, and within that tag you tell the BookCounter object to render itself. Finally, you render an ending TD tag using RenderEndTag, and an ending row tag using the Write method of the HTMLTextWriter:
output.RenderBeginTag("TD");current.RenderControl(output);output.RenderEndTag( ); // end tdoutput.Write("</tr>");
When you tell the contained control to render itself:
current.RenderControl(output);
the Render method of BookCounter is called. Since you have not overridden this method, the Render method of the base class is called, which tells each contained object to render itself. The only contained object is CountedButton. Since you have not overridden Render in CountedButton, the base Render method in Button is called, and the button is rendered.
Assignment of Responsibilities
This simple example of a composite control is interesting because the various responsibilities are spread among the participating objects. The BookInquiryList object assumes all responsibility for laying out the control, creating the table, and deciding what will be rendered where. However, it delegates responsibility for rendering the button object to the individual contained controls.
Similarly, the BookInquiryList is responsible for the total number of inquiries--because that information transcends what any individual BookCounter object might know. However, the responsibility for the count held by each BookCounter is delegated to the BookCounter itself. As far as the BookInquiryList is concerned, it gets that information directly from the BookCounter's Count property. It turns out, however, that BookCounter in turn delegates that responsibility to the CountedButton.
Rendering the summary
Once all of the child controls have been rendered, the BookInquiryList creates a new row to display the total inquiries:
output.Write("<TR><TD colspan='2' align='center'> " +" Total Inquiries: " +totalInquiries + "</TD></TR>");
|
Related Reading |
Back to the .NET DevCenter.


