Building Custom Control


You must answer two questions before writing a custom control:
What type of control do I want to write?
From what class do I inherit?

The two basic types of controls are fully rendered and composite controls. When you build a fully rendered control, you start from scratch. You specify all the HTML content that the control renders to the browser. When you create a composite control, on the other hand, you build a new control from existing controls. For example, you can create a composite AddressForm control from existing TextBox and RequiredFieldValidator controls. When you create a composite control, you bundle together existing controls as a new control. The second question that you must address is the choice of the base control for your new control. You can inherit a new control from any existing ASP.NET control. For example, if you want to create a better GridView control, then you can inherit a new control from the GridView control and add additional properties and methods to your custom GridView control.

Typically, when building a basic control, you inherit your new control from one of the following base classes:
. System.Web.UI.Control
. System.Web.UI.WebControls.WebControl
. System.Web.UI.WebControls.CompositeControl


The CompositeControl class inherits from the WebControl class, which inherits from the Control class. Each of these base classes adds additional functionality. The base class for all controls in the ASP.NET Framework is the System.Web.UI.Control class. Every control, including the TextBox and GridView controls, ultimately derives from this control. This means that all the properties, methods, and events of the System.Web.UI.Control class are shared by all controls in the Framework.

Building Fully Rendered Controls
Let’s start by creating a simple fully rendered control. When you create a fully rendered control, you take on the responsibility of specifying all the HTML content that the control renders to the browser.
The file in Listing contains a fully rendered control that derives from the base Control class.

LISTING FullyRenderedControl.cs
using System.Web.UI;
namespace myControls
{
public class FullyRenderedControl : Control
{
private string _Text;
public string Text
{
get { return _Text; }
set { _Text = value; }
}
protected override void Render(HtmlTextWriter writer)
{
writer.Write(_Text);
}
}
}

Add the control in Listing to your App_Code folder. Any code added to the App_Code folder is compiled dynamically. The control in Listing inherits from the base Control class, overriding the base class Render() method. The control simply displays whatever value that you assign to its Text property. The value of the Text property is written to the browser with the HtmlTextWriter class’s Write() method. The file in Listing illustrates how you can use the new control in a page.

LISTING ShowFullyRenderedControl.aspx
<%@ Page Language=”C#” %>
<%@ 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 id=”Head1” runat=”server”>
<title>Show Fully Rendered Control</title>
</head>
<body>
<form id=”form1” runat=”server”>
<div>
<custom:FullyRenderedControl ID=”FullyRenderedControl1” Text=”Hello World!” runat=”Server” />
</div>
</form>
</body>
</html>

In Listing, the custom control is registered in the page through use of the <%@ Register %> directive. Alternatively, you can register the control for an entire website by registering the control in the <pages> section of the web configuration file. If you open the page in Listing in a browser and select View Source, you can see the HTML rendered by the control. The control simply renders the string ”Hello World!”. Rather than inherit from the base Control class, you can create a fully rendered control by inheriting a new control from the base WebControl class. When inheriting from the WebControl class, you override the RenderContents() method instead of the Render() method.

For example, the control in Listing contains a simple fully rendered control that inherits from the WebControl class.

LISTING FullyRenderedWebControl.cs
using System.Web.UI;
using System.Web.UI.WebControls;
namespace myControls
{
public class FullyRenderedWebControl : WebControl
{
private string _Text;
public string Text
{
get { return _Text; }
set { _Text = value; }
}
protected override void RenderContents(HtmlTextWriter writer)
{
writer.Write(_Text);
}
}
}

The page in Listing illustrates how you can use the new control. Notice that the BackColor, BorderStyle, and Font properties are set. Because the control in Listing derives from the base WebControl class, you get these properties for free.

LISTING ShowFullyRenderedWebControl.aspx
<%@ Page Language=”C#” %>
<%@ 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 id=”Head1” runat=”server”>
<title>Show Fully Rendered WebControl</title>
</head>
<body>
<form id=”form1” runat=”server”>
<div>
<custom:FullyRenderedWebControl ID=”FullyrenderedWebControl1” Text=”Hello World”
BackColor=”Yellow” BorderStyle=”Dashed” Font-Size=”32px” Runat=”Server” />
</div>
</form>
</body>
</html>

After opening the page in Listing, if you select View Source in your browser, you can see the rendered output of the control. It looks like this:

<span id=”FullyrenderedWebControl1” style=”display:inline-block;backgroundcolor:
Yellow;border-style:Dashed;font-size:32px;”>Hello World</span>

A WebControl, unlike a control, renders an enclosing <span> tag by default. Understanding the HtmlTextWriter Class When you create a fully rendered control, you use the HtmlTextWriter class to write the HTML content to the browser. The HtmlTextWriter class was specifically designed to make it easier to render HTML. Here is a partial list of the methods supported by this class:

AddAttribute()—Adds an HTML attribute to the tag rendered by calling RenderBeginTag().
AddStyleAttribute()—Adds a CSS attribute to the tag rendered by a call to RenderBeginTag().
RenderBeginTag()—Renders an opening HTML tag.
RenderEndTag()—Renders a closing HTML tag.
Write()—Renders a string to the browser.
WriteBreak()—Renders a <br /> tag to the browser.

You can call the AddAttribute() or the AddStyleAttribute() method as many times as you want before calling RenderBeginTag(). When you call RenderBeginTag(), all the attributes are added to the opening HTML tag. The methods of the HtmlTextWriter class can use the following enumerations:
HtmlTextWriterTag—Contains a list of the most common HTML tags.
HtmlTextWriterAttribute—Contains a list of the most common HTML attributes.
HtmlTextWriterStyle—Contains a list of the most Cascading Style Sheet attributes.

When using the methods of the HtmlTextWriter class, you should strive to use these enumerations to represent HTML tags and attributes. If a particular tag or attribute is missing from one of the enumerations, you can pass a string value instead.

Filters are an Internet Explorer extension to the Cascading Style Sheet standard. They don’t work with Firefox or Opera. Firefox has its own extensions to Cascading Style Sheets with its -moz style rules. Specifying the Containing WebControl Tag By default, a WebControl renders an HTML <span> tag around its contents. You can specify a different tag by overriding the WebControl’s TagKey property. For example, the control in Listing renders its contents within an HTML <div> tag.

LISTING Glow.cs
using System.Web.UI;
using System.Web.UI.WebControls;
namespace myControls
{
public class Glow : WebControl
{
private string _Text;
public string Text
{
get { return _Text; }
set { _Text = value; }
}
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.Div;
}
}
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
writer.AddStyleAttribute(HtmlTextWriterStyle.Filter,
“glow(Color=#ffd700,Strength=10)”);
base.AddAttributesToRender(writer);
}
protected override void RenderContents(HtmlTextWriter writer)
{
writer.Write(_Text);
}
public Glow()
{
this.Width = Unit.Parse(“500px”);
}
}
}

The control in Listing displays a glowing effect around any text that you assign to its Text property. The control takes advantage of the Internet Explorer Glow filter to create the glow effect.

Notice that the control overrides the base WebControl’s TagKey property. Because the overridden property returns a <div> tag, the WebControl renders a <div> tag. There are several methods you can use to modify the tag rendered by a WebControl. You can override the TagName property instead of the TagKey property. The TagName property enables you to specify an arbitrary string for the tag. (It doesn’t limit you to the HtmlTextWriterTag enumeration.) You also can specify the tag rendered by a WebControl in the WebControl’s constructor. Finally, you can override a WebControl’s RenderBeginTag() and RenderEndTag() methods and completely customize the opening and closing tags.

Building Hybrid Controls
In practice, you rarely build pure composite controls. In most cases in which you override a control’s CreateChildControls() method, you also override the control’s RenderContents() method to specify the layout of the child controls. For example, the control in Listing represents a Login control. In the control’s CreateChildControls() method, two TextBox controls are added to the control’s collection of child controls.

LISTING Login.cs
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
1514 CHAPTER 29 Building Custom Controls
LISTING 29.10 Continued
namespace myControls
{
public class Login : CompositeControl
{
private TextBox txtUserName;
private TextBox txtPassword;
public string UserName
{
get
{
EnsureChildControls();
return txtUserName.Text;
}
set
{
EnsureChildControls();
txtUserName.Text = value;
}
}
public string Password
{
get
{
EnsureChildControls();
return txtPassword.Text;
}
set
{
EnsureChildControls();
txtPassword.Text = value;
}
}
protected override void CreateChildControls()
{
txtUserName = new TextBox();
txtUserName.ID = “txtUserName”;
this.Controls.Add(txtUserName);
txtPassword = new TextBox();
txtPassword.ID = “txtPassword”;
txtPassword.TextMode = TextBoxMode.Password;
this.Controls.Add(txtPassword);
}
protected override void RenderContents(HtmlTextWriter writer)
{
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
// Render UserName Label
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.AddAttribute(HtmlTextWriterAttribute.For, txtUserName.ClientID);
writer.RenderBeginTag(HtmlTextWriterTag.Label);
writer.Write(“User Name:”);
writer.RenderEndTag(); // Label
writer.RenderEndTag(); // TD
// Render UserName TextBox
writer.RenderBeginTag(HtmlTextWriterTag.Td);
txtUserName.RenderControl(writer);
writer.RenderEndTag(); // TD
writer.RenderEndTag();
writer.RenderBeginTag(HtmlTextWriterTag.Tr);
// Render Password Label
writer.RenderBeginTag(HtmlTextWriterTag.Td);
writer.AddAttribute(HtmlTextWriterAttribute.For, txtPassword.ClientID);
writer.RenderBeginTag(HtmlTextWriterTag.Label);
writer.Write(“Password:”);
writer.RenderEndTag(); // Label
writer.RenderEndTag(); // TD
// Render Password TextBox
writer.RenderBeginTag(HtmlTextWriterTag.Td);
txtPassword.RenderControl(writer);
writer.RenderEndTag(); // TD
writer.RenderEndTag(); // TR
}
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.Table;
}
}
}
}

In Listing, the RenderContents() method is overridden in order to layout the two TextBox controls. The TextBox controls are rendered within an HTML table . Notice that each TextBox is rendered by calling the RenderControl() method. The default RenderContents() method simply calls the RenderControl() method for each child control. If you override the RenderContents() method, you have more control over the layout of the control.

The ASP.NET Framework takes advantage of a hidden form field named __VIEWSTATE to preserve the state of control properties across postbacks. If you want your controls to preserve the values of their properties, then you need to add the values of your control properties to this hidden form field. The ASP.NET Framework supports two methods of preserving values across postbacks. You can take advantage of either View State or Control State.

Supporting View State
You can use the ViewState property of the Control or Page class to add values to View State. The ViewState property exposes a dictionary of key and value pairs. For example, the following statement adds the string Hello World! to View State:
ViewState(“message”) = “Hello World!”

Technically, you can add an instance of any serializable class to View State. In practice, however, you should add only simple values to View State, such as Strings, DateTimes, and Integers. Remember that anything that you add to View State must be added to the hidden __VIEWSTATE form field. If this field gets too big, it can have a significant impact on your page’s performance. The control in Listing has two properties, named Text and ViewStateText. The first property does not use View State, and the second property does use View State. The value of the ViewStateText property is preserved across postbacks automatically.

LISTING ViewStateControl.cs
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace myControls
{
public class ViewStateControl : WebControl
{
private string _text;
public string Text
{
get { return _text; }
set { _text = value; }
}
public string ViewStateText
{
get
{
if (ViewState[“ViewStateText”] == null)
return String.Empty;
else
return (string)ViewState[“ViewStateText”];
}
set { ViewState[“ViewStateText”] = value; }
}
protected override void RenderContents(HtmlTextWriter writer)
{
writer.Write(“Text: “ + Text);
writer.WriteBreak();
writer.Write(“ViewStateText: “ + ViewStateText);
writer.WriteBreak();
}
}
}

Notice that the ViewStateText property uses the Control’s ViewState collection to preserve whatever value is assigned to the ViewStateText property across postbacks. When you add a value to the ViewState collection, the value is stuffed into the hidden __VIEWSTATE form field automatically. View State is loaded after the Page InitComplete event, and View State is saved after the Page PreRenderComplete event. This means that you should not attempt to retrieve a value from View State before or during the InitComplete event. You also should not attempt to add a value to View State after the PreRenderComplete event.

Supporting Control State
The ASP.NET Framework includes a feature named Control State. Control State is very similar to View State. Just like View State, any values that you add to Control State are preserved in the hidden __VIEWSTATE form field. However, unlike View State, Control State cannot be disabled. Control State is intended to be used only for storing crucial information across postbacks.

Control State was introduced to address a problem that developers encountered in the first version of the ASP.NET Framework. You can disable View State for any control by assigning the value False to a control’s EnableViewState property. Often, this is a very good idea for performance reasons. However, disabling View State also made several controls nonfunctional.
For example, by default a GridView control retains the values of all the records that it displays in View State. If you display 500 database records with a GridView control, then by default all 500 records are stuffed into the hidden __VIEWSTATE form field. To improve performance, you might want to disable View State for the GridView. However, a GridView uses the __VIEWSTATE form field to remember crucial information required for the proper functioning of the control, such as the current page number and the currently selected row. You don’t want the GridView to forget this critical information even when View State is disabled.

The concept of Control State was introduced enable you to save critical information in the hidden __VIEWSTATE form field even when View State is disabled. Microsoft makes it slightly more difficult to use Control State because they don’t want you to overuse this feature. You should use it only when storing super critical information.

Processing Postback Data and Events
The ASP.NET Framework is built around web forms. ASP.NET controls pass information from the browser to the server by submitting a form to the server. This process of posting a form back to the server is called a postback. When an ASP.NET page processes a form that has been posted back to the server, two
types of information can be passed to the controls in the page.

First, if a control initiates a postback, then a server-side event can be raised when the form is posted to the server. For example, if you click a Button control, then a Click event is raised on the server when the form containing the Button is posted back to the server. This event is called a postback event.

Second, the form data contained in the web form can be passed to a control. For example, when you submit a form that contains a TextBox control, the form data is passed to the TextBox control when the web form is submitted to the server. This form data is called the postback data. When building a custom control, you might need to process either postback data or a postback event. In this section, you learn how to implement the required control interfaces for processing postbacks.

Handling Postback Data
If your control needs to process form data submitted to the server, then you need to implement the IPostbackDataHandler interface. This interface includes the following two methods:
LoadPostData()—Receives the form fields posted from the browser.
RaisePostDataChangedEvent()—Enables you to raise an event indicating that the value of a form field has been changed.

Processing Postback Data and Events
Using Postback Options Postbacks are more complicated than you might think. A postback can involve cross-page posts, validation groups, and programmatic control of control focus. To implement these advanced features in a custom control, you need to be able to specify advanced postback options. You specify advanced postback options by taking advantage of the PostBackOptions class. This class has the following properties:

ActionUrl—Enables you to specify the page where form data is posted.
Argument—Enables you to specify a postback argument.
AutoPostBack—Enables you to add JavaScript necessary for implementing an AutoPostBack event.
ClientSubmit—Enables you to initiate the postback through client-side script.
PerformValidation—Enables you to specify whether validation is performed (set by the CausesValidation property).
RequiresJavaScriptProtocol—Enables you to generate the JavaScript: prefix.
TargetControl—Enables you to specify the control responsible for initiating the postback.
TrackFocus—Enables you to scroll the page back to its current position and return focus to the control after a postback.
ValidationGroup—Enables you to specify the validation group associated with the control.

Working with Control Property Collections
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 serverside 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 illustrates how you can create a content rotator control when ParseChildren has the value False.

LISTING ContentRotator.cs
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace myControls
{

[ParseChildren(false)]
public class ContentRotator : WebControl
{
protected override void AddParsedSubObject(object obj)
{
if (obj is Content)
base.AddParsedSubObject(obj);
}
protected override void RenderContents(HtmlTextWriter writer)
{
Random rnd = new Random();
int index = rnd.Next(this.Controls.Count);
this.Controls[index].RenderControl(writer);
}
}
public class Content : Control
{
}
}

The file in Listing 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. 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.

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 controls—such as Literal controls—from being added to the Controls collection.

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.

Creating a Better Designer Experience
Up to this point, we’ve ignored the Design view experience. In other words, we’ve ignored the question of how our custom controls appear in the Visual Web Developer or Visual Studio .NET Design view. You can modify the appearance of your control in Design view in two ways. You can apply design-time attributes to the control, or you can associate a ControlDesigner with your control. We’ll explore both methods in this section.

Applying Design-Time Attributes to a Control
Design-time attributes enable you to modify how control properties appear in Design view. Some attributes are applied to the control itself, whereas other attributes are applied to particular properties of a control. Here is the list of the design-time attributes you can apply to a control:

DefaultEvent—Enables you to specify the default event for a control. When you double-click a control in Visual Web Developer or Visual Studio .NET, an event handler is automatically created for the default event.
DefaultProperty—Enables you to specify the default property for a control. When you open the Property window for a control, this property is highlighted by default.
PersistChildren—Enables you to specify whether child controls or properties are persisted as control attributes or control contents.
ToolboxData—Enables you to specify the tag added to a page when a control is dragged from the Toolbox.
ToolboxItem—Enables you to block a control from appearing in the Toolbox.

Here is the list of design-time attributes you can apply to a control property:
Bindable—Enables you to indicate to display a Databindings dialog box for the property.
Browsable—Enables you to block a property from appearing in the Properties window.
Category—Enables you to specify the category associated with the property. The property appears under this category in the Properties window.
DefaultValue—Enables you to specify a default value for the property. When you right-click a property in the Properties window, you can select Reset to the return the property to its default value.
Description—Enables you to specify the description associated with the property. The description appears in the Properties window when the property is selected.
DesignerSerializationVisibility—Enables you to specify how changes to a property are serialized. Possible values are Visible, Hidden, and Content.
Editor—Enables you to specify a custom editor for editing the property in Design view.
EditorBrowsable—Enables you to block a property from appearing in Intellisense.
NotifyParentProperty—Enables you to specify that changes to a subproperty should be propagated to the parent property.
PersistenceMode—Enables you to specify whether a property is persisted as a control attribute or control content. Possible values are Attribute, EncodedInnerDefaultProperty, InnerDefaultProperty, and InnerProperty.
TypeConverter—Enables you to associate a custom type converter with a property. A type converter converts a property between a string representation and a type (or vice versa).

The Editor attribute enables you to associate a particular editor with a property. Certain types in the Framework have default editors. For example, a property which represents a System.Drawing.Color value is automatically associated with the ColorEditor. The ColorEditor displays a color picker. To view the list of editors included in the .NET Framework, look up the UITypeEditor class in the .NET Framework SDK Documentation.

Creating Control Designers
You can modify the appearance of your custom controls in Design view by creating a ControlDesigner. The ASP.NET Framework enables you to implement a number of fancy features when you implement a ControlDesigner. This section focuses on just two of these advanced features. First, you learn how to create a ContainerControlDesigner. A ContainerControlDesigner enables you to drag and drop other controls from the Toolbox onto your control in Design view.

You also learn how to add Smart Tags (also called Action Lists) to your control. When a control supports Smart Tags, a menu of common tasks pop up above the control in Design view.

Creating a Container ControlDesigner
If you associate a custom control with a ContainerControlDesigner, then you can add child controls to your control in Design view.  The SmartImage control takes advantage of an Internet Explorer filter named the BasicImage filter. This filter enables you to manipulate images by rotating, mirroring, and changing the opacity of images. The SmartImage control is associated with a ControlDesigner named the SmartImageDesigner through the control’s Designer attribute. The SmartImageDesigner class overrides the base class’s ActionLists property to expose a custom DesignerActionList.

The DesignerActionList is the final class declared in. This class contains four methods named Rotate(), DoRotate(), Mirror(), and DoMirror(). The GetSortedActionItems() method exposes the Rotate and Mirror actions. When all is said and done, the custom ActionList enables you to display Rotate and  Mirror Smart Tags for the SmartImage control in Design view. When you open a page in the browser after clicking the Rotate action in Design view, the image is rotated .

You can view the SmartImage control by opening the ShowSmartImage.aspx page included on the CD that accompanies this book. Sadly, although you could rotate the monkey while in Design view in the previous version of Visual Web Developer, in this version the rotated monkey does not appear until you view the page in a browser. 

No comments:

Post a Comment

Post a Comment