IN THIS CHAPTER
In this chapter, you will learn how to extend the ASP.NET Framework with your own custom Web Form controls. You learn how to create the same type of controls that Microsoft developed for the ASP.NET Framework, like the TextBox
and DataGrid
controls.
By the end of this chapter, you’ll understand how to
Create non-composite Web Form controls that render any content that you please
Create composite controls built from combining existing Web Form controls
Add Designer support to your controls so that they will work well in a development environment, such as Visual Studio .NET
The ASP.NET Framework supports two methods of creating custom controls. You can create either Web User Controls or custom Web Form controls. In this section, we’ll examine the question of when it’s more appropriate to create one type of control rather than another.
We discussed Web User Controls in Chapter 5, “Creating Web User Controls.” The primary advantage of Web User Controls is that you create a Web User Control in exactly the same way as you create a normal Web Form Page. You create a Web User Control simply by dragging and dropping controls from the Toolbox.
On the other hand, creating a custom Web Form control takes a little more work. You can’t use the Visual Studio .NET Designer when building a custom Web Form control. Instead, you must programmatically specify the content or add each control that you want to display.
The primary disadvantage of Web User Controls is that they do not provide good Designer support. When you add a Web User Control to a Web Form Page, you get a gray blob on the Designer surface that represents the control (see Figure 22.1). You must take the additional step of building and browsing the page before you can see what the page will actually look like. Furthermore, because you cannot add a Web User Control to the Toolbox, you cannot easily reuse the same Web User Control in multiple projects.
Custom Web Form controls, on the other hand, offer excellent Designer support. You can add a custom Web Form control to the Toolbox. Furthermore, when you drag a custom Web Form control onto a page, it appears in the same way it will appear when it is displayed in a browser.
The one area in which Web User Controls and custom Web Form controls do not differ is performance. Although the two types of controls are compiled in different ways, they are both compiled. A custom Web Form control must be manually compiled before being used. A Web User Control is dynamically compiled when the page that contains it is first requested. So, at the end of the day, there are no performance differences.
When building custom Web Form controls, there are two basic questions that you must answer:
What control should I derive from?
Should I create a non-composite or composite control?
Let’s start with the first question. When you create a new Web Form control, you must derive the new control from an existing control in the ASP.NET Framework. Typically, you derive a new control from either the Control
(System.Web.UI.Control
) class or the WebControl
(System.Web.UI.WebControls.WebControl
) class.
You can create a custom Web control that derives from any existing control in the ASP.NET Framework. For example, you might want to derive a new control from the Label
or DataGrid
class when you want to automatically get all the functionality of the existing control.
All controls in the ASP.NET Framework, including both HTML and Web controls, ultimately derive from the Control
class. Almost all of the controls in the System.Web.UI.WebControls
namespace, such as the TextBox
and DataGrid
controls, derive from the WebControl
class.
The difference between
deriving from the Control
and WebControl
classes is support for formatting. You’ll need to derive your control from the WebControl
class when you want to take advantage of all the common formatting properties of Web controls. For example, if you derive from the WebControl
class, you automatically get support for specifying different fonts and background colors.
To make this discussion more concrete, consider the difference between two existing controls in the ASP.NET Framework—the Label
control and the Literal
control. Because the Label
control derives from the base WebControl
class, the Label
control includes properties such as the AccessKey
, BackColor
, and Font
properties. Because the Literal
control derives from the base Control
class and not the base WebControl
class, the Literal
control does not support these properties.
In most cases, you’ll derive a new custom control from the WebControl
class to take advantage of these additional formatting properties. In the next section, you’ll be provided with examples of controls that derive both from the base Control
and the base WebControl
classes.
Another decision that you must make before building a new control is whether it makes more sense to create a non-composite or composite control. When you create a non-composite control, you are responsible for specifying all the content that the control renders. When you create a composite control, you build the control out of existing controls.
A good example of a composite control is an Address Form
control. You can create a new Address Form
composite control by combining existing TextBox
and Validation
controls. In that case, you can take advantage of all the existing functionality of these controls.
On the other hand, you’ll want to create a non-composite control when you need more control
over what your control renders. For example, a Color Picker
control that enables you to pick a color from a table of colors would be a good example of a non-composite control. We’ll build both types of controls in the following sections.
A Web Form Page is really nothing more than a collection of controls. More accurately, because some controls contain other controls, a Web Form Page is a control tree.
You can view a Web Form Page’s control tree by enabling Tracing for a page. To learn more about tracing, see Chapter 6, “Debugging Your Web Form Pages.”
When you request a Web Form Page, the page calls the RenderControl()
method of each of its child controls. The RenderControl()
method checks the Visible
property of the control. If Visible
has the value True
, the RenderControl()
method calls the control’s Render()
method.
Consequently, if you want to control the content rendered by a control, you can override the
control’s Render()
method. For example, Listing 22.1 contains the code for
a simple control that displays Hello World!
.
Example 22.1.CS. HelloWorld.cs
using System; using System.Web.UI; namespace myControls { public class HelloWorld : System.Web.UI.Control { override protected void Render(HtmlTextWriter writer) { writer.Write( "Hello World!" ); } } }
Example 22.1.VB. HelloWorld.vb
Imports System.Web.UI Public Class HelloWorld Inherits System.Web.UI.Control Protected Overrides Sub Render(ByVal writer As HtmlTextWriter) writer.Write("Hello World!") End Sub End Class
Notice that the HelloWorld
control in Listing 22.1 derives from the System.Web.UI.Control
class. Furthermore, the HelloWorld
class overrides the Render()
method and writes the value Hello World!.
The control in Listing 22.1 derives from the base Control
class. This means that it does not automatically support formatting properties such as the Font
and BackColor
properties. If you need support for these formatting properties, you need to make two changes. You need to derive from the base WebControl
class and you need to override the RenderContents()
method instead of the Render()
method.
The WebControl
class Render()
method does three things. First, the method creates an opening HTML tag; next it calls the RenderContents()
method; and finally it creates the closing HTML tag. By overriding the RenderContents()
method, you can place content in between the opening and closing tags created automatically by the Render()
method.
The opening and closing HTML tags that are automatically created by the WebControl
class Render()
method add the necessary attributes to support formatting. If you override the Render()
method instead of the RenderContents()
method, all the automatic formatting is lost.
The control in Listing 22.2 contains the code for a simple control that
derives from the base WebControl
class.
Example 22.2.CS. HelloWorldWebControl.cs
using System; using System.Web.UI; namespace myControls { public class HelloWorldWebControl : System.Web.UI.WebControls.WebControl { override protected void RenderContents(HtmlTextWriter writer) { writer.Write( "Hello World!" ); } } }
Example 22.2.VB. HelloWorldWebControl.vb
Imports System.Web.UI Public Class HelloWorldWebControl Inherits System.Web.UI.WebControls.WebControl Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) writer.Write("Hello World!") End Sub End Class
When developing controls to work in the Visual Studio .NET environment, you should normally stick with controls that derive from WebControl
rather than Control
. Controls that derive from the base WebControl
class provide better Designer support. For example, you can absolutely position a control that derives
from WebControl
on the Designer surface.
In the previous
section, we created a simple non-composite control named the HelloWorldWebControl
that displays the text Hello World!. We created the control by overriding the RenderContents()
method. You might have noticed that we did not use the Response.Write()
method to output the text. Instead, we used something called the HtmlTextWriter()
class.
You should never use the Response.Write()
method
inside a custom Web control. If you use the Response.Write()
method to output a string of text, the text can appear either before or after all the page content. In other words, using Response.Write()
ignores the page’s control tree.
The HtmlTextWriter
class has several methods that make it easier to output HTML formatted content:
AddAttribute
. Adds an HTML attribute to the tag being rendered
AddStyleAttribute
. Adds a style attribute to the tag being rendered
RenderBeginTag
. Renders an opening HTML tag
RenderEndTag
. Renders a closing HTML tag that corresponds to the last opening HTML tag
Write
. Writes arbitrary content
WriteLine
. Writes arbitrary content followed by a line terminator
For example,
suppose that you need to output the text Hello World! using a green font. You could output this text using the following call to the Write
method of the HtmlTextWriter
class:
C#.
writer.Write( "<font color="green">Hello World!</font>" );
VB.NET.
writer.Write("<font color=""green"">Hello World!</font>")
However, this method of writing HTML content is messy and hard to read. Instead, you can take advantage of the methods of the HtmlTextWriter
class to output the text like the following:
C#.
writer.AddAttribute( "color", "green" ); writer.RenderBeginTag( "font" ); writer.Write( "Hello World!" ); writer.RenderEndTag();
VB.NET.
writer.AddAttribute("color", "green") writer.RenderBeginTag("font") writer.Write("Hello World!") writer.RenderEndTag()
This code is easier to read. Furthermore, when you use the methods of the HtmlTextWriter
class, the rendered content is automatically indented. For example, if you render an HTML table with the HtmlTextWriter
class, the table cells are correctly indented.
When calling
the RenderBeginTag
and AddAttribute
methods in the previous code, we passed a string that represents the attribute or tag to the method. Instead of passing a string, you can pass a value from either the HtmlTextWriterTag
or HtmlTextWriterAttribute
enumerations.
The HtmlTextWriterTag
enumeration contains common HTML tags and the HtmlTextWriterAttribute
enumeration contains common HTML attributes. There is also an HtmlTextWriterStyle
enumeration that contains common style attributes that can be used with the AddStyleAttribute
method.
When you use the enumerations with the HtmlTextWriter
methods, the HtmlTextWriter
will render different content for downlevel browsers than uplevel browsers. Consider the following code:
writer.AddStyleAttribute( HtmlTextWriterStyle.BackgroundColor, "Yellow" ); writer.RenderBeginTag( HtmlTextWriterTag.Div); writer.Write( "Hello World!" ); writer.RenderEndTag();
This code displays the text Hello World! within a <div>
tag with a yellow background. When the HtmlTextWriter
class renders this content to an uplevel browser, it renders the following content:
<div style="background-color:Yellow;"> Hello World! </div>
However, when this content is rendered to a downlevel browser, the following content is rendered:
<table cellpadding="0" cellspacing="0" border="0" width="100%" bgcolor="Yellow"><tr><td> Hello World! </td></tr></table>
Notice
that the <div>
tag has been automatically downgraded to a <table>
tag, and the background-color
attribute has been downgraded to a bgcolor
attribute. When possible, you should use the enumerations so that your controls will render correctly in both uplevel and downlevel browsers.
In this section,
we’ll tackle a more realistic sample of a non-composite control. We’ll walk through each step of creating a Content Rotator
control. Our Content Rotator
control will randomly display one entry from an XML file named Content.xml.
Perform the following steps to create a new project for the Content Rotator
control:
Open the New Project dialog box by selecting New from the File menu, and pointing to Project.
Select Web Control Library under Templates.
Select Close Solution under Location.
Name the new project myControls
and click OK.
Next, we need to create the Content Rotator
control:
Procedure 22.1. C# Steps
Add references to the System.Data
and System.XML
assemblies to the myControls project by right-clicking the References folder and selecting Add Reference.
Add a new class named ContentRotator.cs
to the myControls project by selecting Add Class from the Project menu.
Enter the
following code for the ContentRotator.cs
class:
using System; using System.Data; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace myControls { /// <summary> /// Summary description for ContentRotator. /// </summary> public class ContentRotator : WebControl { protected override void RenderContents(HtmlTextWriter writer) { // Don't retrieve content in Designer if (HttpContext.Current != null) { // Get path to content file string contentFile = HttpContext.Current.Server.MapPath( "Content.xml" ); // Load content into dataset DataSet dstContent = new DataSet(); dstContent.ReadXml( contentFile ); // Get random entry Random objRan = new Random(); DataTable dtblContent = dstContent.Tables[0]; int intRan = objRan.Next( dtblContent.Rows.Count); // Render the results writer.Write( (string)dtblContent.Rows[intRan]["item_Text"]); } else { writer.Write( "Random Content" ); } } } }
Build the myControls project by select Build myControls from the Build menu.
Procedure 22.2. VB.NET Steps
Add a new class named ContentRotator.vb
to the myControls project by selecting Add Class from the Project menu.
Enter the following code for the ContentRotator.vb
class:
Imports System Imports System.Data Imports System.Web Imports System.Web.UI Imports System.Web.UI.WebControls Public Class ContentRotator Inherits WebControl Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) Dim contentFile As String Dim dstContent As DataSet Dim dtblContent As DataTable Dim intRan As Integer ' Don't retrieve content in Designer If Not HttpContext.Current Is Nothing Then ' Get path to content file contentFile = HttpContext.Current.Server.MapPath("Content.xml") ' Load content into dataset dstContent = New DataSet() dstContent.ReadXml(contentFile) ' Get random entry Dim objRan As New Random() dtblContent = dstContent.Tables(0) intRan = objRan.Next(dtblContent.Rows.Count) ' Render the results writer.Write(dtblContent.Rows(intRan)("item_Text")) Else writer.Write("Random Content") End If End Sub End Class
Build the myControls project by select Build myControls from the Build menu.
Next, we need to create a separate project for testing our control:
Open the New Project dialog box by selecting New from the File menu, and pointing to Project.
Select ASP.NET Web Application under Templates.
Select Add to Solution under Location.
Name the new project TestControls
and click OK.
Next, we need to add the Content Rotator
control to the Toolbox:
Add a new Web Form Page to your project named TestContentRotator.aspx
.
Right-click the Toolbox under the General tab and select Customize Toolbox.
Select the tab labeled .NET Framework Components.
Click the Browse button and browse to the following file:
My DocumentsVisual Studio ProjectsmyControlsmyControlsin DebugmyControls.dll
Click OK.
After you complete these steps, the Content Rotator
control should appear under the General tab. Before
we can test the control, we need to add a Content.xml file to the TestControls project.
Open the Add New Item dialog box by right-clicking the TestControls project in the Solution Explorer window and selecting Add, Add New Item. Select XML File under Templates, name the file Content.xml
and click Open.
Enter the following content for the Content.xml file:
<?xml version="1.0" encoding="utf-8" ?> <content> <item>Aliens attack!</item> <item>Life discovered on Mars!</item> <item>Moon explodes!</item> </content>
Save the changes to the Content.xml file by clicking the Save Content.xml button.
The final step is to add the control to a page and test it:
Drag the Content Rotator
control onto the TestContentRotator.aspx page.
Right-click the TestContentRotator.aspx file in the Solution Explorer window and select Build and Browse.
Every time you click the Refresh button on your browser, a new entry from the Content.xml file will be randomly selected and displayed in the TestControl.aspx page.
There’s one special thing that you should notice about the code for the Content Rotator
control. The following conditional is used to check whether the Content Rotator
is being displayed at design time (on the Designer surface) or at run time (when the TestControl.aspx page is executing):
C#.
VB.NET.
If Not HttpContext.Current Is Nothing Then ... End If
When there is no HttpContext
, the control is not actually being displayed in a browser. Therefore, you can use this check to skip actions that you don’t want performed when manipulating the control in the
Designer.
We
should really implement caching for our Content Rotator
control so that we can cache the Content.xml file in memory. The best way to implement the caching would be to create a file dependency on the Content.xml file. To learn more about creating file dependencies, see Chapter 14, “Improving Application Performance with Caching.”
A
composite control enables you to reuse existing controls in a new control. For example, you can create a new Address Form composite control by combining together exiting TextBox
and Validation
controls.
When we created our non-composite control in the previous section, we overrode the RenderContents()
method. When you create a composite control, on the other hand, you typically override the
CreateChildControls()
method.
Every control in the ASP.NET Framework has a Controls
collection. The Controls
collection contains all of a control’s child controls. For example, the Controls
collection of an Address Form
control would include TextBox
and Validation
controls.
The CreateChildControls()
method is responsible for creating all the controls contained in the Controls
collection. When you override this method, you add each of a control’s child controls using logic that looks like the following:
C#.
protected override void CreateChildControls() { // Add Street TextBox TextBox txtStreet = new TextBox(); txtStreet.ID = "txtStreet"; Controls.Add( txtStreet ); }
VB.NET.
Protected Overrides Sub CreateChildControls() ' Add Street TextBox Dim txtStreet As New TextBox() txtStreet.ID = "txtStreet" Controls.Add(txtStreet) End Sub
This
code adds a TextBox
to the Controls
collection. You can add any control you need—including Repeater, DropDownLists
, and DataGrid
controls—by tossing it into the Controls
collection.
There is an important method that works with the CreateChildControls
method called the EnsureChildControls()
method. The EnsureChildControls()
method calls the CreateChildControls()
method. However, it is guaranteed to call the CreateChildControls()
method only once. You can safely call the
EnsureChildControls
method over and over again without worrying about the Controls
collection being re-created.
You need to call the EnsureChildControls()
method within your control before accessing any child controls. For example, suppose that the Address Form
control has a property named Street
that returns the current value of the txtStreet
text box:
C#.
public string Street { get { EnsureChildControls(); return txtStreet.Text; } }
VB.NET.
Public ReadOnly Property Street() As String Get EnsureChildControls() Return txtStreet.Text End Get End Property
If you neglect to include EnsureChildControls()
in the Street
property, you would receive a Null Reference Exception. You would receive a Null Reference Exception because the child txtStreet
text box would not have been created yet.
The CreateChildControls()
method is unique in that you don’t know exactly when it will be called. It could be called whenever someone happens to read a property of your control. If the CreateChildControls()
method is not called before a control’s PreRender()
method is called, the CreateChildControls()
method is called automatically
at that point.
When
implementing a composite control, you should always implement the INamingContainer
interface. This interface is a marker interface. This means that it doesn’t actually have any methods that you must override.
The INamingContainer
interface is in the System.Web.UI
namespace, so you’ll need to import this namespace when implementing the interface.
You can implement the INamingContainer
interface when declaring a control like the following:
C#.
public class AddressForm : WebControl, INamingContainer { ... }
VB.NET.
Public Class AddressForm Inherits WebControl Implements INamingContainer ... End Class
The INamingContainer
interface creates a new namespace for your control. You need to implement this interface when developing composite controls to prevent ID naming collisions.
For example, if you don’t implement this interface in the case of the AddressForm
control, the values of any TextBox
controls contained within AddressForm
control will not be retained across postbacks. The framework would not be able to assign the values to the TextBox
controls because it would not be able to resolve the IDs of the TextBox
controls.
When you create
a composite control, you override the CreateChildControls()
method. You use the CreateChildControls()
method to specify all the child controls of the composite control.
However, typically, you’ll also want to override the Render()
method of a composite control. You’ll use the Render()
method to lay out the controls that you create in the CreateChildControls()
method.
For example, when creating an AddressForm
control, you’ll normally want to lay out the TextBox
and Validation
controls within an HTML table. That way, all the controls are not bunched together haphazardly on the page.
Listing 22.3 illustrates how you use both the CreateChildControls()
and the Render()
method for the AddressForm
control.
Example 22.3.CS. Partial AddressForm.cs
private TextBox txtStreet; private TextBox txtCity; protected override void CreateChildControls() { // Clear child controls Controls.Clear(); // Add Street TextBox txtStreet = new TextBox(); txtStreet.ID = "txtStreet"; Controls.Add( txtStreet ); // Add City TextBox txtCity = new TextBox(); txtCity.ID = "txtState"; Controls.Add( txtCity ); } protected override void Render(HtmlTextWriter writer) { // Add WebControl Formatting AddAttributesToRender(writer); // Open table writer.AddAttribute(HtmlTextWriterAttribute.Border, "1"); writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "4"); writer.RenderBeginTag( HtmlTextWriterTag.Table); // Create first row writer.RenderBeginTag( HtmlTextWriterTag.Tr); writer.RenderBeginTag( HtmlTextWriterTag.Td); writer.Write( "Street:" ); writer.RenderEndTag(); writer.RenderBeginTag( HtmlTextWriterTag.Td); txtStreet.RenderControl(writer); writer.RenderEndTag(); writer.RenderEndTag(); // Create second row writer.RenderBeginTag( HtmlTextWriterTag.Tr); writer.RenderBeginTag( HtmlTextWriterTag.Td); writer.Write( "City:" ); writer.RenderEndTag(); writer.RenderBeginTag( HtmlTextWriterTag.Td); txtCity.RenderControl(writer); writer.RenderEndTag(); writer.RenderEndTag(); // Close table writer.RenderEndTag(); }
Example 22.3.VB. Partial AddressForm.vb
Dim txtStreet As TextBox Dim txtCity As TextBox Protected Overrides Sub CreateChildControls() ' Clear child controls Controls.Clear() ' Add Street TextBox txtStreet = New TextBox() txtStreet.ID = "txtStreet" Controls.Add(txtStreet) ' Add City TextBox txtCity = New TextBox() txtCity.ID = "txtState" Controls.Add(txtCity) End Sub Protected Overrides Sub Render(ByVal writer As HtmlTextWriter) ' Add WebControl Formatting AddAttributesToRender(writer) ' Open table writer.AddAttribute(HtmlTextWriterAttribute.Border, "1") writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "4") writer.RenderBeginTag(HtmlTextWriterTag.Table) ' Create first row writer.RenderBeginTag(HtmlTextWriterTag.Tr) writer.RenderBeginTag(HtmlTextWriterTag.Td) writer.Write("Street:") writer.RenderEndTag() writer.RenderBeginTag(HtmlTextWriterTag.Td) txtStreet.RenderControl(writer) writer.RenderEndTag() writer.RenderEndTag() ' Create second row writer.RenderBeginTag(HtmlTextWriterTag.Tr) writer.RenderBeginTag(HtmlTextWriterTag.Td) writer.Write("City:") writer.RenderEndTag() writer.RenderBeginTag(HtmlTextWriterTag.Td) txtCity.RenderControl(writer) writer.RenderEndTag() writer.RenderEndTag() ' Close table writer.RenderEndTag() End Sub
Notice that the txtStreet
and txtCity TextBox
controls are created in the CreateChildControls()
method. However, they are actually rendered within the Render()
method by calling txtStreet.RenderControl(writer)
and txtCity.RenderControl(writer)
.
You don’t need to include a Render()
method in a composite control. If you leave it out, each of the controls created in the CreateChildControls()
method will be automatically rendered. However, the Render()
method provides you with access to the HtmlTextWriter
class that makes it easier to layout the controls contained in a composite
control.
When you create a non-composite control and add the control to the Visual Studio .NET Designer, you see exactly what will be displayed by the control when the control is displayed in a Web Form Page. In other words, whatever is rendered by a non-composite control at design time is exactly the same as whatever is rendered by the control at runtime.
By default, this is not true in the case of a composite control. You have to perform some additional work to get a composite control to look the same at design time as it does at runtime. I’m giving you this warning now so that you are not surprised when the composite control that we create in the next section does not appear correctly in the Designer.
The extra work involves creating something called a ControlDesigner
. You can use a ControlDesigner
with both composite and non-composite controls to specify how a control will appear at design time. We’ll discuss the ControlDesigner
designer class later in this chapter in the “Using the ControlDesigner
Class” section.
We’ve discussed pieces
of the AddressForm
control. In this section, we are going to put everything together and build the AddressForm
composite control from start to finish (see Figure 22.2).
Let’s start by creating a new Control Library project:
Open the New Project dialog box by selecting New from the File menu and pointing to Project.
Select Web Control Library under Templates.
Select Close Solution under Location.
Name the new project myCompositeControls
and click OK.
Next, we need to create the AddressForm
control:
Procedure 22.3. C# Steps
Add a new class named AddressForm.cs
to the myCompositeControls project by selecting Add Class from the Project menu.
Enter the following
code for the AddressForm.cs
class:
using System; using System.Web.UI; using System.Web.UI.WebControls; namespace myCompositeControls { public class AddressForm : WebControl, INamingContainer { private TextBox txtStreet; private TextBox txtCity; private TextBox txtState; private TextBox txtZip; private RequiredFieldValidator valStreet; private RequiredFieldValidator valCity; private RequiredFieldValidator valState; private RequiredFieldValidator valZip; public string Street { get { EnsureChildControls(); return txtStreet.Text; } } public string City { get { EnsureChildControls(); return txtCity.Text; } } public string State { get { EnsureChildControls(); return txtState.Text; } } public string Zip { get { EnsureChildControls(); return txtZip.Text; } } public override ControlCollection Controls { get { EnsureChildControls(); return base.Controls; } } protected override void CreateChildControls() { // Clear child controls Controls.Clear(); // Add Street TextBox txtStreet = new TextBox(); txtStreet.ID = "txtStreet"; Controls.Add( txtStreet ); // Add Street Validation valStreet = new RequiredFieldValidator(); valStreet.ControlToValidate = "txtStreet"; valStreet.Text = "*"; Controls.Add( valStreet ); // Add City TextBox txtCity = new TextBox(); txtCity.ID = "txtCity"; Controls.Add( txtCity ); // Add City Validation valCity = new RequiredFieldValidator(); valCity.ControlToValidate = "txtCity"; valCity.Text = "*"; Controls.Add( valCity ); // Add State TextBox txtState = new TextBox(); txtState.ID = "txtState"; Controls.Add( txtState ); // Add State Validation valState = new RequiredFieldValidator(); valState.ControlToValidate = "txtState"; valState.Text = "*"; Controls.Add( valState ); // Add Zip TextBox txtZip = new TextBox(); txtZip.ID = "txtZip"; Controls.Add( txtZip ); // Add State Validation valZip = new RequiredFieldValidator(); valZip.ControlToValidate = "txtZip"; valZip.Text = "*"; Controls.Add( valZip ); } protected override void Render(HtmlTextWriter writer) { // Add WebControl Formatting AddAttributesToRender(writer); // Open table writer.AddAttribute(HtmlTextWriterAttribute.Border, "1"); writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "4"); writer.RenderBeginTag( HtmlTextWriterTag.Table); // Create first row writer.RenderBeginTag( HtmlTextWriterTag.Tr); writer.RenderBeginTag( HtmlTextWriterTag.Td); writer.Write( "Street:" ); writer.RenderEndTag(); writer.RenderBeginTag( HtmlTextWriterTag.Td); txtStreet.RenderControl(writer); valStreet.RenderControl(writer); writer.RenderEndTag(); writer.RenderEndTag(); // Create second row writer.RenderBeginTag( HtmlTextWriterTag.Tr); writer.RenderBeginTag( HtmlTextWriterTag.Td); writer.Write( "City:" ); writer.RenderEndTag(); writer.RenderBeginTag( HtmlTextWriterTag.Td); txtCity.RenderControl(writer); valCity.RenderControl(writer); writer.RenderEndTag(); writer.RenderEndTag(); // Create third row writer.RenderBeginTag( HtmlTextWriterTag.Tr); writer.RenderBeginTag( HtmlTextWriterTag.Td); writer.Write( "State:" ); writer.RenderEndTag(); writer.RenderBeginTag( HtmlTextWriterTag.Td); txtState.RenderControl(writer); valState.RenderControl(writer); writer.RenderEndTag(); writer.RenderEndTag(); // Create fourth row writer.RenderBeginTag( HtmlTextWriterTag.Tr); writer.RenderBeginTag( HtmlTextWriterTag.Td); writer.Write( "ZIP:" ); writer.RenderEndTag(); writer.RenderBeginTag( HtmlTextWriterTag.Td); txtZip.RenderControl(writer); valZip.RenderControl(writer); writer.RenderEndTag(); writer.RenderEndTag(); // Close table writer.RenderEndTag(); } } }
Build the myCompositeControls project by selecting Build myCompositeControls from the Build menu.
Procedure 22.4. VB.NET Steps
Add a new class named AddressForm.vb
to the myCompositeControls project by selecting Add Class from the Project menu.
Enter the following code for the AddressForm.vb
class:
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls Public Class AddressForm Inherits WebControl Implements INamingContainer Private txtStreet As TextBox Private txtCity As TextBox Private txtState As TextBox Private txtZip As TextBox Private valStreet As RequiredFieldValidator Private valCity As RequiredFieldValidator Private valState As RequiredFieldValidator Private valZip As RequiredFieldValidator Public ReadOnly Property Street() As String Get EnsureChildControls() Return txtStreet.Text End Get End Property Public ReadOnly Property City() As String Get EnsureChildControls() Return txtCity.Text End Get End Property Public ReadOnly Property State() As String Get EnsureChildControls() Return txtState.Text End Get End Property Public ReadOnly Property Zip() As String Get EnsureChildControls() Return txtZip.Text End Get End Property Public Overrides ReadOnly Property Controls() As ControlCollection Get EnsureChildControls() Return MyBase.Controls End Get End Property Protected Overrides Sub CreateChildControls() ' Clear child controls Controls.Clear() ' Add Street TextBox txtStreet = New TextBox() txtStreet.ID = "txtStreet" Controls.Add(txtStreet) ' Add Street Validation valStreet = New RequiredFieldValidator() valStreet.ControlToValidate = "txtStreet" valStreet.Text = "*" Controls.Add(valStreet) ' Add City TextBox txtCity = New TextBox() txtCity.ID = "txtCity" Controls.Add(txtCity) ' Add City Validation valCity = New RequiredFieldValidator() valCity.ControlToValidate = "txtCity" valCity.Text = "*" Controls.Add(valCity) ' Add State TextBox txtState = New TextBox() txtState.ID = "txtState" Controls.Add(txtState) ' Add State Validation valState = New RequiredFieldValidator() valState.ControlToValidate = "txtState" valState.Text = "*" Controls.Add(valState) ' Add Zip TextBox txtZip = New TextBox() txtZip.ID = "txtZip" Controls.Add(txtZip) ' Add State Validation valZip = New RequiredFieldValidator() valZip.ControlToValidate = "txtZip" valZip.Text = "*" Controls.Add(valZip) End Sub Protected Overrides Sub Render(ByVal writer As HtmlTextWriter) ' Add WebControl Formatting AddAttributesToRender(writer) ' Open table writer.AddAttribute(HtmlTextWriterAttribute.Border, "1") writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "4") writer.RenderBeginTag(HtmlTextWriterTag.Table) ' Create first row writer.RenderBeginTag(HtmlTextWriterTag.Tr) writer.RenderBeginTag(HtmlTextWriterTag.Td) writer.Writer("Street:") writer.RenderEndTag() writer.RenderBeginTag(HtmlTextWriterTag.Td) txtStreet.RenderControl(writer) valStreet.RenderControl(writer) writer.RenderEndTag() writer.RenderEndTag() ' Create second row writer.RenderBeginTag(HtmlTextWriterTag.Tr) writer.RenderBeginTag(HtmlTextWriterTag.Td) writer.Write("City:") writer.RenderEndTag() writer.RenderBeginTag(HtmlTextWriterTag.Td) txtCity.RenderControl(writer) valCity.RenderControl(writer) writer.RenderEndTag() writer.RenderEndTag() ' Create third row writer.RenderBeginTag(HtmlTextWriterTag.Tr) writer.RenderBeginTag(HtmlTextWriterTag.Td) writer.Write("State:") writer.RenderEndTag() writer.RenderBeginTag(HtmlTextWriterTag.Td) txtState.RenderControl(writer) valState.RenderControl(writer) writer.RenderEndTag() writer.RenderEndTag() ' Create fourth row writer.RenderBeginTag(HtmlTextWriterTag.Tr) writer.RenderBeginTag(HtmlTextWriterTag.Td) writer.Write("ZIP:") writer.RenderEndTag() writer.RenderBeginTag(HtmlTextWriterTag.Td) txtZip.RenderControl(writer) valZip.RenderControl(writer) writer.RenderEndTag() writer.RenderEndTag() ' Close table writer.RenderEndTag() End Sub End Class
Build the myCompositeControls project by selecting Build myCompositeControls from the Build menu.
Next, we need to create a separate project for testing our control:
Open the New Project dialog box by selecting New from the File menu and pointing to Project.
Select ASP.NET Web Application under Templates.
Select Add to Solution under Location.
Name the new project TestCompositeControls
and click OK.
Next, we need to add the Address Form
control to the Toolbox:
Add a new Web Form Page to your project named TestAddressForm.aspx
.
Right-click the Toolbox under the General tab and select Customize Toolbox.
Select the .NET Framework Components tab.
Click the Browse button and browse to the following file:
My DocumentsVisual Studio ProjectsmyCompositeControls myCompositeControlsinDebug myCompositeControls.dll
Click OK.
After you complete these steps, the Address Form
control should appear under the General tab. We are finally ready to test out the control:
Drag the Address Form control onto the TestAddressForm.aspx page.
Right-click the TestAddressForm.aspx page in the Solution Explorer window and select Build and Browse.
When the TestAddressForm.aspx page opens, you can enter address information into the control. Notice that each text box is validated with a RequiredFieldValidator
. You can retrieve the entries in the Address Form
control by using the Street, City, State
, and Zip
properties
of the control.
The primary advantage of creating custom Web Form controls instead of Web User Controls is that custom Web Form controls provide better Designer support. You can add custom Web Form controls to the Toolbox, you can modify the properties of custom Web Form controls in the Properties window, and you can see the content rendered by custom Web Form controls during design time.
In this section, you’ll learn how to take control over the appearance of custom controls in the Visual Studio .NET environment. First, we’ll look at the special attributes that you can apply to a custom control. Next, we’ll look at how you can customize the appearance of a custom control in the Toolbox. Finally, you’ll learn how to take advantage of ControlDesigners
to take control over the appearance of your control on the Designer surface.
There are a
number of special design-time attributes that you can apply to your control. For example, you can use the DefaultProperty
attribute to specify the property that is selected by default in the Properties window when you click a control. The following is a list of the most important design attributes that you can apply at the class, property, or event level:
Bindable
. Contains a Boolean value that indicates whether a property can be used for binding
Browsable
. Contains a Boolean value that determines whether a property or event appears in the Properties window
Category
. Contains a string value that determines the category associated with a property or event
DefaultEvent
. Contains a string value that specifies the default event associated with a control
DefaultProperty
. Contains a string value that represents the property that is selected by default in the Properties window
DefaultValue
. Contains an object that represents the default value for a property
Description
. Contains a string value that determines the help text associated with a property or event at the bottom of the Properties window
Editor
. Contains the name and type of the editor used for editing the value of a property in the Designer
ToolboxData
. Contains a string that specifies the tag generated for a control when the control is dragged onto the Designer surface
TypeConverter
. Contains a string or type that specifies a type converter used for converting the value of a property into a form that can be persisted
For example, the control in Listing 22.4 uses several of these attributes to determine its appearance in the Visual Studio .NET Designer.
Example 22.4.CS. myControl.cs
using System; using System.Web.UI; using System.Web.UI.WebControls; using System.ComponentModel; namespace myControls { [DefaultProperty("Text"), ToolboxData("<{0}:myControl runat=server></{0}:myControl>")] public class myControl : System.Web.UI.WebControls.WebControl { private string text; [Bindable(true), Category("Appearance"), Description("The text this control displays")] public string Text { get { return text; } set { text = value; } } protected override void Render(HtmlTextWriter output) { output.Write(Text); } } }
Example 22.4.VB. myControl.vb
Imports System.Web.UI Imports System.Web.UI.WebControls Imports System.ComponentModel <DefaultProperty("Text"), _ ToolboxData("<0}:myControl runat=server></0}:myControl>")> _ Public Class myControl Inherits System.Web.UI.WebControls.WebControl Private _text As String <Bindable(True), _ Category("Appearance"), _ Description("The text this control displays")> _ Public Property Text() As String Get Return _text End Get Set(ByVal Value As String) _text = Value End Set End Property Protected Overrides Sub Render(ByVal output As HtmlTextWriter) output.Write(_text) End Sub End Class
There is another design-time attribute that applies at the assembly level instead of the class, property, or event level. The TagPrefix
attribute enables you to
specify the tag that appears when your control is declared on a page.
By default, the first control added to a page appears with the tag prefix cc1
, the second cc2
, and so on. If you want to provide a more descriptive tag prefix, you need to assign a value to the TagPrefix
attribute.
When you create a new Web Control Library project, a file is automatically added to your project named AssemblyInfo.cs or AssemblyInfo.vb. You need to add the TagPrefix
attribute to this file. You can add the following attribute anywhere in this file:
C#.
[assembly: System.Web.UI.TagPrefix("myControls","Super")]
VB.NET.
<Assembly: System.Web.UI.TagPrefix("myControls", "Super")>
The first parameter, myControls
, refers to the namespace. Typically, this will be the same name as your project. The second parameter, Super
, is the tag prefix generated when you add a control to a
page.
When you add a custom Web control to the Toolbox, it appears with an icon of a gear by default. You can create your own 16×16 pixel bitmap image to customize this icon.
Open the Image Editor by right-clicking the project that contains your custom control in the Solution Explorer window and selecting Add New Item from the Add menu. Select the Bitmap File template. Make sure that you named the Bitmap image with the same name as your control and click Open.
In the Properties window, set the Width and Height properties to the value 16.
Use the Image Editor tool to draw an appropriate image for your control.
Click the Save button to save your image.
In the Solution Explorer window, select your image.
In the Properties window, assign the value Embedded Resource
to the Build Action property.
Build your project by selecting Build Project Name from the Build menu.
After you complete these steps, you might need to remove and re-add the control to the Toolbox for the changes to appear.
If you want
your control to appear differently in the Visual Studio .NET Designer than it appears at runtime, you can use the ControlDesigner
class to specify a design-time appearance for your control.
The ControlDesigner
class has a method called GetDesignTimeHtml()
that you can override to specify the design-time appearance of a control. This method simply returns a string that is used for rendering the control in the Designer. After you create a ControlDesigner
, you associate it with a control by using the Designer
attribute.
For example, the ControlDesigner
in Listing 22.5 renders the text Hello World!.
Example 22.5.CS. myControlDesigner.cs
using System; using System.Web.UI.Design; namespace myControls { public class myControlDesigner : ControlDesigner { public override string GetDesignTimeHtml() { return "Hello World!"; } } }
You can associate the ControlDesigner
in Listing 22.5 with a control by adding the following attribute to the declaration of a control class:
C#.
[System.ComponentModel.Designer(typeof(myControlDesigner))] public class myControl : WebControl, INamingContainer { ... }
VB.NET.
<System.ComponentModel.Designer(GetType(myControlDesigner))> _ Public Class myControl ... End Class
The ControlDesigner
class is part of the System.Web.UI.Design
namespace. This namespace is located in an assembly that is not one of the default assemblies referenced in a project. You must add a reference to the System.Design.dll assembly before using the ControlDesigner
class.
You can create a ControlDesigner
for any type of control. However, you’ll almost always want to associate a ControlDesigner
class with a composite control because a composite control does not appear correctly in the Designer.
For
example, earlier in this chapter, we created an Address Form
composite control. To get the Address Form
control to appear correctly in the Designer, do the following:
Procedure 22.5. C# Steps
Add a new class to the myCompositeControls project
named AddressFormDesigner.cs
.
Add a new reference to the System.Design
assembly to your project by right-clicking the References folder, selecting Add Reference, and selecting System.Design.dll.
Add a new class to your project named AddressFormDesigner
.
Enter the following code for the AddressFormDesigner
class:
using System; using System.Web.UI; using System.Web.UI.Design; namespace myControls { public class AddressFormDesigner : ControlDesigner { public override string GetDesignTimeHtml() { ControlCollection AddressFormControls = ((Control)Component).Controls; return base.GetDesignTimeHtml(); } } }
Modify the AddressForm control by adding the following attribute before the class declaration:
[System.ComponentModel.Designer(typeof(AddressFormDesigner))]
Rebuild the Solution by selecting Build Solution from the Build menu.
Procedure 22.6. VB.NET Steps
Add a new class to the myCompositeControls project named AddressFormDesigner.vb
.
Add a new reference to the System.Design
assembly to your project by right-clicking the References folder, selecting Add Reference, and selecting System.Design.dll.
Add a new class to your project named AddressFormDesigner
.
Enter the following code for the AddressFormDesigner
class:
Imports System.Web.UI.Design Imports System.Web.UI Public Class AddressFormDesigner Inherits ControlDesigner Public Overrides Function GetDesignTimeHTML() As String Dim AddressFormControls As ControlCollection AddressFormControls = CType(Component, Control).Controls Return MyBase.GetDesignTimeHtml() End Function End Class
Modify the AddressForm control by adding the following attribute before the class declaration:
<System.ComponentModel.Designer(GetType(AddressFormDesigner))> _
Rebuild the Solution by selecting Build Solution from the Build menu.
The important part of the AddressFormDesigner
is the following line of code:
C#.
ControlCollection AddressFormControls = ((Control)Component).Controls;
VB.NET.
AddressFormControls = CType(Component, Control).Controls
The ControlDesigner
class
automatically creates the Component variable. It represents the AddressForm
control. Consequently, this line of code retrieves the Controls
collection from the AddressForm
control.
When we created the AddressForm
control, we overrode the Controls
property to include a call to the EnsureChildControls()
method. Therefore, because the AddressFormDesigner
accesses the Controls
collection, the AddressForm
control is forced to create its child controls and render correctly in the Visual Studio .NET Designer.
In this chapter, you learned how to create custom Web Form controls that work in the Visual Studio .NET environment. In the first section, you learned how to create non-composite controls. We created a simple Content Rotator
control that randomly retrieves content items from an XML file.
Next, we examined the topic of composite controls. You learn how to use composite controls to bundle together the functionality of existing controls in a new control. We create a simple Address Form
composite control.
Finally, we looked at methods for controlling the appearance of a custom control in the Visual Studio .NET environment. You learned how to use design-time attributes, modify the appearance of a control in the Toolbox, and create a custom ControlDesigner
.