Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
908 C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S
The Rendering Process
The previous example introduced several new rendering methods. Before going any further, it’s a good idea to look at how they all work together.
The starting point for the rendering process is the RenderControl() method. The RenderControl() method is the public rendering method that ASP.NET uses to render each control on a web page to HTML. You can’t override RenderControl(). Instead, RenderControl() calls the protected Render() method that starts the rendering process. You can override Render(), as demonstrated in the first example in this chapter. However, if you override Render() and don’t call the base implementation of the Render() method, none of the other rendering methods will fire.
The base implementation of the Render() method calls RenderBeginTag(), RenderContents(), and then RenderEndTag(), as you saw in the previous example. However, this has one more twist. The base implementation of the RenderContents() method calls another rendering method— RenderChildren(). This method loops through the collection of child controls in the Controls collection and calls the RenderControl() method for each individual control. By taking advantage of this behavior, you can easily build a control from other controls. This approach is demonstrated later in this chapter with composite controls (see the section “Composite Controls”).
So, which rendering method should you override? If you want to replace the entire rendering process with something new, or if you want to add HTML content before your base control tag (such as a block of JavaScript code), you can override Render(). If you want to take advantage of the automatic style attributes, you should define a base tag and override RenderContents(). If you want to prevent child controls from being displayed or customize how they are rendered (for example, by rendering them in the reverse order), you can override RenderChildren().
Figure 27-5 summarizes the rendering process.
Figure 27-5. The control rendering methods
It’s worth noting that you can call RenderControl() yourself to examine the HTML output for a control. In fact, this technique can be a convenient shortcut when debugging. Here’s an example that gets the rendered HTML for a control and displays it in a label on a web page:
//Create the in-memory objects that will catch the rendered output. StringWriter writer = new StringWriter();
HtmlTextWriter output = new HtmlTextWriter(writer);
//Render the control.
LinkWebControl1.RenderControl(output);
C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S |
909 |
//Display the HTML (and encode it properly so that
//it appears as text in the browser).
lblHtml.Text = "The HTML for LinkWebControl1 is<br /><blockquote>" + Server.HtmlEncode(writer.ToString()) + "</blockquote>";
Figure 27-6 shows the page with the control and its HTML.
Figure 27-6. Getting the HTML representation of a control
■Tip This technique isn’t just for debugging. You could also use it to simplify your rendering code. For example, you might find it easier to create and configure an HtmlTable control and then call its RenderControl() method, rather than write tags such as <table>, <td>, and <tr> directly to the output stream.
Dealing with Different Browsers
Because of the wide variation in the features supported by different browsers, it’s a challenge to create applications that work across all the browsers and still provide the best possible user experience. ASP.NET 2.0 provides a few features (some new and some old) that help you write the correct type of markup for different devices.
The HtmlTextWriter
First, ASP.NET makes a broad distinction in the type of markup that a client sees so that some clients get HTML 3.2, others get HTML 4.0, and others get XHTML 1.1. You might not even realize that this differentiation is taking place.
It all works through the HtmlTextWriter class, which has several derived classes. HtmlTextWriter itself is designed to write HTML 4.0 markup. But its derived classes are different—so, the Html32TextWriter writes HTML 3.2 markup for down-level clients, the ChtmlTextWriter can write compact HTML (cHTML) for mobile devices, and the XhtmlTextWriter writes XHTML 1.1. Because all these classes derive from HtmlTextWriter, you’re free to use the same basic set of HtmlTextWriter methods in your rendering code. However, the implementations of many of these methods differ, so depending on which object you get, the output might not be the same.
910 C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S
For example, the Html32TextWriter doesn’t support CSS (Cascading Style Sheets). This means that certain details that can’t be easily faked through other means (such as background colors) are simply ignored.
However, it all depends on how high-level your rendering code is. If you write raw HTML text using the HtmlTextWriter.Write() method, it doesn’t matter what text writer you’re using—none of them will change your text. That’s why it’s dangerous to use this approach. On the other hand, if you use the HtmlTextWriter.RenderBeginTag() method, different text writers may substitute another tag.
For example, if you use this rendering code:
output.RenderBeginTag(HtmlTextWriterTag.Div);
You expect this:
<div>
But here’s the result you’ll see with the Html32TextWriter (assuming Html32TextWriter.ShouldPerformDivTableSubstitution is true):
<table cellpadding="0" cellspacing="0" border="0" width="100%"><tr><td>
On the other hand, if you use this code, your rendered output is completely inflexible and never changes:
output.Write("<div>");
Similarly, if you derive from WebControl to get automatic support for style properties, this support is implemented differently depending on the renderer.
■Tip You can try different rendering behaviors by creating a console application that creates the appropriate text writer and uses it directly.
Browser Detection
So, how does ASP.NET decide which type of text writer suits a particular client? It’s all based on the user-agent string that the client supplies when it makes a request. ASP.NET tries to match this string against a large catalog of known browsers. You can find this catalog in c:\[WinDir]\Microsoft.NET\ Framework\[Version]\Config\Browsers. There you’ll see a number of .browser files. Each one is an XML file that maps a user-agent string to a set of capabilities and a text writer.
Every .browser file has this basic structure:
<browsers>
<browser id="..." parentID="..."> <identification>
<!-- Here is one regular expression that attempts to match the user-agent string.
There may also be multiple nonmatches, which disqualify user-agent strings that otherwise match the desired pattern. -->
<userAgent match="..." /> <userAgent nonMatch="..." />
</identification>
<capabilities>
<!-- Assuming the user-agent string matches, here are the capabilities ASP.NET should assume that the client has. -->
</capabilities>
C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S |
911 |
<controlAdapters>
<!-- For this client, some controls may need nondefault rendering of specific controls. This is made possible through adapters. Here is a list of all the control-specific adapters ASP.NET should use. -->
</controlAdapters>
</browser>
<!-- More browsers can be defined here. --> </browsers>
Further complicating the model is that you can create subcategories of browsers. To do this, the <browser> element includes the parentID attribute, which refers to another <browser> definition from which it should inherit settings.
For example, if you look at the opera.browser file, you’ll find information like this:
<browser id="Opera" parentID="Default"> <identification>
<userAgent match=
"Opera[ /](?'version'(?'major'\d+)(?'minor'\.\d+)(?'letters'\w*))" /> </identification>
<capabilities> |
|
<capability name="browser" |
value="Opera" /> |
<capability name="cookies" |
value="true" /> |
<capability name="css1" |
value="true" /> |
<capability name="css2" |
value="true" /> |
<capability name="ecmascriptversion" |
value="1.1" /> |
<capability name="frames" |
value="true" /> |
<capability name="javascript" |
value="true" /> |
...
<capability name="tagwriter" value="System.Web.UI.HtmlTextWriter" /> </capabilities>
<controlAdapters>
<adapter controlType="System.Web.UI.WebControls.CheckBox" adapterType= "System.Web.UI.WebControls.Adapters.HideDisabledControlAdapter"/> <adapter controlType="System.Web.UI.WebControls.RadioButton" adapterType= "System.Web.UI.WebControls.Adapters.HideDisabledControlAdapter"/> <adapter controlType="System.Web.UI.WebControls.Menu" adapterType= "System.Web.UI.WebControls.Adapters.MenuAdapter"/>
</controlAdapters>
</browser>
Here the Opera browser is given a set of basic settings and specifically associated with the HtmlTextWriter for HTML 4.0 rendering. In addition, several control adapters are defined to give Opera-specific rendering for these elements (more on that in the “Adaptive Rendering” section).
You probably think this is a somewhat brittle system—and unfortunately, it is. You have no guarantee that a browser won’t appear with a browser string that doesn’t match any of the known patterns or that a browser won’t submit the wrong string. However, this is a necessary compromise in the loosely coupled world of the Web, and the ASP.NET team has worked hard to make sure the browser information that ships with ASP.NET 2.0 is much more reliable and up-to-date than the information with ASP.NET 1.1. You’re also free to customize the browser presets completely or even add new definitions for different user-agent strings.
912 C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S
Browser Properties
You can detect the current browser configuration using the Browser property of the HttpRequest object, which returns a reference to an HttpBrowserCapabilities object. (You can also get the useragent string from the UserAgent property.) When a client makes an HTTP request, an HttpBrowserCapabilities object is created and filled with information about the capabilities of the browser based on the corresponding .browser file. The information provided in the HttpBrowserCapabilities class includes the kind of browser and its version, whether scripting support is available on the client side, and so on. By detecting the capabilities of the browser, you can choose to customize your output to provide different behaviors on different browsers. This way, you can fully exploit the potential capabilities of up-level clients, without breaking down-level clients.
Table 27-2 summarizes the properties of HttpBrowserCapabilities class.
Table 27-2. HttpBrowserCapabilities Properties
Property |
Description |
Browser |
Gets the browser string that was sent with the request in the user-agent |
|
header. |
MajorVersion |
Gets the major version number of the client browser. (For example, this |
|
returns 4 for version 4.5.) |
MinorVersion |
Gets the minor version number of the client browser. (For example, this |
|
returns 5 for version 4.5.) |
Type |
Gets the name and the major version number of the client browser. |
Version |
Gets the full version number of the client browser. |
Beta |
Returns true if the client browser is a beta release. |
AOL |
Returns true if the client is an AOL (America Online) browser. |
Platform |
Provides the name of the operating system platform that the client uses. |
Win16 |
Returns true if the client is a Win16-based computer. |
Win32 |
Returns true if the client is a Win32-based computer. |
ClrVersion |
Provides the highest version number of the .NET CLR installed on the |
|
client computer. You can also use the GetClrVersions() method to retrieve |
|
information about all the installed CLR versions. This setting is significant |
|
only if you have embedded .NET Windows Forms controls in your web |
|
page. Client browsers don’t need the CLR to run ordinary ASP.NET |
|
web pages. |
ActiveXControls |
Returns true if the client browser supports ActiveX controls. |
BackgroundSounds |
Returns true if the client browser supports background sounds. |
Cookies |
Returns true if the client browser supports cookies. |
Frames |
Returns true if the client browser supports frames. |
Tables |
Returns true if the client browser supports HTML tables. |
JavaScript |
Indicates whether the client browser supports JavaScript. |
VBScript |
Returns true if the client browser supports VBScript. |
JavaApplets |
Returns true if the client browser supports embedded Java applets. |
EcmaScriptVersion |
Gets the version number of ECMA script that the client browser supports. |
MSDomVersion |
Gets the version of Microsoft HTML DOM that the client browser |
|
supports. |
Crawler |
Returns true if the client browser is a web crawler search engine. |
|
|
C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S |
913 |
The following code snippet shows how you could dynamically tailor rendered output based on the capabilities of the requesting browser. In this example, the code simply outputs different strings to indicate what it has detected. In a more realistic example, you would render different HTML or JavaScript based on the same information.
protected override void RenderContents(HtmlTextWriter writer)
{
base.RenderContents(writer);
if (Page.Request.Browser.JavaScript)
{
writer.Write("<i>You support JavaScript.</i><br>");
}
if (Page.Request.Browser.Browser == "IE")
{
writer.Write("<i>Output configured for IE.</i><br>");
}
else if (Page.Request.Browser.Browser == "Netscape")
{
writer.Write("<i>Output configured for Netscape.</i><br>");
}
}
The HttpBrowserCapabilities class has one glaring limitation—it’s limited to evaluating the expected built-in functionality of the browser. It does not evaluate the current state of a browser’s functionality. For example, imagine you are evaluating the client-side JavaScript support provided by the browser. If the requesting browser is Internet Explorer 5.5, this will return true since the browser supports client-side JavaScript support. However, if the user has the scripting capabilities turned off, the JavaScript property still returns true. In other words, you don’t learn what the browser is capable of doing, just what it should be capable of doing. In fact, all ASP.NET really does is read the user-agent information that’s passed from the browser to the server during the request and compare this string against the predefined user-agent information in the machine.config file. It’s the machine.config file that lists the corresponding browser capabilities, such as whether the browser supports scripting, styles, frames, and so on. Unfortunately, the client just doesn’t send any information about how the browser is configured.
This situation leaves you with two options. You can rely on the HttpBrowserCapabilities class to tell you whether certain browser features should be available and base your programming logic on that information. In this case, you may need to tolerate the occasional error. If you need a more robust approach, you need to write your own code to actually test the support for the features you need. For example, with cookies you could (over two web pages) attempt to set a cookie and then attempt to read it. If the second test doesn’t succeed, cookie support isn’t enabled. You could use similar workarounds to check for other features such as JavaScript support. For example, you could add a piece of JavaScript code to the page that writes to a hidden form variable and then check it on the server. These steps are awkward and messy, but they’re the only way to be absolutely certain of specific browser features. Unfortunately, when creating custom controls, you usually don’t have the luxury of performing these tests.
Table 27-3 shows how some common browsers stack up with the HttpBrowserCapabilities class.
914 C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S
Table 27-3. HttpBrowserCapabilities Properties for Common Browsers
Browser |
EcmaScriptVersion |
MSDomVersion |
W3CDomVersion |
ClrVersion |
IE6+ |
1.2 |
6.0 |
1.0 |
1.0.3705 |
NS6+ |
1.5 |
0.0 |
1.0 |
0.0 |
Opera6+ |
1.3 |
0.0 |
1.0 |
0.0 |
|
|
|
|
|
Adaptive Rendering
In ASP.NET 1.x, every control needed to have the built-in smarts to tailor itself for different browsers. If you needed to support different devices and different types of markup, you needed to develop an entirely separate control.
ASP.NET 2.0 improves this situation dramatically with a new adaptive rendering model that’s based on control adapters. This model makes it possible to create a single control that can be adapted for multiple types of devices. Best of all, because of the separation between controls and control adapters, third-party developers can write adapters for existing controls, allowing them to work with other platforms.
You can link any control to an adapter through the .browser file. For example, you could create a FirefoxSlideMenuAdapter that changes the rendered code for your SlideMenu control so that it better works with Firefox. You would then edit the mozilla.browser file to specifically indicate that this adapter should be used for your control with all Firefox browsers.
The control adapter works by plugging into the rendering process. ASP.NET calls the adapter at each state of the web control’s life cycle, which allows the adapter to adjust the rendering process and handle other details, such as device-specific view state logic.
To create an adapter, derive a new class from System.Web.UI.Adapters.ControlAdapter (if your custom control derives from Control) or System.Web.UI.WebControls.Adapters.WebControlAdapter (if your custom control derives from WebControl). You can then implement the functionality you want by overriding methods. Each method corresponds to a method in the custom control class, and when you override the method in a control adapter, the control adapter method is used instead of the control method.
For example, in the ControlAdapter you can override methods such as OnInit(),Render(), and RenderChildren(). In the WebControlAdapter you can also override RenderBeginTag(), RenderEndTag(), and RenderContents(). Here’s an example:
public class LinkControlAdapter : ControlAdapter
{
//Replace the ordinary rendering logic so it uses different color
//and doesn't change the font.
protected override void Render(HtmlTextWriter output)
{
//Specify the URL for the upcoming anchor tag. output.AddAttribute(HtmlTextWriterAttribute.Href,
"http://www.apress.com");
//Add the style attributes. output.AddStyleAttribute(HtmlTextWriterStyle.Color, "Red");
//Create the anchor tag. output.RenderBeginTag(HtmlTextWriterTag.A); output.Write("Click to visit Apress"); output.RenderEndTag();
}
}
C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S |
915 |
If you want to perform the normal control rendering and add your custom rendering steps, simply call the base ControlAdapter.Render() implementation, which calls the Render() method of the corresponding control. This technique works for all the rendering methods.
protected override void Render(HtmlTextWriter output)
{
//(Custom rendering code here.) base.Render(output);
//(More custom rendering code here.)
}
You can also access the linked control through the ControlAdapter.Control property if you need to examine additional details.
The adaptive rendering model is a major shift in ASP.NET 2.0, and it allows endlessly customizable controls and cross-device integration. You can do quite a bit more with a custom control adapter. For example, you could hook to events in the underlying control and then use that to customize event behavior on different devices.
■Note The implications of the adaptive rendering model haven’t appeared yet, because it’s quite new. Originally, Microsoft planned to remove all its mobile controls and allow the standard web controls to support mobile devices through specialized adapters. Unfortunately, this feature was cut during the beta cycle because of time constraints.
Control State and Events
ASP.NET uses web controls to create an object-oriented layer of abstraction over the lower-level details of HTML and HTTP. Two cornerstones of this abstraction are view state (the mechanism that lets you store information between requests) and postback (the technique wherein a web page posts back to the same URL with a collection of form data). To create realistic server controls, you need to know how to create classes that plug into both of these parts of the web-page infrastructure.
View State
Controls need to store information in state just like your web pages. Fortunately, all controls provide a ViewState property that you can use to store and retrieve information just as you do with a web page. You’ll need to use the ViewState collection to restore private information after a postback.
A common design pattern with web controls is to access the ViewState collection in your property procedures. For example, consider the LinkWebControl presented earlier. Currently, this control doesn’t use view state, which means that if you change its Text and HyperLink properties programmatically, the changes will be lost in subsequent postbacks. (This isn’t true of the style properties such as Font, ForeColor, and BackColor, which are stored in view state automatically.) To change the LinkWebControl to ensure that state information is retained for the Text and HyperLink properties, you need to rewrite the property procedure code, as shown here:
public string Text
{
get {return (string)ViewState["Text"];} set {ViewState["Text"] = value;}
}
public string HyperLink
{
C H A P T E R 2 7 ■ C U S TO M S E R V E R C O N T R O L S |
917 |
Finally, keep in mind that you can’t assume data is in the ViewState collection. If you try to retrieve an item that doesn’t exist, you’ll run into a NullReferenceException. To prevent this problem, you should check for null values or set default view state information in the OnInit() method or the custom control constructor. For example, the LinkWebControl won’t run into null references because it uses OnInit() to set initial view state values.
■Note Although the WebControl provides a ViewState property, it doesn’t provide properties such as Cache, Session, and Application. However, if you need to use these objects to store or retrieve data, you can access them through the static HttpContext.Current property.
Occasionally, you might want more flexibility to customize how view state information is stored. You can take control by overriding the LoadViewState() and SaveViewState() methods. The SaveViewState() method is always called before a control is rendered to HTML. You can return a single serializable object from this method, which will be stored in view state. Similarly, the LoadViewState() is called when your control is re-created on subsequent postbacks. You receive the object you stored as a parameter, and you can now use it to configure control properties. In most simple controls, you’ll have no reason to override these methods. However, sometimes it does become useful, such as when you’ve developed a more compact way of storing multiple pieces of information in view state using a single object or when you’re deriving from an existing control and you want to prevent it from saving its state. You also need this method when you’re managing how a complex control saves the state of nested child controls. You’ll see an example of this last technique at the end of this chapter. For more information about advanced control programming, you may want to consult a dedicated book about ASP.NET control programming, such as Developing Microsoft ASP.NET Server Controls and Components (Microsoft Press, 2002).
Control State
ASP.NET 2.0 adds a new feature called control state. Technically, control state works in the same way as view state—it stores serializable information that’s stuffed into a hidden field when the page is rendered. In fact, ASP.NET puts the view state information and the control state information into the same hidden field. The difference is that control state is not affected by the EnableViewState property. Even if this is set to false, your control can still store and retrieve information from control state.
■Note The LinkWebControl doesn’t require control state. If the developer sets EnableViewState to true, it’s probably because the developer expects to set the HyperLink and Text properties in every postback.
Because control state cannot be disabled, you should carefully restrict the amount of information you store. Usually, it should be limited to something critical such as a current page index or a data key value. To use control state, you must begin by overriding the OnInit() method and call Page.RegisterRequiresControlState() to signal that your control needs to access control state.
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
Page.RegisterRequiresControlState(this);
}
