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.