WHAT’S IN THIS CHAPTER?
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
Please note that all the code examples in this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com on the Download Code tab.
In an object-oriented environment like .NET, the encapsulation of code into small, single-purpose, reusable objects is one of the keys to developing a robust system. For example, if your application deals with customers, you might consider creating a Customer object to represent a single instance of a Customer. This object encapsulates all the properties and behaviors a Customer can perform in the system. The advantage of creating this object is that you create a single point with which other objects can interact, and a single point of code to create, debug, deploy, and maintain. Objects like a Customer object are typically known as business objects because they encapsulate all of the business logic needed for a specific entity in the system.
.NET, being an object-oriented framework, includes many other types of reusable objects. The focus of this chapter is to discuss and demonstrate how you can use the .NET Framework to create two different types of reusable visual components for an ASP.NET application — user controls and server controls:
Because the topics of user controls and server controls are so large, and because discussing the intricacies of each could easily fill an entire book by itself, this chapter can’t possibly investigate every option available to you. Instead, it attempts to give you a brief overview of building and using user controls and server controls and demonstrates some common scenarios for each type of control. By the end of this chapter, you should have learned enough to start building basic controls of each type and be able to continue to learn on your own.
User controls represent the most basic form of ASP.NET visual encapsulation. Because they are the most basic, they are also the easiest to create and use. Essentially, a user control is used to group existing server controls into a single-container control. This enables you to create powerful objects that you can use easily throughout an entire web project.
Creating user controls is very simple in Visual Studio 2012. To create a new user control, you first add a new User Control file to your website. From the Website menu, select the Add New Item option. After the Add New File dialog box appears, select the Web User Control File template from the list and click Add. Notice that after the file is added to the project, the file has an .ascx extension. This extension signals to ASP.NET that this file is a user control. If you attempt to load the user control directly into your browser, ASP.NET returns an error telling you that this type of file cannot be served to the client.
If you look at the HTML source shown in Listing 7-1, you see several interesting differences from a standard ASP.NET web page.
LISTING 7-1: A web user control file template
<%@ Control Language="C#" ClassName="Listing07_01" %>
<script runat="server">
</script>
Notice that the source uses the @Control directive rather than the @Page directive that a standard web page would use. Also notice that unlike a standard ASP.NET web page, no other HTML tags besides the <script> tags exist in the control. The web page containing the user control provides the basic HTML, such as the <body> and <form> tags. In fact, if you try to add a server-side form tag to the user control, ASP.NET returns an error when the page is served to the client. The error message tells you that only one server-side form tag is allowed in your web page.
To add controls to the form, simply drag them from the Toolbox onto your user control. Listing 7-2 shows the user control after a label and a button have been added.
LISTING 7-2: Adding controls to the web user control
<%@ Control Language="C#" ClassName="Listing07_02" %>
<script runat="server">
</script>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
<asp:Button ID="Button1" runat="server" Text="Button" />
After you add the controls to the user control, you put the user control onto a standard ASP.NET web page. To do this, drag the file from the Solution Explorer onto your web page.
Figure 7-1 shows the user control after it has been dropped onto a host web page.
After you have placed the user control onto a web page, open the page in a browser to see the fully rendered web page.
User controls participate fully in the page-rendering life cycle, and controls contained within a user control behave just as they would if placed onto a standard ASP.NET web page. This means that the user control has its own page events (such as Init, Load, and Prerender) that execute as the page is processed, and that controls within the user control will also fire events as they normally would. Listing 7-3 shows how to use the user control’s Page_Load event to populate the label and to handle a button’s Click event.
LISTING 7-3: Creating control events in a user control
VB
<%@ Control Language="VB" ClassName="Listing07_03" %>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object,
ByVal e As System.EventArgs)
Me.Label1.Text = "The quick brown fox jumped over the lazy dog"
End Sub
Protected Sub Button1_Click(ByVal sender As Object,
ByVal e As System.EventArgs)
Me.Label1.Text =
"The quick brown fox clicked the button on the page"
End Sub
</script>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
<asp:Button ID="Button1" runat="server" Text="Button" />
C#
<%@ Control Language="C#" ClassName="Listing07_03" %>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
this.Label1.Text = "The quick brown fox jumped over the lazy dog";
}
protected void Button1_Click(object sender, EventArgs e)
{
this.Label1.Text = "The quick brown fox clicked the button on the page";
}
</script>
When you render the web page, you see that the text of the label changes as the user control loads, and again when you click the bottom of the page. In fact, if you put a breakpoint on either of these two events, you can see that ASP.NET does indeed break inside the user control code when the page is executed.
So far, you have learned how you can create user controls and add them to a web page. You have also learned how user controls can execute their own code and fire events. Most user controls, however, are not isolated islands within their parent page. Often, the host web page needs to interact with user controls that have been placed on it. For instance, you may decide that the text you want to load in the label must be given to the user control by the host page. To do this, you simply add a public property to the user control, and then assign text using the property. Listing 7-4 shows the modified user control.
LISTING 7-4: Exposing user control properties
VB
<%@ Control Language="VB" ClassName="Listing07_04" %>
<script runat="server">
Public Property Text() As String
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Me.Label1.Text = Me.Text
End Sub
</script>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
C#
<%@ Control Language="C#" ClassName="Listing07_04" %>
<script runat="server">
public string Text { get; set; }
protected void Page_Load(object sender, EventArgs e)
{
this.Label1.Text = this.Text;
}
</script>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
After you modify the user control, you simply populate the property from the host web page. Listing 7-5 shows how to set the Text property in code.
LISTING 7-5: Populating user control properties from the host web page
VB
Protected Sub Page_Load(sender As Object, e As EventArgs)
Listing0704.Text = "The quick brown fox jumped over the lazy dog"
End Sub
C#
protected void Page_Load(Object sender, EventArgs e)
{
Listing0704.Text = "The quick brown fox jumped over the lazy dog";
}
Note that public properties exposed by user controls are also exposed by the Property Browser, so you can set a user control’s properties using it as well.
User controls are simple ways of creating powerful, reusable components in ASP.NET. They are easy to create using the built-in templates. Because they participate fully in the page life cycle, you can create controls that can interact with their host page and even other controls on the host page.
You can also create and add user controls to the Web Form dynamically at run time. The ASP.NET Page object includes the LoadControl method, which enables you to load user controls at run time by providing the method with a virtual path to the user control you want to load. The method returns a generic Control object that you can then add to the page’s Controls collection. Listing 7-6 demonstrates how you can use the LoadControl method to dynamically add a user control to a web page.
LISTING 7-6: Dynamically adding a user control
VB
<%@ Page Language="VB" %>
<!DOCTYPE html>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Dim myForm As Control = Page.FindControl("Form1")
Dim c1 As Control = LoadControl("Listing07-04.ascx")
myForm.Controls.Add(c1)
End Sub
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Dynamically adding a user control</title>
</head>
<body>
<form id="form1" runat="server">
</form>
</body>
</html>
C#
<%@ Page Language="C#" %>
<!DOCTYPE html>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
Control myForm = Page.FindControl("Form1");
Control c1 = LoadControl("Listing07-04.ascx");
myForm.Controls.Add(c1);
}
</script>
The first step in adding a user control to the page is to locate the page’s Form control using the FindControl method. Should the user control contain ASP.NET controls that render form elements such as a button or textbox, you must add the user control to the form element’s Controls collection.
After the form has been found, the sample uses the page’s LoadControl() method to load an instance of the user control. The method accepts a virtual path to the user control you want to load and returns the loaded user control as a generic Control object.
Finally, you add the control to the Form object’s Controls collection. You can also add the user control to other container controls that may be present on the web page, such as a Panel or Placeholder control.
After you have the user control loaded, you can also work with its object model, just as you can with any other control. To access properties and methods that the user control exposes, you need to cast the control from the generic control type to its actual type. To do that, you also need to add the @Reference directive to the page. This tells ASP.NET to compile the user control and link it to the ASP.NET page so that the page knows where to find the user control type. Listing 7-7 demonstrates how you can access a custom property of your user control by casting the control after loading it. The sample loads a modified user control that hosts an ASP.NET TextBox control and exposes a public property that enables you to access the TextBox control’s Text property.
LISTING 7-7: Casting a user control to its native type
VB
<%@ Page Language="VB" %>
<!DOCTYPE html>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Dim myForm As Control = Page.FindControl("Form1")
Dim c1 As Listing07_04 = CType(LoadControl("Listing07-04.ascx"), Listing07_04)
myForm.Controls.Add(c1)
c1.ID = "Listing07_04"
c1.Text = "Text about our custom user control."
End Sub
</script>
C#
<%@ Page Language="C#" %>
<!DOCTYPE html>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
Control myForm = Page.FindControl("Form1");
Listing07_04 c1 = (Listing07_04)LoadControl("Listing07-04.ascx");
myForm.Controls.Add(c1);
c1.ID = "Listing07_04";
c1.Text = "Text about our custom user control.";
}
</script>
Notice that the sample adds the control to the Form’s Controls collection and then sets the Text property. The ordering of this is important because after a page postback occurs the control’s ViewState is not calculated until the control is added to the Controls collection. Therefore, if you set the Text value (or any other property of the user control) before the control’s ViewState is calculated, the value is not persisted in the ViewState.
One additional twist to adding user controls dynamically occurs when you are using output caching to cache the user controls. In this case, after the control has been cached, the LoadControl method does not return a new instance of the control. Instead, it returns the cached copy of the control. This presents problems when you try to cast the control to its native type because, after the control is cached, the LoadControl method returns it as a PartialCachingControl object rather than as its native type. Therefore, the cast in the previous sample results in an exception being thrown.
To solve this problem, you simply test the object type before attempting the cast. This is shown in Listing 7-8.
LISTING 7-8: Detecting cached user controls
VB
<%@ Page Language="VB" %>
<!DOCTYPE html>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Dim myForm As Control = Page.FindControl("Form1")
Dim c1 As Control = LoadControl("Listing07-04.ascx")
myForm.Controls.Add(c1)
If c1.GetType() Is GetType(Listing07_04) Then
CType(c1, Listing07_04).ID = "Listing07_04"
CType(c1, Listing07_04).Text = "Text about our custom user control (not cached)"
ElseIf c1.GetType() Is GetType(PartialCachingControl) _
And Not (IsNothing(CType(c1, PartialCachingControl).CachedControl)) Then
Dim listingControl As Listing07_04 = _
CType(CType(c1, PartialCachingControl).CachedControl, Listing07_04)
listingControl.ID = "Listing07_04"
listingControl.Text = "Text about our custom user control (partially cached)"
End If
End Sub
</script>
C#
<%@ Page Language="C#" %>
<!DOCTYPE html>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
Control myForm = Page.FindControl("Form1");
Control c1 = LoadControl("Listing07-04.ascx");
myForm.Controls.Add(c1);
if (c1 is Listing07_04)
{
((Listing07_04)c1).ID = "Listing07_04";
((Listing07_04)c1).Text = "Text about our custom user control (not cached)";
}
else if ((c1 is PartialCachingControl) &&
((PartialCachingControl)c1).CachedControl != null)
{
Listing07_04 listingControl =
((Listing07_04)((PartialCachingControl)c1).CachedControl);
listingControl.ID = "Listing07_04";
listingControl.Text = "Text about our custom user control (partially cached)";
}
}
</script>
The sample demonstrates how you can test to see what type the LoadControl returns and set properties based on the type. For more information on caching, check out Chapter 22.
Finally, in the previous samples user controls have been added dynamically during the Page_Load event. But there may be times when you want to add the control in a different event, such as a Button’s Click event or the SelectedIndexChanged event of a DropDownList control. Using these events to add user controls dynamically presents new challenges. Because these events may not be raised each time a page postback occurs, you need to create a way to track when a user control has been added so that it can be re-added to the web page as additional postbacks occur.
A simple way to do this is to use the ASP.NET session to track when the user control is added to the web page. Listing 7-9 demonstrates this.
LISTING 7-9: Tracking added user controls across postbacks
VB
<%@ Page Language="VB" %>
<!DOCTYPE html>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
If IsNothing(Session("Listing07-04")) Or
Not (CBool(Session("Listing07-04"))) Then
Dim myForm As Control = Page.FindControl("Form1")
Dim c1 As Control = LoadControl("Listing07-04.ascx")
CType(c1, Listing07_04).Text = "Loaded after first page load"
myForm.Controls.Add(c1)
Session("Listing07-04") = True
End If
End Sub
Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs)
If Not IsNothing(Session("Listing07-04")) And
(CBool(Session("Listing07-04"))) Then
Dim myForm As Control = Page.FindControl("Form1")
Dim c1 As Control = LoadControl("Listing07-04.ascx")
CType(c1, Listing07_04).Text = "Loaded after a postback"
myForm.Controls.Add(c1)
End If
End Sub
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Button ID="Button1" runat="server" Text="Load Control"
OnClick="Button1_Click" />
</div>
</form>
</body>
</html>
C#
<%@ Page Language="C#" %>
<!DOCTYPE html>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
if ((Session["Listing07-04"] == null) ||
(!(bool)Session["Listing07-04"]))
{
Control myForm = Page.FindControl("Form1");
Control c1 = LoadControl("Listing07-04.ascx");
((Listing07_04)c1).Text = "Loaded after first page load";
myForm.Controls.Add(c1);
Session["Listing07-04"] = true;
}
}
protected void Button1_Click(object sender, EventArgs e)
{
if ((Session["Listing07-04"] != null) &&
((bool)Session["Listing07-04"]))
{
Control myForm = Page.FindControl("Form1");
Control c1 = LoadControl("Listing07-04.ascx");
((Listing07_04)c1).Text = "Loaded after a postback";
myForm.Controls.Add(c1);
}
}
</script>
This sample uses a simple Session variable to track whether the user control has been added to the page. When the Button1 Click event fires, the session variable is set to True, indicating that the user control has been added. Then, each time the page performs a postback, the Page_Load event checks to see whether the session variable is set to True, and if so, it re-adds the control to the page.
The power to create server controls in ASP.NET is one of the greatest tools you can have as an ASP.NET developer. Creating your own custom server controls and extending existing controls are actually both quite easy. In ASP.NET, all controls are derived from two basic classes: System.Web.UI.WebControls .WebControl or System.Web.UI.ScriptControl. Classes derived from the WebControl class have the basic functionality required to participate in the Page framework. These classes include most of the common functionality needed to create controls that render a visual HTML representation and provide support for many of the basic styling elements such as Font, Height, and Width. Because the WebControl class derives from the Control class, the controls derived from it have the basic functionality to be designable controls, meaning you can add them to the Visual Studio Toolbox, drag them onto the page designer, and display their properties and events in the Property Browser.
Controls derived from the ScriptControl class build on the functionality that the WebControl class provides by including additional features designed to make working with client-side script libraries easier. The class tests to ensure that a ScriptManager control is present in the hosting page during the control’s PreRender stage, and also ensures that derived controls call the proper ScriptManager methods during the Render event.
To make creating a custom server control easy, Visual Studio provides two different project templates that set up a basic project structure including the files you need to create a server control. Figure 7-2 shows the ASP.NET Server Control and ASP.NET AJAX Server Control projects in Visual Studio’s New Project dialog box.
The ASP.NET Server Control project creates a basic class library project with a single server control class included by default that derives from WebControl. The ASP.NET AJAX Server Control project also creates a basic class library project, but includes a single server control class derived from ScriptControl and a Resource file and a JavaScript file. Creating either of these project types results in a runnable, though essentially functionless, server control.
You can add additional server control classes to the project by selecting the ASP.NET Server Control file template from the Add New Item dialog box. Note that this template differs slightly from the default template included in the server control projects. It uses a different filename scheme and includes slightly different code in the default control template.
After you’ve created a new project, you can test the control by adding a new Web Project to the existing solution, rebuilding the entire solution, and opening the default web page. Visual Studio automatically adds the server control to the Toolbox as shown in Figure 7-3.
Visual Studio does this for any controls contained in projects in the currently open solution.
Now simply drag the control onto the Web Form. Visual Studio adds a reference to the control to the project, and the control is added to the web page. Listing 7-10 shows you what the web page source code looks like after you have added the control.
LISTING 7-10: Adding a web control library to a web page
<%@ Page Language="C#" %>
<%@ Register Assembly="CSharpServerControl1" Namespace="CSharpServerControl1"
TagPrefix="cc1" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Adding a Custom Web Control</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<cc1:ServerControl1 ID="ServerControl1" runat="server" />
</div>
</form>
</body>
</html>
After you drag the control onto the Web Form, take a look at its properties in the Properties window. Figure 7-4 shows the properties of your custom control.
Notice that the control has all the basic properties of a visual control, including various styling and behavior properties. The properties are exposed because the control was derived from the WebControl class. The control also inherits the base events exposed by WebControl.
Make sure the control is working by entering a value for the Text property and viewing the page in a browser. Figure 7-5 shows what the page looks like if you set the Text property to "Hello World!".
As expected, the control has rendered the value of the Text property to the web page.
Now that you have a basic server control project up and running, you can go back and take a look at the template class created for you by the ASP.NET Server Control project. The default template is shown in Listing 7-11 (ServerControl1.vb and ServerControl1.cs in the code download for this chapter).
LISTING 7-11: The Visual Studio ASP.NET Server Control class template
VB
Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
<DefaultProperty("Text"), ToolboxData("<{0}:ServerControl1 runat=server>
</{0}:ServerControl1>")>
Public Class ServerControl1
Inherits WebControl
<Bindable(True), Category("Appearance"), DefaultValue(""), Localizable(True)>
Property Text() As String
Get
Dim s As String = CStr(ViewState("Text"))
If s Is Nothing Then
Return "[" & Me.ID & "]"
Else
Return s
End If
End Get
Set(ByVal Value As String)
ViewState("Text") = Value
End Set
End Property
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
output.Write(Text)
End Sub
End Class
C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace CSharpServerControl1
{
[DefaultProperty("Text")]
[ToolboxData("<{0}:ServerControl1 runat=server></{0}:ServerControl1>")]
public class ServerControl1 : WebControl
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null)? "[" + this.ID + "]" : s);
}
set
{
ViewState["Text"] = value;
}
}
protected override void RenderContents(HtmlTextWriter output)
{
output.Write(Text);
}
}
}
You can see a number of interesting things about the default server control template generated by the project. First, notice that both the class declaration and the Text property are decorated by attributes. ASP.NET server controls make heavy use of attributes to indicate different types of runtime and design-time behaviors. You learn more about the attributes you can apply to server control classes and properties later in this chapter.
Second, by default the template includes a single property called Text and a simple overridden method called RenderContents that renders the value of that property to the screen. The RenderContents method of the control is the primary method used to output content from the server control.
If you view the HTML source of the previous sample, you will see that not only has ASP.NET added the value of the Text property to the HTML markup, but it has surrounded the text with a <SPAN> block. If you look at the code for the WebControl class’s render method, you can see that, in addition to calling the RenderContents method, it also includes calls to render a begin and end tag by inserting the Span tag around the control’s content.
protected internal override void Render(HtmlTextWriter writer)
{
this.RenderBeginTag(writer);
this.RenderContents(writer);
this.RenderEndTag(writer);
}
If you have provided an ID value for your control, the Span tag also, by default, renders an ID attribute. Having the Span tags can sometimes be problematic, so if you want to prevent ASP.NET from automatically adding the Span tags you can override the Render method in your control and call the RenderContents method directly:
VB
Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
Me.RenderContents(writer)
End Sub
C#
protected override void Render(HtmlTextWriter writer)
{
this.RenderContents(writer);
}
The default server control template does a good job of demonstrating how easy it is to create a simple server control, but of course, this control does not have much functionality and lacks many of the features you might find in a typical server control. The rest of this chapter focuses on how you can use different features of the .NET Framework to add additional runtime and design-time features to a server control.
Much of the design-time experience a server control offers developers is configured by adding attributes to the server control class and properties. For example, when you look at the default control template from the previous section (Listing 7-11), notice that attributes have been applied to both the class and to the Text property. This section describes the attributes that you can apply to server controls and how they affect the behavior of the control.
Class attributes for server controls can be divided into three general categories: global control runtime behaviors, how the control looks in the Visual Studio Toolbox, and how it behaves when placed on the design surface. Table 7-1 describes some of these attributes.
ATTRIBUTE | DESCRIPTION |
Designer | Indicates the designer class this control should use to render a design-time view of the control on the Visual Studio design surface |
TypeConverter | Specifies what type to use as a converter for the object |
DefaultEvent | Indicates the default event created when the user double-clicks the control on the Visual Studio design surface |
DefaultProperty | Indicates the default property for the control |
ControlBuilder | Specifies a ControlBuilder class for building a custom control in the ASP.NET control parser |
ParseChildren | Indicates whether XML elements nested within the server control’s tags are treated as properties or as child controls |
TagPrefix | Indicates the text that prefixes the control in the web page HTML |
You use property attributes to control a number of different aspects of server controls, including how your properties and events behave in the Visual Studio Property Browser and how properties and events are serialized at design time. Table 7-2 describes some of the property and event attributes you can use.
ATTRIBUTE | DESCRIPTION |
Bindable | Indicates that the property can be bound to a data source |
Browsable | Indicates whether the property should be displayed at design time in the Property Browser |
Category | Indicates the category this property should be displayed under in the Property Browser |
Description | Displays a text string at the bottom of the Property Browser that describes the purpose of the property |
EditorBrowsable | Indicates whether the property should be editable when shown in the Property Browser |
DefaultValue | Indicates the default value of the property shown in the Property Browser |
DesignerSerializationVisibility | Specifies the visibility a property has to the design-time serializer |
NotifyParentProperty | Indicates that the parent property is notified when the value of the property is modified |
PersistChildren | Indicates whether, at design time, the child controls of a server control should be persisted as nested inner controls |
PersistanceMode | Specifies how a property or an event is persisted to the ASP.NET page |
TemplateContainer | Specifies the type of INamingContainer that will contain the template after it is created |
Editor | Indicates the UI Type Editor class this control should use to edit its value |
Localizable | Indicates that the property contains text that can be localized |
Themable | Indicates whether this property can have a theme applied to it |
Obviously, the class and property/event attribute tables present a lot of information up front. You already saw a demonstration of some of these attributes in Listing 7-11; as you go through the rest of this chapter, the samples leverage other attributes listed in the tables.
So far in this chapter you have seen how easy it is to develop a very basic server control using the Visual Studio project templates and how you can apply attributes in the server control to influence some basic control behaviors. The rest of the chapter focuses on how you can use features of ASP.NET to add more advanced control capabilities to your server controls.
Before digging deeper into server controls, spending a moment to look at the general ASP.NET page life cycle that server controls operate within is helpful. As the control developer, you are responsible for overriding methods that execute during the life cycle and implementing your own custom rendering logic.
Remember that when a web browser makes a request to the server, it is using HTTP, a stateless protocol. ASP.NET provides a page-execution framework that helps create the illusion of state in a web application. This framework is basically a series of methods and events that execute every time an ASP.NET page is processed. Figure 7-6 shows the events and methods called during the control’s life cycle.
As you read through the rest of this chapter, you will see that a server control uses many of these events and methods. Understanding them and the order in which they execute is helpful so that as you are adding features to your server control, you can structure your logic to follow the page life cycle.
The main job of a server control is to render some type of markup language to the HTTP output stream, which is returned to and displayed by the client. If your client is a standard browser, the control should emit HTML; if the client is something like a mobile device, the control may need to emit a different type of markup. As stated earlier, your responsibility as the control developer is to tell the server control what markup to render. The overridden RenderContents method, called during the control’s life cycle, is the primary location where you tell the control what you want to emit to the client. In Listing 7-12, notice that the RenderContents method is used to tell the control to print the value of the Text property (see ServerControl1.vb and ServerControl1.cs in the code download for this chapter).
LISTING 7-12: Overriding the Render method
VB
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
output.Write(Text)
End Sub
C#
protected override void RenderContents(HtmlTextWriter output)
{
output.Write(Text);
}
Also notice that the RenderContents method has one method parameter called output. This parameter is an HtmlTextWriter class, which is what the control uses to render HTML to the client. This special writer class is specifically designed to emit HTML 4.0–compliant HTML to the browser.
The HtmlTextwriter class has a number of methods you can use to emit your HTML, including RenderBeginTag and WriteBeginTag. Listing 7-13 shows how you can modify the control’s Render method to emit an HTML <input> tag.
LISTING 7-13: Using the HtmlTextWriter to render an HTML tag
VB
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
output.RenderBeginTag(HtmlTextWriterTag.Input)
output.RenderEndTag()
End Sub
C#
protected override void RenderContents(HtmlTextWriter output)
{
output.RenderBeginTag(HtmlTextWriterTag.Input);
output.RenderEndTag();
}
First, notice that the RenderBeginTag method is used to emit the HTML. The advantage of using this method to emit HTML is that it requires you to select a tag from the HtmlTextWriterTag enumeration. Using the RenderBeginTag method and the HtmlTextWriterTag enumeration enables you to have your control automatically support down-level browsers that cannot understand HTML 4.0 or later syntax.
Second, notice that the RenderEndTag method is also used. As the name suggests, this method renders the closing tag. Notice, however, that you do not have to specify in this method which tag you want to close. RenderEndTag automatically closes the last begin tag rendered by the RenderBeginTag method, which in this case is the <input> tag. If you want to emit multiple HTML tags, make sure you order your Begin and End render methods properly. Listing 7-14, for example, adds a <div> tag to the control. The <div> tag surrounds the <input> tag when rendered to the page.
LISTING 7-14: Using the HtmlTextWriter to render multiple HTML tags
VB
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
output.RenderBeginTag(HtmlTextWriterTag.Div)
output.RenderBeginTag(HtmlTextWriterTag.Input)
output.RenderEndTag()
output.RenderEndTag()
End Sub
C#
protected override void RenderContents(HtmlTextWriter output)
{
output.RenderBeginTag(HtmlTextWriterTag.Div);
output.RenderBeginTag(HtmlTextWriterTag.Input);
output.RenderEndTag();
output.RenderEndTag();
}
Now that you have a basic understanding of how to emit simple HTML, look at the output of your control. Figure 7-7 shows the source for the page.
You can see that the control emitted some simple HTML markup. Also notice that the control was smart enough to realize that the input control did not contain any child controls and, therefore, the control did not need to render a full closing tag. Instead, it automatically rendered the shorthand />, rather than </input>.
Emitting HTML tags is a good start to building the control, but perhaps this is a bit simplistic. Normally, when rendering HTML you would emit some tag attributes (such as ID or Name) to the client in addition to the tag. Listing 7-15 shows how you can easily add tag attributes.
LISTING 7-15: Rendering HTML tag attributes
VB
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
output.RenderBeginTag(HtmlTextWriterTag.Div)
output.AddAttribute(HtmlTextWriterAttribute.Type, "text")
output.AddAttribute(HtmlTextWriterAttribute.Id, Me.ClientID & "_i")
output.AddAttribute(HtmlTextWriterAttribute.Name,
Me.ClientID & "_i")
output.AddAttribute(HtmlTextWriterAttribute.Value, Me.Text)
output.RenderBeginTag(HtmlTextWriterTag.Input)
output.RenderEndTag()
output.RenderEndTag()
End Sub
C#
protected override void RenderContents(HtmlTextWriter output)
{
output.RenderBeginTag(HtmlTextWriterTag.Div);
output.AddAttribute(HtmlTextWriterAttribute.Type, "text");
output.AddAttribute(HtmlTextWriterAttribute.Id,
this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Name,
this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
output.RenderBeginTag(HtmlTextWriterTag.Input);
output.RenderEndTag();
output.RenderEndTag();
}
You can see that by using the AddAttribute method, you have added three attributes to the <input> tag. Also notice that, once again, you are using an enumeration, HtmlTextWriterAttribute, to select the attribute you want to add to the tag. This serves the same purpose as using the HtmlTextWriterTag enumeration, allowing the control to degrade its output to down-level browsers.
As with the Render methods, the order in which you place the AddAttributes methods is important. You place the AddAttributes methods directly before the RenderBeginTag method in the code. The AddAttributes method associates the attributes with the next HTML tag that is rendered by the RenderBeginTag method — in this case, the <input> tag.
Now browse to the test page and check out the HTML source with the added tag attributes. Figure 7-8 shows the HTML source rendered by the control.
You can see that the tag attributes you added in the server control are now included as part of the HTML tag rendered by the control.
Notice that in Listing 7-15, using the control’s ClientID property as the value of both the Id and Name attributes is important. Controls that derive from the WebControl class automatically expose three different types of ID properties: ID, UniqueID, and ClientID. Each of these properties exposes a slightly altered version of the control’s ID for use in a specific scenario.
The ID property is the most obvious. Developers use it to get and set the control’s ID. It must be unique to the page at design time.
The UniqueID property is a read-only property generated at run time that returns an ID that has been prepended with the containing control’s ID. This is essential so that ASP.NET can uniquely identify each control in the page’s control tree, even if the control is used multiple times by a container control such as a Repeater or GridView. For example, if you add this custom control to a Repeater, the UniqueID for each custom control rendered by the Repeater is modified to include the Repeater’s ID when the page is executed:
MyRepeater:Ctrl0:MyCustomControl
Beginning with ASP.NET 4.0, the ClientID property can be generated differently depending on the value of the ClientIDMode property. The ClientIDMode property enables you to select one of four mechanisms that ASP.NET uses to generate the ClientID:
Additionally, to ensure that controls can generate a unique ID, they should implement the INamingContainer interface. This is a marker interface only, meaning that it does not require any additional methods to be implemented; it does, however, ensure that the ASP.NET run time guarantees that the control always has a unique name within the page’s tree hierarchy, regardless of its container.
So far, you have seen how easy it is to build a simple server control and emit the proper HTML, including attributes. However, modern web development techniques generally restrict the use of HTML to a basic content description mechanism, relying instead on CSS for the positioning and styling of HTML elements in a web page. In this section, you learn how you can have your control render style information.
As mentioned at the very beginning of this section, you are creating controls that inherit from the WebControl class. Because of this, these controls already have the basic infrastructure for emitting most of the standard CSS-style attributes. In the Property Browser for this control, you should see a number of style properties already listed, such as background color, border width, and font. You can also launch the style builder to create complex CSS styles. These basic properties are provided by the WebControl class, but it is up to you to tell your control to render the values set at design time. To do this, you simply execute the AddAttributesToRender method. Listing 7-16 shows you how to do this.
LISTING 7-16: Rendering style properties
VB
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
output.RenderBeginTag(HtmlTextWriterTag.Div)
output.AddAttribute(HtmlTextWriterAttribute.Type, "text")
output.AddAttribute(HtmlTextWriterAttribute.Id, Me.ClientID & "_i")
output.AddAttribute(HtmlTextWriterAttribute.Name,
Me.ClientID & "_i")
output.AddAttribute(HtmlTextWriterAttribute.Value, Me.Text)
Me.AddAttributesToRender(output)
output.RenderBeginTag(HtmlTextWriterTag.Input)
output.RenderEndTag()
output.RenderEndTag()
End Sub
C#
protected override void RenderContents(HtmlTextWriter output)
{
output.RenderBeginTag(HtmlTextWriterTag.Div);
output.AddAttribute(HtmlTextWriterAttribute.Type, "text");
output.AddAttribute(HtmlTextWriterAttribute.Id,
this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Name,
this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
this.AddAttributesToRender(output);
output.RenderBeginTag(HtmlTextWriterTag.Input);
output.RenderEndTag();
output.RenderEndTag();
}
Executing this method tells the control to render any style information that has been set. It not only causes the style-related properties to be rendered, but also several other attributes, including ID, tabindex, and tooltip. If you are manually rendering these attributes earlier in your control, you may end up with duplicate attributes being rendered.
Additionally, being careful about where you execute the AddAttributesToRender method is important. In Listing 7-16, it is executed immediately before the Input tag is rendered, which means that the attributes are rendered both on the Input element and on the Span element surrounding the Input element. Placing the method call before the beginning Div tag is rendered ensures that the attributes are now applied to the Div and its surrounding span. Placing the method call after the end Div means the attribute is applied only to the span.
Using the Property Browser, you can set the background color of the control to Silver and the font to Bold. When you set these properties, they are automatically added to the control tag in the ASP.NET page. After you have added the styles, the control tag looks like this:
<cc1:Listing0716 ID="Listing07161" runat="server" BackColor="Silver"
Font-Bold="True" Text="Hello World!" />
The style changes have been persisted to the control as attributes. When you execute this page in the browser, the style information should be rendered to the HTML, making the background of the textbox silver and its font bold. Figure 7-9 shows the page in the browser.
Once again, look at the source for this page. The style information has been rendered to the HTML as a style tag. Figure 7-10 shows the HTML emitted by the control.
If you want more control over the rendering of styles in your control you can use the HtmlTextWriter’s AddStyleAttribute method. Similar to the AddAttribute method, the AddStyleAttribute method enables you to specify CSS attributes to add to a control using the HtmlTextWriterStyle enumeration. However, unlike the AddAttribute method, attributes you add using AddStyleAttribute are defined inside of a style attribute on the control. Listing 7-17 demonstrates the use of the AddStyleAttribute method.
LISTING 7-17: Adding control styles using AddStyleAttribute
VB
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
output.RenderBeginTag(HtmlTextWriterTag.Div)
output.AddAttribute(HtmlTextWriterAttribute.Type, "text")
output.AddAttribute(HtmlTextWriterAttribute.Id, Me.ClientID & "_i")
output.AddAttribute(HtmlTextWriterAttribute.Name,
Me.ClientID & "_i")
output.AddAttribute(HtmlTextWriterAttribute.Value, Me.Text)
output.AddStyleAttribute(HtmlTextWriterStyle.BackgroundColor, "Silver")
output.RenderBeginTag(HtmlTextWriterTag.Input)
output.RenderEndTag()
output.RenderEndTag()
End Sub
C#
protected override void RenderContents(HtmlTextWriter output)
{
output.RenderBeginTag(HtmlTextWriterTag.Div);
output.AddAttribute(HtmlTextWriterAttribute.Type, "text");
output.AddAttribute(HtmlTextWriterAttribute.Id,
this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Name,
this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
output.AddStyleAttribute(HtmlTextWriterStyle.BackgroundColor, "Silver");
output.RenderBeginTag(HtmlTextWriterTag.Input);
output.RenderEndTag();
output.RenderEndTag();
}
Running this sample results in a silver background color being applied to the control.
Although the capability to render and style HTML is quite powerful by itself, you can send other resources to the client, such as client-side scripts, images, and resource strings. ASP.NET provides you with some powerful tools for using client-side scripts in your server controls and exposing other resources to the client along with the HTML your control emits. Additionally, ASP.NET includes an entire model that enables you to make asynchronous callbacks from your web page to the server.
Having your control emit client-side script like JavaScript enables you to add powerful client-side functionality to your control. Client-side scripting languages take advantage of the client’s browser to create more flexible and easy-to-use controls. ASP.NET provides a wide variety of methods for emitting client-side script that you can use to control where and how your script is rendered.
Most of the properties and methods needed to render client script are available from the ClientScriptManager class, which you can access using Page.ClientScript. Listing 7-18 demonstrates how you can use the RegisterStartupScript method to render JavaScript to the client. This listing adds the code into the OnPreRender method, rather than into the Render method used in previous samples. This method allows every control to inform the page about the client-side script it needs to render. After the Render method is called, the page is able to render all the client-side script it collected during the OnPreRender method. If you call the client-side script registration methods in the Render method, the page has already completed a portion of its rendering before your client-side script can render itself.
LISTING 7-18: Rendering a client-side script to the browser
VB
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)
Page.ClientScript.RegisterStartupScript(GetType(Page),
"ControlFocus", "document.getElementById('" &
Me.ClientID & "_i" & "').focus();", True)
End Sub
C#
protected override void OnPreRender(EventArgs e)
{
Page.ClientScript.RegisterStartupScript(typeof(Page),
"ControlFocus", "document.getElementById('" +
this.ClientID + "_i" + "').focus();", true);
}
In this listing, the code emits client-side script to automatically move the control focus to the TextBox control when the web page loads. When you use the RegisterStartupScript method, notice that it now includes an overload that lets you specify whether the method should render surrounding script tags. This can be handy if you are rendering more than one script to the page.
Also notice that the method requires a key parameter. This parameter is used to uniquely identify the script block; if you are registering more than one script block in the web page, make sure that each block is supplied a unique key. You can use the IsStartupScriptRegistered method and the key to determine whether a particular script block has been previously registered on the client using the RegisterStartupScript method.
When you execute the page in the browser, notice that the focus is automatically placed into a textbox. If you look at the source code for the web page, you should see that the JavaScript was written to the bottom of the page, as shown in Figure 7-11.
If you want the script to be rendered to the top of the page, you use the RegisterClientScriptBlock method that emits the script block immediately after the opening <form> element.
Keep in mind that the browser parses the web page from top to bottom, so if you emit client-side script at the top of the page that is not contained in a function, any references in that code to HTML elements further down the page will fail. The browser has not parsed that portion of the page yet.
Being able to render script that executes automatically when the page loads is nice, but it is more likely that you will want the code to execute based on an event fired from an HTML element on your page, such as the Click, Focus, or Blur events. To do this, you add an attribute to the HTML element you want the event to fire from. Listing 7-19 shows how you can modify your control’s Render and PreRender methods to add this attribute.
LISTING 7-19: Using client-side script and event attributes to validate data
VB
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
output.RenderBeginTag(HtmlTextWriterTag.Div)
output.AddAttribute(HtmlTextWriterAttribute.Type, "text")
output.AddAttribute(HtmlTextWriterAttribute.Id, Me.ClientID & "_i")
output.AddAttribute(HtmlTextWriterAttribute.Name,
Me.ClientID & "_i")
output.AddAttribute(HtmlTextWriterAttribute.Value, Me.Text)
output.AddAttribute("OnBlur", "ValidateText(this)")
output.RenderBeginTag(HtmlTextWriterTag.Input)
output.RenderEndTag()
output.RenderEndTag()
End Sub
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)
Page.ClientScript.RegisterStartupScript(GetType(Page),
"ControlFocus", "document.getElementById('" & Me.ClientID &
"_i" & "').focus();", True)
Page.ClientScript.RegisterClientScriptBlock(
GetType(Page),
"ValidateControl",
"function ValidateText() {" &
"if (ctl.value=='') {" &
"alert('Please enter a value.');ctl.focus(); }" &
"}", True)
End Sub
C#
protected override void RenderContents(HtmlTextWriter output)
{
output.RenderBeginTag(HtmlTextWriterTag.Div);
output.AddAttribute(HtmlTextWriterAttribute.Type, "text");
output.AddAttribute(HtmlTextWriterAttribute.Id,
this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Name,
this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
output.AddAttribute("OnBlur", "ValidateText(this)");
output.RenderBeginTag(HtmlTextWriterTag.Input);
output.RenderEndTag();
output.RenderEndTag();
}
protected override void OnPreRender(EventArgs e)
{
Page.ClientScript.RegisterStartupScript(
typeof(Page),
"ControlFocus", "document.getElementById('" +
this.ClientID + "_i" + "').focus();", true);
Page.ClientScript.RegisterClientScriptBlock(
typeof(Page),
"ValidateControl",
"function ValidateText(ctl) {" +
"if (ctl.value=='') {" +
"alert('Please enter a value.'); ctl.focus(); }" +
"}", true);
}
As you can see, the TextBox control is modified to check for an empty string. An attribute that adds the JavaScript OnBlur event to the textbox is also included. The OnBlur event fires when the control loses focus. When this happens, the client-side ValidateText method is executed, which is rendered to the client using RegisterClientScriptBlock.
The rendered HTML is shown in Figure 7-12.
Embedding JavaScript in the page is powerful, but if you are writing large amounts of client-side code, you might want to consider storing the JavaScript in an external file. You can include this file in your HTML by using the RegisterClientScriptInclude method. This method renders a script tag using the URL you provide to it as the value of its src element:
<script src="[url]" type="text/javascript"></script>
Listing 7-20 shows how you can modify the validation added to the input element in Listing 7-18 to store the JavaScript validation function in an external file.
LISTING 7-20: Adding client-side script include files to a web page
VB
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)
Page.ClientScript.RegisterClientScriptInclude(
"UtilityFunctions", "Listing07-21.js")
Page.ClientScript.RegisterStartupScript(GetType(Page),
"ControlFocus", "document.getElementById('" &
Me.ClientID & "_i" & "').focus();",
True)
End Sub
C#
protected override void OnPreRender(EventArgs e)
{
Page.ClientScript.RegisterClientScriptInclude(
"UtilityFunctions", "Listing07-21.js");
Page.ClientScript.RegisterStartupScript(
typeof(Page),
"ControlFocus", "document.getElementById('" +
this.ClientID + "_i" + "').focus();",
true);
}
You have modified the OnPreRender event to register a client-side script file include, which contains the ValidateText function. You need to add a JavaScript file to the project and create the ValidateText function, as shown in Listing 7-21.
LISTING 7-21: The validation JavaScript
function ValidateText(ctl)
{
if (ctl.value=='') {
alert('Please enter a value.');
ctl.focus();
}
}
The ClientScriptManager also provides methods for registering hidden HTML fields and adding script functions to the OnSubmit event.
A great way to distribute application resources like JavaScript files, images, or resource files is to embed them directly into the compiled assembly. ASP.NET makes this easy by using the RegisterClientScriptResource method, which is part of the ClientScriptManager.
This method makes it possible for your web pages to retrieve stored resources — like JavaScript files — from the compiled assembly at run time. It works by using an HttpHandler to retrieve the requested resource from the assembly and return it to the client. The RegisterClientScriptResource method emits a <script> block whose src value points to this HttpHandler (Note that in the code download MiscEmbeddedScript.cs, AssemblyInfo.cs, and MiscEmbeddedScript.aspx are used to generate this):
<script
src="/WebResource.axd?d=ktqObNC8c8uwwm_pAVTdak1ofzyXH33vOZkC2Pqa7gWUvT7XCNmAeT5Jig-FTrYk2SaMtNk3sR42AEFFZgrf7vQ_knZp3JtuIRq9xHCIDZBhHD0zyFRsRo-3AU0VeOyQ0rJ15SnnA-9ScC-bwU0-PQ2&t=634880142370628393" type="text/javascript"></script>
As you can see, the WebResource.axd handler is used to return the resource — in this case, the JavaScript file. You can use this method to retrieve any resource stored in the assembly, such as images or localized content strings from resource files.
Finally, ASP.NET also includes a convenient mechanism for enabling basic Ajax behavior, or client-side callbacks, in a server control. Client-side callbacks enable you to take advantage of the XmlHttp components found in most modern browsers to communicate with the server without actually performing a complete postback. Figure 7-13 shows how client-side callbacks work in the ASP.NET Framework.
To enable callbacks in your server control, you implement the System.Web.UI.ICallBackEventHander interface. This interface requires you to implement two methods: the RaiseCallbackEvent method and the GetCallbackResult method. These server-side events fire when the client executes the callback. After you implement the interface, you want to tie your client-side events back to the server. You do this by using the Page.ClientScript.GetCallbackEventReference method. This method enables you to specify the two client-side functions: one to serve as the callback handler and one to serve as an error handler. Listing 7-22 demonstrates how you can modify the TextBox control’s Render methods and add the RaiseCallbackEvent method to use callbacks to perform validation.
LISTING 7-22: Adding an asynchronous callback to validate data
VB
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)
Page.ClientScript.RegisterClientScriptInclude(
"UtilityFunctions", "Listing07-23.js")
Page.ClientScript.RegisterStartupScript(GetType(Page),
"ControlFocus", "document.getElementById('" &
Me.ClientID & "_i" & "').focus();",
True)
Page.ClientScript.RegisterStartupScript(
GetType(Page), "ClientCallback",
"function ClientCallback() {" &
"args=document.getElementById('" &
Me.ClientID & "_i" & "').value;" &
Page.ClientScript.GetCallbackEventReference(Me, "args",
"CallbackHandler", Nothing, "ErrorHandler", True) &
"}",
True)
End Sub
Public Sub RaiseCallbackEvent(ByVal eventArgument As String) _
Implements System.Web.UI.ICallbackEventHandler.RaiseCallbackEvent
Dim result As Int32
If (Not Int32.TryParse(eventArgument, result)) Then
Throw New Exception("The method or operation is not implemented.")
End If
End Sub
Public Function GetCallbackResult() As String _
Implements System.Web.UI.ICallbackEventHandler.GetCallbackResult
Return "Valid Data"
End Function
C#
protected override void RenderContents(HtmlTextWriter output)
{
output.RenderBeginTag(HtmlTextWriterTag.Div);
output.AddAttribute(HtmlTextWriterAttribute.Type, "text");
output.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Name, this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
output.AddAttribute("OnBlur", "ClientCallback();");
output.RenderBeginTag(HtmlTextWriterTag.Input);
output.RenderEndTag();
output.RenderEndTag();
}
protected override void OnPreRender(EventArgs e)
{
Page.ClientScript.RegisterClientScriptInclude(
"UtilityFunctions", "Listing07-23.js");
Page.ClientScript.RegisterStartupScript(
typeof(Page),
"ControlFocus", "document.getElementById('" + this.ClientID + "_i" + "').focus();",
true);
Page.ClientScript.RegisterStartupScript(
typeof(Page), "ClientCallback",
"function ClientCallback() {" +
"args=document.getElementById('" + this.ClientID + "_i" + "').value;" +
Page.ClientScript.GetCallbackEventReference(this, "args",
"CallbackHandler", null, "ErrorHandler", true) + "}",
true);
}
public void RaiseCallbackEvent(string eventArgument)
{
int result;
if (!Int32.TryParse(eventArgument, out result))
throw new Exception("The method or operation is not implemented.");
}
public string GetCallbackResult()
{
return "Valid Data";
}
As you can see, the OnBlur attribute has again been modified, this time by simply calling the ClientCallback method. This method is created and rendered during the PreRender event. The main purpose of this event is to populate the client-side args variable and call the client-side callback method.
The GetCallbackEventReference method is used to generate the client-side script that actually initiates the callback. The parameters passed to the method indicate which control is initiating the callback, the names of the client-side callback method, and the names of the callback method parameters. Table 7-3 provides more details on the GetCallbackEventReference arguments.
PARAMETER | DESCRIPTION |
Control | Server control that initiates the callback. |
Argument | Client-side variable used to pass arguments to the server-side event handler. |
ClientCallback | Client-side function serving as the Callback method. This method fires when the server-side processing has completed successfully. |
Context | Client-side variable that gets passed directly to the receiving client-side function. The context does not get passed to the server. |
ClientErrorCallback | Client-side function serving as the Callback error-handler method. This method fires when the server-side processing encounters an error. |
In the code, you call two client-side methods: CallbackHandler and ErrorHandler, respectively. The two method parameters are args and ctx.
In addition to the server control code changes, you add the two client-side callback methods to the JavaScript file. Listing 7-23 shows these new functions.
LISTING 7-23: The client-side callback JavaScript functions
var args;
var ctx;
function ValidateText(ctl)
{
if (ctl.value='') {
alert('Please enter a value.');
ctl.focus();
}
}
function CallbackHandler(args,ctx)
{
alert("The data is valid");
}
function ErrorHandler(args,ctx)
{
alert("Please enter a number");
}
Now, when you view your web page in the browser, as soon as the textbox loses focus, you perform a client-side callback to validate the data. The callback raises the RaiseCallbackEvent method on the server, which validates the value of the textbox that was passed to it in the eventArguments. If the value is valid, you return a string and the client-side CallbackHandler function fires. If the value is invalid, you throw an exception, which causes the client-side ErrorHandler function to execute.
So far this chapter has described many powerful features, such as styling and emitting client-side scripts that you can utilize when writing your own custom control. But if you are taking advantage of these features, you must also consider how you can handle certain browsers, often called down-level browsers, which might not understand these advanced features or might not have them enabled. Being able to detect and react to down-level browsers is an important consideration when creating your control. ASP.NET includes some powerful tools you can use to detect the type and version of the browser making the page request, as well as what capabilities the browser supports.
ASP.NET uses a highly flexible method for configuring, storing, and discovering browser capabilities. All browser identification and capability information is stored in .browser files. ASP.NET stores these files in the C:\Windows\Microsoft.NET\Framework\v4.0.xxxxx\CONFIG\Browsers directory. If you open this folder, you see that ASP.NET provides you with a variety of .browser files that describe the capabilities of most of today’s common desktop browsers, as well as information on browsers in devices such as PDAs and cellular phones. Open one of the browser files, and you see that the file contains all the identification and capability information for the browser. Listing 7-24 shows the contents of the iPhone capabilities file, which can usually be found on your machine at C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\Browsers\iphone.browser.
LISTING 7-24: A sample browser capabilities file
<browsers>
<!-- Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko)
Version/3.0 Mobile/1A543a Safari/419.3 -->
<gateway id="IPhone" parentID="Safari">
<identification>
<userAgent match="iPhone" />
</identification>
<capabilities>
<capability name="isMobileDevice" value="true" />
<capability name="mobileDeviceManufacturer" value="Apple" />
<capability name="mobileDeviceModel" value="IPhone" />
<capability name="canInitiateVoiceCall" value="true" />
</capabilities>
</gateway>
<!-- Mozilla/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko)
Version/3.0 Mobile/4A93 Safari/419.3 -->
<gateway id="IPod" parentID="Safari">
<identification>
<userAgent match="iPod" />
</identification>
<capabilities>
<capability name="isMobileDevice" value="true" />
<capability name="mobileDeviceManufacturer" value="Apple" />
<capability name="mobileDeviceModel" value="IPod" />
</capabilities>
</gateway>
<!-- Mozilla/5.0 (iPad; U; CPU OS 4_3 like Mac OS X; en-us) AppleWebKit/533.17.9
(KHTML, like Gecko) Version/5.0.2 Mobile/8F191 Safari/6533.18.5 -->
<gateway id="IPad" parentID="Safari">
<identification>
<userAgent match="iPad" />
</identification>
<capabilities>
<capability name="isMobileDevice" value="true" />
<capability name="mobileDeviceManufacturer" value="Apple" />
<capability name="mobileDeviceModel" value="IPad" />
</capabilities>
</gateway>
</browsers>
The advantage of this method for storing browser capability information is that as new browsers are created or new versions are released, developers simply create or update a .browser file to describe the capabilities of that browser.
Now that you have seen how ASP.NET stores browser capability information, you need to know how you can access this information at run time and program your control to change what it renders based on the browser. To access capability information about the requesting browser, you can use the Page.Request.Browser property. This property gives you access to the System.Web.HttpBrowserCapabilities class, which provides information about the capabilities of the browser making the current request. The class provides you with a myriad of attributes and properties that describe what the browser can support and render and what it requires. Lists use this information to add capabilities to the TextBox control. Listing 7-25 shows how you can detect browser capabilities to make sure a browser supports JavaScript.
LISTING 7-25: Detecting browser capabilities in server-side code
VB
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)
If (Page.Request.Browser.EcmaScriptVersion.Major > 0) Then
Page.ClientScript.RegisterClientScriptInclude(
"UtilityFunctions", "Listing07-23.js")
Page.ClientScript.RegisterStartupScript(
GetType(Page), "ClientCallback",
"function ClientCallback() {" &
"args=document.getElementById('" &
Me.ClientID & "_i" & "').value;" &
Page.ClientScript.GetCallbackEventReference(Me, "args",
"CallbackHandler", Nothing, "ErrorHandler", True) +
"}",
True)
Page.ClientScript.RegisterStartupScript(GetType(Page),
"ControlFocus", "document.getElementById('" &
Me.ClientID & "_i" & "').focus();",
True)
End If
End Sub
C#
protected override void OnPreRender(EventArgs e)
{
if (Page.Request.Browser.EcmaScriptVersion.Major > 0)
{
Page.ClientScript.RegisterClientScriptInclude(
"UtilityFunctions", "Listing07-23.js");
Page.ClientScript.RegisterStartupScript(
typeof(Page), "ControlFocus", "document.getElementById('" +
this.ClientID + "_i" + "').focus();",
true);
Page.ClientScript.RegisterStartupScript(
typeof(Page), "ClientCallback",
"function ClientCallback() {" +
"args=document.getElementById('" + this.ClientID + "_i" +
"').value;" +
Page.ClientScript.GetCallbackEventReference(this, "args",
"CallbackHandler", null, "ErrorHandler", true) + "}",
true);
}
}
This is a very simple sample, but it gives you an idea of what is possible using the HttpBrowserCapabilities class.
When you are developing web applications, remember that they are built on the stateless HTTP protocol. ASP.NET gives you a number of ways to give users the illusion that they are using a stateful application, but perhaps the most ubiquitous is called ViewState. ViewState enables you to maintain the state of the objects and controls that are part of the web page through the page’s life cycle by storing the state of the controls in a hidden form field that is rendered as part of the HTML. The state contained in the form field can then be used by the application to reconstitute the page’s state when a postback occurs. Figure 7-14 shows how ASP.NET stores ViewState information in a hidden form field.
Notice that the page contains a hidden form field named _ViewState. The value of this form field is the ViewState for your web page. By default, ViewState is enabled in all in-box server controls shipped with ASP.NET. If you write customer server controls, however, you are responsible for ensuring that a control is participating in the use of ViewState by the page.
The ASP.NET ViewState is basically a storage format that enables you to save and retrieve objects as key/value pairs. As you see in Figure 7-14, these objects are then serialized by ASP.NET and persisted as an encrypted string, which is pushed to the client as a hidden HTML form field. When the page posts back to the server, ASP.NET can use this hidden form field to reconstitute the StateBag, which you can then access as the page is processed on the server.
As shown in Listing 7-26, by default, the Text property included with the ASP.NET Server Control template is set up to store its value in ViewState.
LISTING 7-26: The Text property’s use of ViewState
VB
Property Text() As String
Get
Dim s As String = CStr(ViewState("Text"))
If s Is Nothing Then
Return "[" & Me.ID & "]"
Else
Return s
End If
End Get
Set(ByVal Value As String)
ViewState("Text") = Value
End Set
End Property
C#
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null)? "[" + this.ID + "]" : s);
}
set
{
ViewState["Text"] = value;
}
}
When you are creating new properties in an ASP.NET server control, you should remember to use this same technique to ensure that the values set by the end user in your control will be persisted across page postbacks.
As mentioned in the preceding section, the ViewState is basically a generic collection of objects, but not all objects can be added to the ViewState. Only types that can be safely persisted can be used in the ViewState, so objects such as database connections or file handles should not be added to the ViewState.
Additionally, certain data types are optimized for use in the ViewState. When adding data to the ViewState, try to package the data into these types:
At times, your control must store small amounts of critical, usually private, information across postbacks. To allow for the storage of this type of information, even if a developer disables ViewState, ASP.NET includes a separate type of ViewState called ControlState. ControlState is essentially a private ViewState for your control only.
Two methods, SaveViewState and LoadViewState, provide access to ControlState; however, the implementation of these methods is left up to you. Listing 7-27 shows how you can use the LoadControlState and SaveViewState methods.
LISTING 7-27: Using ControlState in a server control
VB
<DefaultProperty("Text")>
<ToolboxData("<{0}:Listing0727 runat=server></{0}:Listing0727>")>
Public Class Listing0727
Inherits WebControl
Dim state As String
Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
Page.RegisterRequiresControlState(Me)
MyBase.OnInit(e)
End Sub
Protected Overrides Sub LoadControlState(ByVal savedState As Object)
state = CStr(savedState)
End Sub
Protected Overrides Function SaveControlState() As Object
Return CType("ControlSpecificData", Object)
End Function
Protected Overrides Sub Render(ByVal output As System.Web.UI.HtmlTextWriter)
output.Write("Control State: " & state)
End Sub
End Class
C#
[DefaultProperty("Text")]
[ToolboxData("<{0}:Listing0727 runat=server></{0}:Listing0727>")]
public class Listing0727 : WebControl
{
string state;
protected override void OnInit(EventArgs e)
{
Page.RegisterRequiresControlState(this);
base.OnInit(e);
}
protected override void LoadControlState(object savedState)
{
state = (string)savedState;
}
protected override object SaveControlState()
{
return (object)"ControlSpecificData";
}
protected override void RenderContents(HtmlTextWriter output)
{
output.Write("Control State: " + state);
}
}
Controls intending to use ControlState must call the Page.RegisterRequiresControlState method before attempting to save control state data. Additionally, the RegisterRequiresControlState method must be called for each page load because the value is not retained through page postbacks.
As you have seen in this chapter, ASP.NET provides a very powerful set of tools you can use to develop server controls and emit them to a client browser. But this is still one-way communication because the server only pushes data to the client. It would be useful if the server control could send data back to the server. The process of sending data back to the server is generally known as a page postback. You experience a page postback any time you click a form button or link that causes the page to make a new request to the web server.
ASP.NET provides a rich framework for handling postbacks from ASP.NET web pages. A development model that mimics the standard Windows Forms event model is provided that enables you to use controls that, even though they are rendered in the client browser, can raise events in server-side code. It also provides an easy mechanism for plugging a server control into that framework, enabling you to create controls that can initiate a page postback. Figure 7-15 shows the ASP.NET postback framework.
To initiate a page postback, by default ASP.NET uses client-side scripting. If you want your control to be able to initiate a postback, you must attach the postback initiation script to an HTML element event using the GetPostBackEventReference method during the control’s render method. Listing 7-28 shows how you can attach the postback client script to the onClick event of a standard HTML Button element.
LISTING 7-28: Adding postback capabilities to a server control
VB
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
Dim p As New PostBackOptions(Me)
output.AddAttribute(HtmlTextWriterAttribute.Onclick,
Page.ClientScript.GetPostBackEventReference(p))
output.AddAttribute(HtmlTextWriterAttribute.Value, "My Button")
output.AddAttribute(HtmlTextWriterAttribute.Id,
Me.ClientID & "_i")
output.AddAttribute(HtmlTextWriterAttribute.Name,
Me.ClientID & "_i")
output.RenderBeginTag(HtmlTextWriterTag.Button)
output.Write("My Button")
output.RenderEndTag()
End Sub
C#
protected override void RenderContents(HtmlTextWriter output)
{
PostBackOptions p = new PostBackOptions(this);
output.AddAttribute(HtmlTextWriterAttribute.Onclick,
Page.ClientScript.GetPostBackEventReference(p));
output.AddAttribute(HtmlTextWriterAttribute.Id,
this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Name,
this.ClientID + "_i");
output.RenderBeginTag(HtmlTextWriterTag.Button);
output.Write("My Button");
output.RenderEndTag();
}
When the GetPostBackEventReference method is called, it requires a PostBackOptions object be passed to it. The PostBackOptions object enables you to specify a number of configuration options that influence how ASP.NET will initiate the postback.
You can add the postback JavaScript to any client-side event, or even add the code to a client-side function if you want to include some additional pre-postback logic for your control.
Now that the control can initiate a postback, you may want to add events to your control that execute during the page’s postback. To raise server-side events from a client-side object, you implement the System.Web.IPostBackEventHandler interface. Listing 7-29 shows how to do this for the Button in the previous listing.
LISTING 7-29: Handling postback events in a server control
VB
<DefaultProperty("Text"), ToolboxData("<{0}:Listing0729 runat=server></{0}:Listing0729>")>
Public Class Listing0729
Inherits WebControl
Implements IPostBackEventHandler
<Bindable(True), Category("Appearance"), DefaultValue(""), Localizable(True)>
Property Text() As String
Get
Dim s As String = CStr(ViewState("Text"))
If s Is Nothing Then
Return "[" & Me.ID & "]"
Else
Return s
End If
End Get
Set(ByVal Value As String)
ViewState("Text") = Value
End Set
End Property
Public Event Click()
Public Sub OnClick(ByVal args As EventArgs)
RaiseEvent Click()
End Sub
Public Sub RaisePostBackEvent(ByVal eventArgument As String) _
Implements System.Web.UI.IPostBackEventHandler.RaisePostBackEvent
OnClick(EventArgs.Empty)
End Sub
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
Dim p As New PostBackOptions(Me)
output.AddAttribute(HtmlTextWriterAttribute.Onclick,
Page.ClientScript.GetPostBackEventReference(p))
output.AddAttribute(HtmlTextWriterAttribute.Value, "My Button")
output.AddAttribute(HtmlTextWriterAttribute.Id,
Me.ClientID & "_i")
output.AddAttribute(HtmlTextWriterAttribute.Name,
Me.ClientID & "_i")
output.RenderBeginTag(HtmlTextWriterTag.Button)
output.Write("My Button")
output.RenderEndTag()
End Sub
End Class
C#
[DefaultProperty("Text")]
[ToolboxData("<{0}:Listing0729 runat=server></{0}:Listing0729>")]
public class Listing0729 : WebControl, IPostBackEventHandler
{
public event EventHandler Click;
public virtual void OnClick(EventArgs e)
{
if (Click != null)
{
Click(this, e);
}
}
public void RaisePostBackEvent(string eventArgument)
{
OnClick(EventArgs.Empty);
}
protected override void RenderContents(HtmlTextWriter output)
{
PostBackOptions p = new PostBackOptions(this);
output.AddAttribute(HtmlTextWriterAttribute.Onclick,
Page.ClientScript.GetPostBackEventReference(p));
output.AddAttribute(HtmlTextWriterAttribute.Id,
this.ClientID + "_i");
output.AddAttribute(HtmlTextWriterAttribute.Name,
this.ClientID + "_i");
output.RenderBeginTag(HtmlTextWriterTag.Button);
output.Write("My Button");
output.RenderEndTag();
}
}
When the user clicks the Button, a page postback occurs, and ASP.NET calls the RaisePostBackEvent method in the control, which lets you raise a server-side event. If several different client events in your control can initiate a postback, you can change the behavior of your control by using the RaisePostBackEvent method’s eventArgument parameter to determine which element caused the postback. You set the eventArgument parameter using the PostBackOptions object mentioned previously.
Now that you have learned how to store control data in ViewState and add postback capabilities to a control, you can enable the control to handle data a user has entered into form fields on the page. When an ASP.NET control initiates a postback, all the form data from the page is posted to the server. A server control can access and interact with that data, storing the information in ViewState and completing the illusion of a stateful application.
To access postback data, your control must implement the System.Web.IPostBackDataHandler interface. This interface allows ASP.NET to hand to your control the form data that is passed back to the server during the postback.
The IPostBackDataHandler interface requires you to implement two methods: the LoadPostData and RaisePostBackDataChangedEvent methods. Listing 7-30 shows how you implement the IPostBackDataHandler interface method in a simple text input control.
LISTING 7-30: Accessing postback data in a server control
VB
<DefaultProperty("Text"),
ToolboxData("<{0}:Listing0730 runat=server></{0}:Listing0730>")>
Public Class Listing0730
Inherits WebControl
Implements IPostBackEventHandler, IPostBackDataHandler
<Bindable(True), Category("Appearance"),
DefaultValue(""), Localizable(True)>
Property Text() As String
Get
Dim s As String = CStr(ViewState("Text"))
If s Is Nothing Then
Return String.Empty
Else
Return s
End If
End Get
Set(ByVal Value As String)
ViewState("Text") = Value
End Set
End Property
Protected Overrides Sub RenderContents(
ByVal output As HtmlTextWriter)
Dim p As New PostBackOptions(Me)
output.AddAttribute(HtmlTextWriterAttribute.Id, Me.ClientID)
output.AddAttribute(HtmlTextWriterAttribute.Name, Me.ClientID)
output.AddAttribute(HtmlTextWriterAttribute.Value, Me.Text)
output.RenderBeginTag(HtmlTextWriterTag.Input)
output.RenderEndTag()
End Sub
Public Function LoadPostData(ByVal postDataKey As String,
ByVal postCollection As _
System.Collections.Specialized.NameValueCollection) _
As Boolean Implements _
System.Web.UI.IPostBackDataHandler.LoadPostData
Me.Text = postCollection(postDataKey)
Return False
End Function
Public Sub RaisePostDataChangedEvent() _
Implements _
System.Web.UI.IPostBackDataHandler.RaisePostDataChangedEvent
End Sub
Public Event Click()
Public Sub OnClick(ByVal args As EventArgs)
RaiseEvent Click()
End Sub
Public Sub RaisePostBackEvent(ByVal eventArgument As String) _
Implements System.Web.UI.IPostBackEventHandler.RaisePostBackEvent
OnClick(EventArgs.Empty)
End Sub
End Class
C#
[DefaultProperty("Text")]
[ToolboxData("<{0}:Listing0730 runat=server></{0}:Listing0730>")]
public class Listing0730 : WebControl, IPostBackEventHandler, IPostBackDataHandler
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Localizable(true)]
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null) ? "[" + this.ID + "]" : s);
}
set
{
ViewState["Text"] = value;
}
}
protected override void RenderContents(HtmlTextWriter output)
{
PostBackOptions p = new PostBackOptions(this);
output.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
output.AddAttribute(HtmlTextWriterAttribute.Name,
this.ClientID);
output.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);
output.RenderBeginTag(HtmlTextWriterTag.Input);
output.RenderEndTag();
}
public bool LoadPostData(string postDataKey,
System.Collections.Specialized.NameValueCollection postCollection)
{
this.Text = postCollection[postDataKey];
return false;
}
public void RaisePostDataChangedEvent()
{
}
public event EventHandler Click;
public virtual void OnClick(EventArgs e)
{
if (Click != null)
{
Click(this, e);
}
}
public void RaisePostBackEvent(string eventArgument)
{
OnClick(EventArgs.Empty);
}
}
During a postback, ASP.NET calls the LoadPostData method for this control, passing to it as a NameValueCollection any data submitted with the form. The postDataKey method parameter allows the control to access the postback data specific to it from the NameValueCollection.
Using the method parameters you can save the input element’s text to the server control’s Text property. If you remember the earlier ViewState example, the Text property saves the value to ViewState, allowing the control to automatically repopulate the input element’s value when another page postback occurs.
The LoadPostData method requires you to return a boolean value from the method. This value indicates whether ASP.NET should call the RaisePostBackDataChangedEvent method after the LoadPostData method returns. For example, if you created a TextChanged event in the control to notify you that the control’s text has changed, you would want to return True from this method so that you could subsequently raise that event in the RaisePostDataChangedEvent method.
So far, in looking at server controls, you have concentrated on emitting a single HTML element; but this can be fairly limiting. Creating extremely powerful controls often requires that you combine several HTML elements together. Although you can always use the RenderContents method to emit multiple HTML elements, ASP.NET also enables you to emit existing ASP.NET controls from within a custom server control. These types of controls are called composite controls.
To demonstrate how easy creating a composite control can be, try changing the control shown in Listing 7-30 into a composite control. Listing 7-31 shows how you can do this.
LISTING 7-31: Creating a composite control
VB
<DefaultProperty("Text"),
ToolboxData("<{0}:Listing0731 runat=server></{0}:Listing0731>")>
Public Class Listing0731
Inherits System.Web.UI.WebControls.CompositeControl
Protected textbox As TextBox = New TextBox()
Protected Overrides Sub CreateChildControls()
Me.Controls.Add(textbox)
End Sub
End Class
C#
[DefaultProperty("Text")]
[ToolboxData("<{0}:Listing0731 runat=server></{0}:Listing0731>")]
public class Listing0731 : CompositeControl
{
protected TextBox textbox = new TextBox();
protected override void CreateChildControls()
{
this.Controls.Add(textbox);
}
}
A number of things in this listing are important. First, notice that the control class is now inheriting from CompositeControl, rather than WebControl. Deriving from CompositeControl gives you a few extra features specific to this type of control.
Second, notice that no Render method appears in this code. Instead, you simply create an instance of another type of server control and add that to the Controls collection in the CreateChildControls method. When you run this sample, you see that it renders a textbox just like the previous control did. In fact, the HTML that it renders is almost identical.
When you drop a composite control (such as the control from the previous sample) onto the design surface, notice that even though you are leveraging a powerful ASP.NET TextBox control within the control, none of that control’s properties are exposed to you in the Properties Explorer. To expose child control properties through the parent container, you must create corresponding properties in the parent control. For example, if you want to expose the ASP.NET textbox Text property through the parent control, you create a Text property. Listing 7-32 shows how to do this.
LISTING 7-32: Exposing control properties in a composite control
VB
<DefaultProperty("Text"),
ToolboxData("<{0}:Listing0732 runat=server></{0}:Listing0732>")>
Public Class Listing0732
Inherits System.Web.UI.WebControls.CompositeControl
Protected textbox As TextBox = New TextBox()
Public Property Text() As String
Get
EnsureChildControls()
Return textbox.Text
End Get
Set(ByVal value As String)
EnsureChildControls()
textbox.Text = value
End Set
End Property
Protected Overrides Sub CreateChildControls()
Me.Controls.Add(textbox)
Me.ChildControlsCreated = True
End Sub
End Class
C#
[DefaultProperty("Text")]
[ToolboxData("<{0}:Listing0732 runat=server></{0}:Listing0732>")]
public class Listing0732 : CompositeControl
{
protected TextBox textbox = new TextBox();
public string Text
{
get
{
EnsureChildControls();
return textbox.Text;
}
set
{
EnsureChildControls();
textbox.Text = value;
}
}
protected override void CreateChildControls()
{
this.Controls.Add(textbox);
this.ChildControlsCreated = true;
}
}
Notice that you use this property simply to populate the underlying control’s properties. Also notice that before you access the underlying control’s properties, you always call the EnsureChildControls method. This method ensures that children of the container control have actually been initialized before you attempt to access them.
In addition to composite controls, you can also create templated controls. Templated controls enable the developer to specify a portion of the HTML that is used to render a container control with nested controls. You might be familiar with the Repeater or DataList control. These are both templated controls that let you specify how you would like data to be bound and displayed when the page renders.
To demonstrate a templated control, the following code listings give you a basic example of displaying a simple text message on a web page. Because the control is a templated control, the developer has complete control over how the message is displayed.
To get started, create the Message server control that will be used as the template inside of a container control. Listing 7-33 shows the class that simply extends the existing Panel control by adding two additional properties, Name and Text, and a new constructor.
LISTING 7-33: Creating the templated control’s inner control class
VB
Public Class Message
Inherits System.Web.UI.WebControls.Panel
Implements System.Web.UI.INamingContainer
Public Property Name() As String
Public Property Text() As String
End Class
C#
public class Message : Panel, INamingContainer
{
public string Name { get; internal set; }
public string Text { get; internal set; }
}
As you see in a moment, you can access the public properties exposed by the Message class to insert dynamic content into the template. You also see how you can display the values of the Name and Text properties as part of the rendered template control.
Next, as shown in Listing 7-34, create a new server control that will be the container for the Message control. This server control is responsible for rendering any template controls nested in it.
LISTING 7-34: Creating the template control container class
VB
<DefaultProperty("Text")>
<ToolboxData("<{0}:Listing0734 runat=server></{0}:Listing0734>")>
Public Class Listing0734
Inherits System.Web.UI.WebControls.WebControl
<Browsable(False)> Public Property TemplateMessage() As Message
<PersistenceMode(PersistenceMode.InnerProperty),
TemplateContainer(GetType(Message))>
Public Property MessageTemplate() As ITemplate
<Bindable(True), DefaultValue("")>
Public Property Name() As String
<Bindable(True), DefaultValue("")>
Public Property Text() As String
Public Overrides Sub DataBind()
EnsureChildControls()
ChildControlsCreated = True
MyBase.DataBind()
End Sub
Protected Overrides Sub CreateChildControls()
Me.Controls.Clear()
Me.TemplateMessage = New Message() With {.Name = Name, .Text = Text}
If Me.MessageTemplate Is Nothing Then
Me.MessageTemplate = New DefaultMessageTemplate()
End If
Me.MessageTemplate.InstantiateIn(Me.TemplateMessage)
Controls.Add(Me.TemplateMessage)
End Sub
Protected Overrides Sub RenderContents(
ByVal writer As System.Web.UI.HtmlTextWriter)
EnsureChildControls()
ChildControlsCreated = True
MyBase.RenderContents(writer)
End Sub
End Class
C#
[DefaultProperty("Text")]
[ToolboxData("<{0}:Listing0734 runat=server></{0}:Listing0734>")]
public class Listing0734 : WebControl
{
[Browsable(false)]
public Message TemplateMessage { get; internal set; }
[PersistenceMode(PersistenceMode.InnerProperty)]
[TemplateContainer(typeof(Message))]
public virtual ITemplate MessageTemplate { get; set; }
[Bindable(true)]
[DefaultValue("")]
public string Name { get; set; }
[Bindable(true)]
[DefaultValue("")]
public string Text { get; set; }
public override void DataBind()
{
EnsureChildControls();
ChildControlsCreated = true;
base.DataBind();
}
protected override void CreateChildControls()
{
this.Controls.Clear();
this.TemplateMessage = new Message() { Name = Name, Text = Text };
if (this.MessageTemplate == null)
{
this.MessageTemplate = new DefaultMessageTemplate();
}
this.MessageTemplate.InstantiateIn(this.TemplateMessage);
Controls.Add(this.TemplateMessage);
}
protected override void RenderContents(HtmlTextWriter writer)
{
EnsureChildControls();
ChildControlsCreated = true;
base.RenderContents(writer);
}
}
To start to dissect this sample, first notice the MessageTemplate property. This property allows Visual Studio to understand that the control will contain a template and allows it to display the IntelliSense for that template. The property has been marked with the PersistanceMode attribute, indicating that the template control should be persisted as an inner property within the control’s tag in the ASPX page. Additionally, the property is marked with the TemplateContainer attribute, which helps ASP.NET figure out what type of template control this property represents. In this case, it’s the Message template control you created earlier.
The container control exposes two public properties: Name and Text. These properties are used to populate the Name and Text properties of the Message control because that class does not allow developers to set the properties directly.
Finally, the CreateChildControls method, called by the DataBind method, does most of the heavy lifting in this control. It creates a new Message object, passing the values of Name and Text as constructor values. After the CreateChildControls method completes, the base DataBind operation continues to execute. This is important because that is where the evaluation of the Name and Text properties occurs, which enables you to insert these properties’ values into the template control.
One additional thing to consider when creating templated controls is what should happen if you do not specify a template for the control. In the previous code listing, if you removed the MessageTemplate from the TemplateContainer, a NullReferenceException would occur when you tried to run your web page because the container control’s MessageTemplate property would return a null value. To prevent this, you can include a default template class as part of the container control. An example of a default template is shown in Listing 7-35.
LISTING 7-35: Creating the templated control’s default template class
VB
Friend Class DefaultMessageTemplate
Implements ITemplate
Public Sub InstantiateIn(ByVal container As System.Web.UI.Control) _
Implements System.Web.UI.ITemplate.InstantiateIn
Dim l As New Literal()
l.Text = "No MessageTemplate was included."
container.Controls.Add(l)
End Sub
End Class
C#
internal sealed class DefaultMessageTemplate : ITemplate
{
public void InstantiateIn(Control container)
{
Literal l = new Literal();
l.Text = "No MessageTemplate was included.";
container.Controls.Add(l);
}
}
Notice that the DefaultMessageTemplate implements the ITemplate interface. This interface requires that the InstantiateIn method be implemented, which you use to provide the default template content.
To include the default template, simply add the class to the TemplatedControl class. You also need to modify the CreateChildControls method to detect the null MessageTemplate and instead create an instance of and use the default template:
VB
If Me.MessageTemplate Is Nothing Then
Me.MessageTemplate = New DefaultMessageTemplate()
End If
C#
if (this.MessageTemplate == null)
{
this.MessageTemplate = new DefaultMessageTemplate();
}
After the control and default template are created, you can drop them onto a test web page. Listing 7-36 shows how you can use the control to customize the display of the data.
LISTING 7-36: Adding a templated control to a web page
VB
<%@ Page Language="VB" %>
<%@ Register assembly="VbServerControl1" namespace="VbServerControl1" tagprefix="cc1" %>
<!DOCTYPE html>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Me.Listing07341.DataBind()
End Sub
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title>Templated Web Controls</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<cc1:Listing0734 ID="Listing07341" runat="server" Name="John Doe"
Text="Hello World!">
<MessageTemplate>The user '<%# Container.Name %>' has a
message for you: <br />"<%# Container.Text %>"
</MessageTemplate>
</cc1:Listing0734>
</div>
</form>
</body>
</html>
C#
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
this.Listing07341.DataBind();
}
</script>
As you can see in the listing, the <cc1:TemplatedControl> control contains a MessageTemplate within it, which has been customized to display the Name and Text values. Figure 7-16 shows this page after it has been rendered in the browser.
So far in this chapter, you concentrated primarily on what gets rendered to the client’s browser, but the browser is not the only consumer of server controls. Visual Studio and the developer using a server control are also consumers, and you need to consider their experiences when using your control.
ASP.NET offers numerous ways to give developers using your control a great design-time experience. Some of these require no additional coding, such as the WYSIWYG rendering of user controls and basic server controls. For more complex scenarios, ASP.NET includes a variety of options that enable you to give the developer an outstanding design-time experience when using your control.
When you write server controls, a priority should be to give the developer a design-time experience that closely replicates the runtime experience. This means altering the appearance of the control on the design surface in response to changes in control properties and the introduction of other server controls onto the design surface. Three main components are involved in creating the design-time behaviors of a server control:
Because a chapter can be written for each one of these topics, this section gives you just an overview of each, how they tie into a control’s design-time behavior, and some simple examples of their use.
TypeConverter is a class that enables you to perform conversions between one type and another. Visual Studio uses type converters at design time to convert object property values to String types so that they can be displayed on the Property Browser, and it returns them to their original types when the developer changes the property.
ASP.NET includes a wide variety of type converters you can use when creating your control’s design-time behavior. These range from converters that enable you to convert most numeric types, to converters that let you convert Fonts, Colors, DataTimes, and Guids. The easiest way to see what type converters are available to you in the .NET Framework is to search for types in the framework that derive from the TypeConverter class using the MSDN Library help.
After you have found a type converter that you want to use on a control property, mark the property with a TypeConverter attribute, as shown in Listing 7-37.
LISTING 7-37: Applying the TypeConverter attribute to a property
VB
<DefaultProperty("Text"),
ToolboxData("<{0}:ServerControl39 runat=server></{0}:ServerControl39>")>
Public Class Listing0737
Inherits WebControl
<Bindable(True)>
<Category("Appearance")>
<DefaultValue("")>
<TypeConverter(GetType(GuidConverter))>
Property BookId() As System.Guid
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
output.Write(BookId.ToString())
End Sub
End Class
C#
[DefaultProperty("Text")]
[ToolboxData("<{0}:Listing0737 runat=server></{0}:Listing0737>")]
public class Listing0737 : WebControl
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[TypeConverter(typeof(GuidConverter))]
public Guid BookId { get; set; }
protected override void RenderContents(HtmlTextWriter output)
{
output.Write(BookId.ToString());
}
}
In this example, a property is exposed that accepts and returns an object of type Guid. The Property Browser cannot natively display a Guid object, so you convert the value to a string so that it can be displayed properly in the Property Browser. Marking the property with the TypeConverter attribute and, in this case, specifying the GuidConverter as the type converter you want to use, allows complex objects like a Guid to display properly in the Property Browser.
Creating your own custom type converters if none of the in-box converters fit into your scenario is also possible. Type converters derive from the System.ComponentModel.TypeConverter class. Listing 7-38 shows a custom type converter that converts a custom object called Name to and from a string.
LISTING 7-38: Creating a custom type converter
VB
Imports System
Imports System.ComponentModel
Imports System.Globalization
Public Class Name
Private _first As String
Private _last As String
Public Sub New(ByVal first As String, ByVal last As String)
_first = first
_last = last
End Sub
Public Property First() As String
Get
Return _first
End Get
Set(ByVal value As String)
_first = value
End Set
End Property
Public Property Last() As String
Get
Return _last
End Get
Set(ByVal value As String)
_last = value
End Set
End Property
End Class
Public Class NameConverter
Inherits TypeConverter
Public Overrides Function CanConvertFrom(ByVal context As _
ITypeDescriptorContext, ByVal sourceType As Type) As Boolean
If (sourceType Is GetType(String)) Then
Return True
End If
Return MyBase.CanConvertFrom(context, sourceType)
End Function
Public Overrides Function ConvertFrom( _
ByVal context As ITypeDescriptorContext, _
ByVal culture As CultureInfo, ByVal value As Object) As Object
If (value Is GetType(String)) Then
Dim v As String() = (CStr(value).Split(New [Char]() {" "c}))
Return New Name(v(0), v(1))
End If
Return MyBase.ConvertFrom(context, culture, value)
End Function
Public Overrides Function ConvertTo( _
ByVal context As ITypeDescriptorContext, _
ByVal culture As CultureInfo, ByVal value As Object, _
ByVal destinationType As Type) As Object
If (destinationType Is GetType(String)) Then
Return (CType(value, Name).First + " " + (CType(value, Name).Last))
End If
Return MyBase.ConvertTo(context, culture, value, destinationType)
End Function
End Class
C#
using System;
using System.ComponentModel;
using System.Globalization;
public class Name
{
public Name(string first, string last)
{
First = first;
Last = last;
}
public string First { get; set; }
public string Last { get; set; }
}
public class NameConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context,
Type sourceType) {
if (sourceType == typeof(string)) {
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value) {
if (value is string) {
string[] v = ((string)value).Split(new char[] {' '});
return new Name(v[0],v[1]);
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context,
CultureInfo culture, object value, Type destinationType) {
if (destinationType == typeof(string)) {
return ((Name)value).First + " " + ((Name)value).Last;
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
The NameConverter class overrides three methods: CanConvertFrom, ConvertFrom, and ConvertTo. The CanConvertFrom method enables you to control what types the converter can convert from. The ConvertFrom method converts the string representation back into a Name object, and ConvertTo converts the Name object into a string representation.
After you have built your type converter, you can use it to mark properties in your control with the TypeConverter attribute, as you saw in Listing 7-37.
Controls that live on the Visual Studio design surface depend on control designers to create the design-time experience for the end user. Control designers, for both WinForms and ASP.NET, are classes that derive from the System.ComponentModel.Design.ComponentDesigner class. .NET provides an abstracted base class specifically for creating ASP.NET control designers called the System.Web.UI.Design.ControlDesigner. To access these classes you need to add a reference to the System.Design.dll assembly to your project.
.NET includes a number of in-box control designer classes that you can use when creating a custom control; but as you develop server controls, you see that .NET automatically applies a default designer. The designer it applies is based on the type of control you are creating. For instance, when you created your first TextBox control, Visual Studio used the ControlDesigner class to achieve the WYSIWYG design-time rendering of the textbox. If you develop a server control derived from the ControlContainer class, .NET automatically uses the ControlContainerDesigner class as the designer.
You can also explicitly specify the designer you want to use to render your control at design time using the Designer attribute on your control’s class, as shown in Listing 7-39.
LISTING 7-39: Adding a Designer attribute to a control class
VB
<DefaultProperty("Text"),
ToolboxData("<{0}:Listing0739 runat=server></{0}:Listing0739>"),
Designer(GetType(System.Web.UI.Design.ControlDesigner))>
Public Class Listing0739
Inherits System.Web.UI.WebControls.WebControl
C#
[DefaultProperty("Text")]
[ToolboxData("<{0}:Listing0739 runat=server></{0}:Listing0739>")]
[Designer(typeof(System.Web.UI.Design.ControlDesigner))]
public class Listing0739 : WebControl
Notice that you added the Designer attribute to the Listing0739 class. You have specified that the control should use the ControlDesigner class (found in the System.Design assembly) as its designer. Other in-box designers you could have specified are:
Each designer provides a specific design-time behavior for the control, and you can select one that is appropriate for the type of control you are creating.
As you saw earlier, ASP.NET enables you to create server controls that consist of other server controls and text. ASP.NET enables you to create server controls that have design-time editable portions using a technique called designer regions. Designer regions enable you to create multiple, independent regions defined within a single control and respond to events raised by a design region. This might be the designer drawing a control on the design surface or the user clicking an area of the control or entering or exiting a template edit mode.
To show how you can use designer regions, create a container control to which you can apply a custom control designer, as shown in Listing 7-40.
LISTING 7-40: Creating a composite control with designer regions
VB
Imports System.ComponentModel
Imports System.Web.UI
Imports System.Web.UI.WebControls
<Designer(GetType(MultiRegionControlDesigner))> _
<ToolboxData("<{0}:Listing0740 runat=server width=100%></{0}:Listing0740>")> _
Public Class Listing0740
Inherits CompositeControl
' Define the templates that represent 2 views on the control
Private _view1 As ITemplate
Private _view2 As ITemplate
' These properties are inner properties
<PersistenceMode(PersistenceMode.InnerProperty), DefaultValue("")> _
Public Overridable Property View1() As ITemplate
Get
Return _view1
End Get
Set(ByVal value As ITemplate)
_view1 = value
End Set
End Property
<PersistenceMode(PersistenceMode.InnerProperty), DefaultValue("")> _
Public Overridable Property View2() As ITemplate
Get
Return _view2
End Get
Set(ByVal value As ITemplate)
_view2 = value
End Set
End Property
' The current view on the control; 0= view1, 1=view2, 2=all views
Private _currentView As Int32 = 0
Public Property CurrentView() As Int32
Get
Return _currentView
End Get
Set(ByVal value As Int32)
_currentView = value
End Set
End Property
Protected Overrides Sub CreateChildControls()
MyBase.CreateChildControls()
Controls.Clear()
Dim template As ITemplate = View1
If (_currentView = 1) Then
template = View2
End If
Dim p As New Panel()
Controls.Add(p)
If (Not template Is Nothing) Then
template.InstantiateIn(p)
End If
End Sub
End Class
C#
[Designer(typeof(MultiRegionControlDesigner))]
[ToolboxData("<{0}:Listing0740 runat=\"server\" width=\"100%\">" +
"</{0}:Listing0740>")]
public class Listing0740 : CompositeControl
{
// Define the templates that represent 2 views on the control
private ITemplate _view1;
private ITemplate _view2;
// These properties are inner properties
[PersistenceMode(PersistenceMode.InnerProperty), DefaultValue(null)]
public virtual ITemplate View1
{
get { return _view1; }
set { _view1 = value; }
}
[PersistenceMode(PersistenceMode.InnerProperty), DefaultValue(null)]
public virtual ITemplate View2
{
get { return _view2; }
set { _view2 = value; }
}
// The current view on the control; 0= view1, 1=view2, 2=all views
private int _currentView = 0;
public int CurrentView
{
get { return _currentView; }
set { _currentView = value; }
}
protected override void CreateChildControls()
{
Controls.Clear();
ITemplate template = View1;
if (_currentView == 1)
template = View2;
Panel p = new Panel();
Controls.Add(p);
if (template != null)
template.InstantiateIn(p);
}
}
The container control creates two ITemplate objects, which serve as the controls to display. The ITemplate objects are the control containers for this server control, enabling you to drop other server controls or text into this control. The control also uses the Designer attribute to indicate to Visual Studio that it should use the MultiRegionControlDesigner class when displaying this control on the designer surface.
Now you create the control designer that defines the regions for the control. Listing 7-41 shows the designer class.
LISTING 7-41: A custom designer class used to define designer regions
VB
Public Class MultiRegionControlDesigner
Inherits System.Web.UI.Design.WebControls.CompositeControlDesigner
Protected _currentView As Int32 = 0
Private myControl As Listing0740
Public Overrides Sub Initialize(ByVal component As IComponent)
MyBase.Initialize(component)
myControl = CType(component, Listing0740)
End Sub
Public Overrides ReadOnly Property AllowResize() As Boolean
Get
Return True
End Get
End Property
Protected Overrides Sub OnClick(ByVal e As DesignerRegionMouseEventArgs)
If (e.Region Is Nothing) Then
Return
End If
If ((e.Region.Name = "Header0") And (Not _currentView = 0)) Then
_currentView = 0
UpdateDesignTimeHtml()
End If
If ((e.Region.Name = "Header1") And (Not _currentView = 1)) Then
_currentView = 1
UpdateDesignTimeHtml()
End If
End Sub
Public Overrides Function GetDesignTimeHtml( _
ByVal regions As DesignerRegionCollection) As String
BuildRegions(regions)
Return BuildDesignTimeHtml()
End Function
Protected Overridable Sub BuildRegions( _
ByVal regions As DesignerRegionCollection)
regions.Add(New DesignerRegion(Me, "Header0"))
regions.Add(New DesignerRegion(Me, "Header1"))
' If the current view is for all, we need another editable region
Dim edr0 As New EditableDesignerRegion(Me, "Content" & _currentView, False)
edr0.Description = "Add stuff in here if you dare:"
regions.Add(edr0)
' Set the highlight, depending upon the selected region
If ((_currentView = 0) Or (_currentView = 1)) Then
regions(_currentView).Highlight = True
End If
End Sub
Protected Overridable Function BuildDesignTimeHtml() As String
Dim sb As New StringBuilder()
sb.Append(BuildBeginDesignTimeHtml())
sb.Append(BuildContentDesignTimeHtml())
sb.Append(BuildEndDesignTimeHtml())
Return sb.ToString()
End Function
Protected Overridable Function BuildBeginDesignTimeHtml() As String
' Create the table layout
Dim sb As New StringBuilder()
sb.Append("<table ")
' Styles that we'll use to render for the design-surface
sb.Append("height='" & myControl.Height.ToString() & "' width='" & _
myControl.Width.ToString() & "'>")
' Generate the title or caption bar
sb.Append("<tr height='25px' align='center' " & _
"style='font-family:tahoma;font-size:10pt;font-weight:bold;'>" & _
"<td style='width:50%' " & _
DesignerRegion.DesignerRegionAttributeName & "='0'>")
sb.Append("Page-View 1</td>")
sb.Append("<td style='width:50%' " & _
DesignerRegion.DesignerRegionAttributeName & "='1'>")
sb.Append("Page-View 2</td></tr>")
Return sb.ToString()
End Function
Protected Overridable Function BuildEndDesignTimeHtml() As String
Return ("</table>")
End Function
Protected Overridable Function BuildContentDesignTimeHtml() As String
Dim sb As New StringBuilder()
sb.Append("<td colspan='2' style='")
sb.Append("background-color:" & _
myControl.BackColor.Name.ToString() & ";' ")
sb.Append(DesignerRegion.DesignerRegionAttributeName & "='2'>")
Return sb.ToString()
End Function
Public Overrides Function GetEditableDesignerRegionContent( _
ByVal region As EditableDesignerRegion) As String
Dim host As IDesignerHost = _
CType(Component.Site.GetService(GetType(IDesignerHost)), IDesignerHost)
If (Not host Is Nothing) Then
Dim template As ITemplate = myControl.View1
If (region.Name = "Content1") Then
template = myControl.View2
End If
If (Not template Is Nothing) Then
Return ControlPersister.PersistTemplate(template, host)
End If
End If
Return String.Empty
End Function
Public Overrides Sub SetEditableDesignerRegionContent( _
ByVal region As EditableDesignerRegion, ByVal content As String)
Dim regionIndex As Int32 = Int32.Parse(region.Name.Substring(7))
If (content Is Nothing) Then
If (regionIndex = 0) Then
myControl.View1 = Nothing
ElseIf (regionIndex = 1) Then
myControl.View2 = Nothing
Return
End If
Dim host As IDesignerHost = _
CType(Component.Site.GetService(GetType(IDesignerHost)),
IDesignerHost)
If (Not host Is Nothing) Then
Dim template = ControlParser.ParseTemplate(host, content)
If (Not template Is Nothing) Then
If (regionIndex = 0) Then
myControl.View1 = template
ElseIf (regionIndex = 1) Then
myControl.View2 = template
End If
End If
End If
End If
End Sub
End Class
C#
public class MultiRegionControlDesigner :
System.Web.UI.Design.WebControls.CompositeControlDesigner {
protected int _currentView = 0;
private Listing0740 myControl;
public override void Initialize(IComponent component)
{
base.Initialize(component);
myControl = (Listing0740)component;
}
public override bool AllowResize { get { return true;}}
protected override void OnClick(DesignerRegionMouseEventArgs e)
{
if (e.Region == null)
return;
if (e.Region.Name == "Header0" && _currentView != 0) {
_currentView = 0;
UpdateDesignTimeHtml();
}
if (e.Region.Name == "Header1" && _currentView != 1) {
_currentView = 1;
UpdateDesignTimeHtml();
}
}
public override String GetDesignTimeHtml(DesignerRegionCollection regions)
{
BuildRegions(regions);
return BuildDesignTimeHtml();
}
protected virtual void BuildRegions(DesignerRegionCollection regions)
{
regions.Add(new DesignerRegion(this, "Header0"));
regions.Add(new DesignerRegion(this, "Header1"));
// If the current view is for all, we need another editable region
EditableDesignerRegion edr0 = new
EditableDesignerRegion(this, "Content" + _currentView, false);
edr0.Description = "Add stuff in here if you dare:";
regions.Add(edr0);
// Set the highlight, depending upon the selected region
if (_currentView ==0 || _currentView==1)
regions[_currentView].Highlight = true;
}
protected virtual string BuildDesignTimeHtml()
{
StringBuilder sb = new StringBuilder();
sb.Append(BuildBeginDesignTimeHtml());
sb.Append(BuildContentDesignTimeHtml());
sb.Append(BuildEndDesignTimeHtml());
return sb.ToString();
}
protected virtual String BuildBeginDesignTimeHtml()
{
// Create the table layout
StringBuilder sb = new StringBuilder();
sb.Append("<table ");
// Styles that we'll use to render for the design-surface
sb.Append("height='" + myControl.Height.ToString() + "' width='" +
myControl.Width.ToString() + "'>");
// Generate the title or caption bar
sb.Append("<tr height='25px' align='center' " +
"style='font-family:tahoma;font-size:10pt;font-weight:bold;'>" +
"<td style='width:50%' " + DesignerRegion.DesignerRegionAttributeName +
"='0'>");
sb.Append("Page-View 1</td>");
sb.Append("<td style='width:50%' " +
DesignerRegion.DesignerRegionAttributeName + "='1'>");
sb.Append("Page-View 2</td></tr>");
return sb.ToString();
}
protected virtual String BuildEndDesignTimeHtml()
{
return ("</table>");
}
protected virtual String BuildContentDesignTimeHtml()
{
StringBuilder sb = new StringBuilder();
sb.Append("<td colspan='2' style='");
sb.Append("background-color:" + myControl.BackColor.Name.ToString() +
";' ");
sb.Append(DesignerRegion.DesignerRegionAttributeName + "='2'>");
return sb.ToString();
}
public override string GetEditableDesignerRegionContent
(EditableDesignerRegion region)
{
IDesignerHost host =
(IDesignerHost)Component.Site.GetService(typeof(IDesignerHost));
if (host != null) {
ITemplate template = myControl.View1;
if (region.Name == "Content1")
template = myControl.View2;
if (template != null)
return ControlPersister.PersistTemplate(template, host);
}
return String.Empty;
}
public override void SetEditableDesignerRegionContent
(EditableDesignerRegion region, string content)
{
int regionIndex = Int32.Parse(region.Name.Substring(7));
if (content == null)
{
if (regionIndex == 0)
myControl.View1 = null;
else if (regionIndex == 1)
myControl.View2 = null;
return;
}
IDesignerHost host =
(IDesignerHost)Component.Site.GetService(typeof(IDesignerHost));
if (host != null)
{
ITemplate template = ControlParser.ParseTemplate(host, content);
if (template != null)
{
if (regionIndex == 0)
myControl.View1 = template;
else if (regionIndex == 1)
myControl.View2 = template;
}
}
}
}
The designer overrides the GetDesignTimeHtml method, calling the BuildRegions and BuildDesignTimeHtml methods to alter the HTML that the control renders to the Visual Studio design surface.
The BuildRegions method creates three design regions in the control: two header regions and an editable content region. The regions are added to the DesignerRegionCollection. The BuildDesignTimeHtml method calls three methods to generate the actual HTML that is generated by the control at design time.
The designer class also contains two overridden methods for getting and setting the editable designer region content: GetEditableDesignerRegionContent and SetEditableDesignerRegionContent. These methods get or set the appropriate content HTML, based on the designer region template that is currently active.
Finally, the class contains an OnClick method that it uses to respond to click events fired by the control at design time. This control uses the OnClick event to switch the current region being displayed by the control at design time.
When you add the control to a Web Form, you see that you can toggle between the two editable regions, and each region maintains its own content. Figure 7-17 shows what the control looks like on the Visual Studio design surface.
As you can see in Figure 7-17, the control contains three separate design regions. When you click design regions 1 or 2, the OnClick method in the designer fires and redraws the control on the design surface, changing the template area located in design region 3.
Another great feature of ASP.NET design-time support is control smart tags. Smart tags give developers using a control quick access to common control properties. To add menu items to a server control’s smart tag, you create a new class that inherits from the DesignerActionList class. The DesignerActionList class contains the list of designer action items that are displayed by a server control. Classes that derive from the DesignerActionList class can override the GetSortedActionItems method, creating their own DesignerActionItemsCollection object to which you can add designer action items.
You can add several different types of DesignerActionItems to the collection:
Listing 7-42 shows a control designer class that contains a private class deriving from DesignerActionList.
LISTING 7-42: Adding designer actions to a control designer
VB
Public Class Listing0742Designer
Inherits ControlDesigner
Private _actionLists As DesignerActionListCollection
Public Overrides ReadOnly Property ActionLists() _
As DesignerActionListCollection
Get
If IsNothing(_actionLists) Then
_actionLists = New DesignerActionListCollection()
_actionLists.AddRange(MyBase.ActionLists)
_actionLists.Add(New ServerControl44ControlList(Me))
End If
Return _actionLists
End Get
End Property
Private NotInheritable Class ServerControl44ControlList
Inherits DesignerActionList
Public Sub New(ByVal c As Listing0742Designer)
MyBase.New(c.Component)
End Sub
Public Overrides Function GetSortedActionItems() _
As DesignerActionItemCollection
Dim c As New DesignerActionItemCollection()
c.Add(New DesignerActionTextItem("Text Action Item",
"Custom Category"))
Return c
End Function
End Class
End Class
C#
public class Listing0742Designer : ControlDesigner
{
private DesignerActionListCollection _actionLists = null;
public override DesignerActionListCollection ActionLists
{
get
{
if (_actionLists == null)
{
_actionLists = new DesignerActionListCollection();
_actionLists.AddRange(base.ActionLists);
_actionLists.Add(new ServerControl44ControlList(this));
}
return _actionLists;
}
}
private sealed class ServerControl44ControlList :
DesignerActionList
{
public ServerControl44ControlList(ControlDesigner c)
: base(c.Component)
{
}
public override DesignerActionItemCollection
GetSortedActionItems()
{
DesignerActionItemCollection c =
new DesignerActionItemCollection();
c.Add(new DesignerActionTextItem("Text Action Item",
"Custom Category"));
return c;
}
}
}
The control designer class overrides the ActionsLists property. The property creates an instance of the TextControlList class, which derives from DesignerActionList and overrides the GetSortedActionItems method. The method creates a new DesignerActionListCollection, and a DesignerActionTextItem is added to the collection (see Figure 7-18). The DesignerActionTextItem class enables you to add text menu items to the smart tag.
As shown in Figure 7-18, when you add the control to a web page, the control now has a smart tag with the DesignerActionTextItem class as content.
A UI type editor is a way to provide users of your controls with a custom interface for editing properties directly from the Property Browser. One type of UI type editor you might already be familiar with is the Color Picker you see when you want to change the ForeColor attribute that exists on most ASP.NET controls. ASP.NET provides a wide variety of in-box UI type editors that make editing more complex property types easy. The easiest way to find what UI type editors are available in the .NET Framework is to search for types derived from the UITypeEditor class in the MSDN Library help or searching the Internet.
After you find the type editor you want to use on your control property, you simply apply the UI type editor to the property using the Editor attribute. Listing 7-43 shows how to do this.
LISTING 7-43: Adding a UI type editor to a control property
VB
<ToolboxData("<{0}:Listing0743 runat=server></{0}:Listing0743>")>
Public Class Listing0743
Inherits System.Web.UI.WebControls.WebControl
<Bindable(True), Category("Appearance"), DefaultValue(""),
Editor(
GetType(System.Web.UI.Design.UrlEditor),
GetType(System.Drawing.Design.UITypeEditor))>
Public Property Url() As String
Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
output.Write(Url.ToString())
End Sub
End Class
C#
[ToolboxData("<{0}:Listing0743 runat=server></{0}:Listing0743>")]
public class Listing0743 : WebControl
{
[Bindable(true)]
[Category("Appearance")]
[DefaultValue("")]
[Editor(typeof(System.Web.UI.Design.UrlEditor),
typeof(System.Drawing.Design.UITypeEditor))]
public string Url { get; set; }
protected override void RenderContents(HtmlTextWriter output)
{
output.Write(this.Url);
}
}
In this sample, you have created a Url property for a control. Because you know this property will be a URL, you want to give the control user a positive design-time experience. You can use the UrlEditor type editor to make it easier for users to select a URL. Figure 7-19 shows the URL Editor that appears when the user edits the control property.
In this chapter, you learned a number of ways you can create reusable, encapsulated chunks of code. You first looked at user controls, the simplest form of control creation. You learned how to create user controls and how you can make them interact with their host web pages. Creating user controls is quite easy, but they lack the portability of other control-creation options.
Then, you saw how you can create your own custom server controls. You looked at many of the tools you can create by writing custom server controls, emitting HTML, and creating CSS styles. The chapter also discussed the type of server controls you can create, ranging from server controls that simply inherit from the WebControl class to templated controls that give users of the control the power to define the display of the server control.
Finally, you looked at ways you can give the users of your server control a great design-time experience by providing them with type convertors, design surface interactions, and custom property editors in your server control.