14.2 Custom Controls

In addition to creating user controls, which are essentially reusable
small web pages, you can also create your own compiled custom
controls. There are three ways to create custom controls:

Create a derived custom control by deriving from an existing control.

Create a composite control by grouping existing controls together
into a new compiled control.

Create a full custom control by deriving from
System.Web.UI.WebControls.WebControl.

Composite controls are
most
similar to user controls. The key difference is that composite
controls are compiled into a DLL and used as you would any server
control.

To get started, you'll create
a Web
Control Library in which you'll create the various
custom controls for this chapter. Open Visual Studio .NET and choose
New Project. In the New Project Window, select either Visual C#
Projects or Visual Basic Projects and create a Web Control Library
called CustomControls, as shown in Figure 14-7.

Figure 14-7. Custom control New Project window

You'll notice that Visual Studio has created a
complete custom control named WebCustomControl1. Before examining
this control, create a Web Application to test it. From the File menu
choose New Project (Ctrl-Shift-N) and create a project named
CustomControlWebPage in the same directory. Be sure to choose the
"Add to Solution" radio button, as
shown in Figure 14-8.

Figure 14-8. Add custom control web page

You'll create a series of custom
controls and test them from this application. Right-click on the
CustomControls project to bring up the context menu. First click on
Set as Startup Project, and then choose Properties, as shown in Figure 14-9.

Figure 14-9. Choosing project properties

Choose the configuration properties and set the output path to the
same directory as the test page, as shown in Figure 14-10.

Figure 14-10. Setting the output path

Normally, when you build a custom control you will copy the
.dll file to the \bin
directory of the page that will test it. By setting the output to the
\bin directory of your test page you will save
that step and thus be able to test the control quickly.

14.2.1 The Default (Full) Custom Control

Visual Studio .NET has provided a custom
control
named WebCustomControl1, as we saw. This is a full custom control,
derived from System.Web.UI.WebControls.WebControl. Even before you
fully understand how this code works, you can test it in the test
page you created. Open WebForm1.aspx and add a
statement to register the new control:

This registers the custom control with the web page, similar to how
you registered the user control in Example 14-3. Once
again you use the @Register tag and provide a tag
prefix (OReilly). Rather than providing a
Tagname and src, however, you
provide a Namespace and
Assembly, which uniquely identify the control and
the DLL that the page must use.

You now add the control to the page. The two attributes you must set
are the Runat attribute, which is needed for all
server-side controls, and the Text attribute,
which dictates how the control is displayed at runtime. The tag
should appear as follows:

This control contains a single property, Text, backed by a private
string variable, text.

Note that there are attributes provided both for the property and for
the class. These attributes are used by Visual Studio .NET and are
not required when creating custom controls. The most common
attributes for
custom controls are shown in Table 14-2.

Table 14-2. Common attributes for custom controls

Attribute

Description

Bindable

Boolean. true indicates that VS .NET will display
this control in the data bindings dialog box.

Browsable

Boolean. Is the property displayed in the designer?

Category

Determines in which category this control will be displayed when the
Properties dialog is sorted by category.

DefaultValue

The default value.

Description

The text you provide is displayed in the description box in the
Properties panel.

14.2.1.1 Properties

Custom controls can
expose
properties just as any other class can. You access these properties
either programmatically (e.g., in code-behind) or declaratively, by
setting attributes of the custom control, as you did in the text
page, and as shown here:

<OReilly:WebCustomControl1 Runat="Server" Text="Hello World!" />

The Text property of the control is accessed through the
Text attribute in the web page.

In the case of the Text property and the Text
attribute, the mapping between the attribute and the underlying
property is straightforward because both are strings. ASP.NET will
provide intelligent conversion of other types, however. For example,
if the underlying type is an integer or a long, the attribute will be
converted to the appropriate value type. If the value is an
enumeration, ASP.NET matches the string value against the evaluation
name and sets the correct enumeration value. If the value is a
Boolean, ASP.NET matches the string value against the Boolean value;
that is, it will match the string
"True" to the Boolean value
true.

14.2.1.2 The Render method

The key method of the
custom
control is Render. This method is declared in the base class, and
must be overridden in your derived class if you wish to take control
of rendering to the page. In Example 14-9 and Example 14-10, the Render method uses the HtmlTextWriter
object passed in as a parameter to write the string held in the Text
property.

The HtmlTextWriter class derives from TextWriter
and provides rich formatting capabilities.
HtmlTextWriter will ensure that the elements
produced are well-formed, and it will manage the attributes,
including style attributes. Thus, if you want to set the text to red,
you can add a color attribute, passing in an enumerated color object
that you've translated to HTML, as shown here:

output.AddStyleAttribute("color", ColorTranslator.ToHtml(Color.Red));

In order for the line of code to work, you will need to add an alias
to the System.Drawing namespace with either a using statement (in C#)
or an imports statement (in VB .NET) at the top of the source code.

You can set the text to be within header
(<h2>) tags with the
HtmlTextWriter's RenderBeginTag and RenderEndTag
methods:

The result is that when the text is output, the correct tags are
created, as shown in Figure 14-12. (The source output
that illustrates the HTML rendered by the HtmlTextWriter is circled
and highlighted.)

Figure 14-12. The output and its source

14.2.1.3 Maintaining state

In the next example, you'll add a button to increase the size of
the text. To accomplish this, you'll eschew the
rendering support of the HtmlTextWriter, instead writing the text
yourself, using a new Size property (to set the size of the output
text). The C# code for the Render method should appear as follows:

The Size property must maintain its state through the postback fired
by pressing the button. This is as simple as writing to and reading
from the ViewState collection maintained by the page (see Chapter 6), as shown in the C# property definition of
the Size property:

Public Property Size( ) As Integer
Get
Return Convert.ToInt32(ViewState("Size"))
End Get
Set(ByVal Value As Integer)
ViewState("Size") = Value.ToString( )
End Set
End Property

The property Get method retrieves the value from ViewState, casts it
to a string in the case of C#, and then converts that string to its
integer equivalent. The property Set method stashes a string
representing the size into ViewState.

To ensure that a valid value is in ViewState to start with,
you'll also add a constructor to this control. In
C#, the constructor is:

public WebCustomControl1( )
{
ViewState["Size"] = "1";
}

In VB.NET, it is:

Public Sub New( )
ViewState("Size") = "1"
End Sub

The constructor initializes the value held in ViewState to 1. Each
press of the button will update the Size property. To make this work,
you'll add a button declaration in the test page:

The important changes here are that you've added an
ID attribute (Button1) and defined an event
handler for the button. You will also need to create an event handler
in the code-behind page.

Be sure to add a reference to the CustomControls DLL file to the web
page. That will allow Intellisense to see your object, and
you'll be able to declare the control in the
code-behind page. In C#, this takes the form:

To illustrate the effect of clicking the button, in Figure 14-13 we created two instances of the program, and
in the second instance we pressed the button three times.

Figure 14-13. Maintaining state

Each time the button is clicked, the state variable Size is
incremented; when the page is drawn, the state variable is retrieved
and
used to set the size of the text.

14.2.2 Creating Derived Controls

There are times when it is not
necessary to create your own control from scratch. You may simply
want to extend the behavior of an existing control type. You can
derive from an existing control just as you might derive from any
class.

Imagine, for example, that you would like a button to maintain a
count of the number of times it has been clicked. Such a button might
be useful in any number of applications, but unfortunately the web
Button control does not provide this functionality.

To overcome this limitation of the button class,
you'll derive a new custom control from
System.Web.UI.WebControls.Button, as shown in Example 14-15 (for C#) and Example 14-16 (for
VB.NET).

The work of this class is to maintain its state: how many times the
button has been clicked. You provide a public property,
Count, which is backed not by a private member
variable but rather by a value stored in view state. This is
necessary because the button will post the page, and the state would
otherwise be lost. The Count property is defined as follows in C#:

Because CountedButton derives from Button, it is easy to override the
behavior of a Click event. In this case, when the user clicks the
button, you will increment the Count value held in
view state and update the text on the button to reflect the new
count. You will then call the base class' OnClick
method to carry on with the normal processing of the Click event. The
C# event handler is as follows:

You add this control to the .aspx form just as
you would your composite control:

<OReilly:CountedButton Runat="Server" id="CB1" />

You do not need to add an additional Register
statement because this control, like the custom control, is in the
CustomControls namespace and the CustomControls assembly.

When you click the button four times, the button reflects
the current count of clicks, as shown in Figure 14-14.

Figure 14-14. Counted button

14.2.3 Creating Composite Controls

The third way to create a custom
control is to combine two or more existing controls. In the next
example, you will act as a contract programmer, and we will act as
the client. We'd like you to build a slightly more
complex control that we might use to keep track of the number of
inquiries we receive about our books.

As your potential client, we might ask you to write a control that
lets us put in one or more books, and each time we click on a book
the control will keep track of the number of clicks for that book, as
shown in Figure 14-15.

Figure 14-15. Composite control

The .aspx file for this program is shown in
Chapter 14. Its C# and VB versions are identical,
except for the @ Page directive.

The key thing to note in this code is that the BookInquiryList
component contains a number of BookCounter elements. There is one
BookCounter element for each book we want to track in the control.
The control is quite flexible. We can track one, eight (as shown
here), or any arbitrary number of books. Each BookCounter element has
a BookName attribute that is used to display the
name of the book being tracked.

You can see from Figure 14-15 that each
book
is tracked using a CountedButton custom control, but you do not see a
declaration of the CountedButton in the .aspx
file. The CountedButton control is entirely encapsulated within the
BookCounter custom control.

The entire architecture, therefore, is as follows:

The BookInquiryList composite control derives from WebControl and
implements INamingContainer , as described
shortly.

The BookInquiryList control has a Controls property that it inherits
from the Control class (through WebControl) and that returns a
collection of child controls.

Within this Controls collection is an arbitrary number of BookCounter
controls.

BookCounter is itself a composite control that derives from
WebControl and that also implements
INamingContainer.

Each instance of BookContainer has two properties, BookName and
Count.

The Name property is backed by view state and is initialized through
the BookName BookName in the
.aspx file

The Count property delegates to a private CountedButton object, which
is instantiated in BookContainer.CreateChildControls( ).

The BookInquiryList object has only two purposes: it acts as a
container for the BookCounter objects, and it is responsible for
rendering itself and ensuring that it's contained
BookCounter objects render themselves on demand.

The best way to see how all this works is to work your way through
the code from the inside out. The most contained object is the
CountedButton.

Because you want the button to be able to display the string
5 Inquiries rather than 5 clicks, you must change
the line within the OnClick method that sets the
button's text:

this.Text = ViewState["Count"] + " " + displayString;

The VB.NET equivalent is:

Me.Text = ViewState("Count") & " " & displayString

Rather than hard-wiring the string, you'll use a
private member variable, displayString, to store a value passed in to
the constructor:

private string displayString;

In VB.NET, you'd use:

Private displayString As String

You must set this string in the constructor. To protect client code
that already uses the default constructor (with no parameters),
you'll overload the constructor, adding a version
that takes a string:

With these changes, the CountedButton is ready to be used in the
first composite control, BookCounter.

14.2.3.2 Creating the BookCounter composite control

The BookCounter composite control is responsible for keeping track
of and displaying the number of inquiries about an individual book.
Its complete source code is shown in C# in Example 14-20 and in VB.NET in Example 14-21.

Example 14-21. BookCounter.vb

Imports System
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.ComponentModel
Public Class BookCounter
Inherits System.Web.UI.WebControls.WebControl
Implements INamingContainer
' intialize the counted button member
P btn As CountedButton = New CountedButton("inquiries")
Public Property BookName( ) As String
Get
Return CStr(ViewState("BookName"))
End Get
Set(ByVal Value As String)
ViewState("BookName") = Value
End Set
End Property
Public Property Count( ) As Integer
Get
Return btn.Count
End Get
Set(ByVal Value As Integer)
btn.Count = Value
End Set
End Property
Public Sub Reset( )
btn.Count = 0
End Sub
Protected Overrides Sub CreateChildControls( )
Controls.Add(btn)
End Sub
End Class

INamingContainer

The first thing to note about the BookCounter class is
that it implements the INamingContainer interface.
This is a "marker" interface that
has no methods. The purpose of this interface is to identify a
container control that creates a new ID namespace, guaranteeing that
all child controls have IDs that are unique to the page.

14.2.3.2.2 Containing CountedButton

The BookCounter class contains an instance of CountedButton:

CountedButton btn = new CountedButton("inquiries");

or:

Private btn As CountedButton = New CountedButton("inquiries")

The btn member is instantiated in the CreateChildControls method
inherited from System.Control:

CreateChildControls is called in preparation for rendering and offers
the BookCounter class the opportunity to add the btn object as a
contained control.

There is no need for BookCounter to override the Render method; the
only thing it must render is the CountedButton, which can render
itself. The default behavior of Render is to render all the child
controls, so you need not do anything special to make this work.

BookCounter also has two properties: BookName and Count. BookName is
a string to be displayed in the control and is managed through
ViewState. Its C# source code is:

Public Property Count( ) As Integer
Get
Return btn.Count
End Get
Set(ByVal Value As Integer)
btn.Count = Value
End Set
End Property

There is no need to place the value in ViewState, since the button
itself is responsible for its own data.

14.2.3.3 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.

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:

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.

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.

Figure 14-16. Total inquiries displayed

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.

Figure 14-17. Stepping into BookCounter.Count

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

With that object, you are able to get the Count, as described
previously:

totalInquiries += current.Count;

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:

When you do this, 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.

14.2.3.3.4 Rendering the summary

Once all of the child controls have been rendered, the
BookInquiryList creates a new row to display the total inquiries: