Extreme ASP.NET

Control Adapters

Fritz Onion

Code download available at:ExtremeASPNET2006_10.exe(187 KB)


Building a Control Adapter
Browser Recognition
CSS-Friendly Adapters
Using the Control Adapter in Your App
Wrap Up

The release of ASP.NET 2.0 introduces an alternate way to specify renderings of controls through a technology called control adapters. If you have heard of control adapters at all, it is probably in the context of providing alternate renderings of controls for mobile devices. They actually serve a more general purpose of providing a way of completely changing the rendering of a control based on the browser type of the client, which turns out to be useful in a number of situations.

Mobile device rendering is still the most obvious application of control adapters. For example, for a mobile phone, the control contents should render as WML instead of HTML. Ideally, developers would still use familiar ASP.NET controls, like Calendar and GridView, but when they are accessed by a WML device, a control adapter would kick in and provide an alternate rendering that would work within the constraints of the client device. With an updated collection of control adapters released as new devices come onto the market, developers would only have to update their list of control adapters, and suddenly their Web content would be available to these new devices without changing any of their actual code or page content.

But, as mobile devices continue to proliferate at what feels like an exponential rate—each with different form factors and rendering capabilities—trying to create mappings from standard ASP.NET controls to all devices becomes overwhelming. Even more problematic is that developers building sites that target screens with 1024×768 resolution or higher make assumptions that just can't be adapted to mobile devices without significant intervention by the developer. If you want a site that works well with a number of different devices today, you need to build a separate UI for each distinct device to make users happy. The Mobile Control Toolkit introduced as an add-on to ASP.NET 1.x and carried forward to 2.0 can help, but the goal of building sites that render successfully to a multitude of devices using control adapters has not come to fruition.

The good news is that the ASP.NET team found another compelling use for this technology in the form of the CSS Control Adapter Toolkit, which is available for download at www.asp.net/cssadapters. Instead of creating alternate renderings of controls for specific devices, this toolkit defines a number of control adapters that change the default table-based renderings of several controls (like Menu, TreeView, and FormView) to eschew tables completely and render using <ul>, <div>, and <span> elements with associated styles defined in CSS stylesheets (a common requirement for site designs today). In this column I'll introduce the control adapter architecture and explain how it can be used to completely change control renderings based on the User Agent string. I'll then discuss the CSS Control Adapter Toolkit and how you can integrate its features into your sites today.

Building a Control Adapter

At their core, control adapters are simply a way of providing different renderings for controls without actually modifying the controls themselves. Because control adapters are designed to provide alternate renderings for different clients, you specify control adapter mappings in a .browser file, which is where associations between User Agent strings and browser capabilities are defined. The control adapter class itself must inherit from the System.Web.UI.Adapters.ControlAdapter, which is an abstract base class that looks much like the Control base class, with events for Init, Load, PreRender, and Unload, as well as a virtual Render method.

Internally, when a control is rendering, the Control base class will first check to see if there is a control adapter currently associated with the control. If there is, it will invoke the Render method of the adapter; if not, it calls the standard Render method of the control. Figure 1 shows the control adapter architecture.

Figure 1 Control Adapter Architecture

Figure 1** Control Adapter Architecture **

To build your own control adapter, you start by creating a new class that inherits from the ControlAdapter base class, or more commonly from one of its derivatives—WebControlAdapter. However, the control adapter class you derive from depends on the control you're building the adapter for. If it is one of the built-in WebControls, you should select the WebControlAdapter as it adds virtual RenderBeginTag, RenderEndTag, and RenderContents methods to more closely mirror how WebControls render. There are also more specific control adapters defined for data-bound controls and for the Menu control, each of which provides more virtual methods to access and modify features of the controls with which it is associated.

To show you how to build a control adapter to change the rendering of a particular control, I'll create a control adapter for the BulletedList control named BulletedListControlAdapter. This simple control renders a collection of items as an ordered or unordered list (<ol> or <ul>). To illustrate how flexible the adapter model is, I'll build the control adapter to completely change the rendering to be table-based instead. Since the BulletedList is derived from WebControl, I'll start by creating the BulletedListControlAdapter class as a derivative of WebControlAdapter. Next, I'll override the RenderBeginTag and RenderEndTag methods to render the opening and closing tags of the table. Finally, I'll override RenderContents to iterate across the items of the BulletedList control and render each item as a single-cell row in the table prefixed with an asterisk. The full control adapter implementation is shown in Figure 2.

Figure 2 The Control Adapter

namespace MsdnMagazine { public class BulletedListControlAdapter : WebControlAdapter { protected override void RenderBeginTag(HtmlTextWriter writer) { writer.WriteLine(); writer.WriteBeginTag("table"); writer.Write(HtmlTextWriter.TagRightChar); writer.Indent++; } protected override void RenderEndTag(HtmlTextWriter writer) { writer.WriteEndTag("table"); writer.Indent--; writer.WriteLine(); } protected override void RenderContents(HtmlTextWriter writer) { writer.Indent++; BulletedList bl = Control as BulletedList; if (bl != null) { foreach (ListItem i in bl.Items) { writer.WriteLine(); writer.WriteBeginTag("tr"); writer.Write(HtmlTextWriter.TagRightChar); writer.WriteLine(); writer.Indent++; writer.WriteBeginTag("td"); writer.Write(HtmlTextWriter.TagRightChar); writer.WriteLine(); writer.Indent++; writer.Write("*"); writer.Write(HtmlTextWriter.SpaceChar); writer.Write(i.Text); writer.WriteLine(); writer.Indent--; writer.WriteEndTag("td"); writer.WriteLine(); writer.Indent--; writer.WriteEndTag("tr"); writer.WriteLine(); } } writer.Indent--; } } }

The next step is to associate the BulletedListControlAdapter class with the BulletedList control. As I mentioned earlier, this is done by specifying which subset of browsers you want to target. For now, I'll simply associate all browsers with this adapter by specifying a refID attribute value of Default in the browser element of my .browser file. (In the next section I'll discuss how you can create more granular associations.) In this sample, I created a new file named MyAdapters.browser and placed it in the App_Browsers directory of my application:

<!-- File: MyAdapters.browser --> <browsers> <browser refID="Default"> <controlAdapters> <adapter controlType="System.Web.UI.WebControls.BulletedList" adapterType="MsdnMagazine.BulletedListControlAdapter" /> </controlAdapters> </browser> </browsers>

Any additional browser definitions or control adapters defined in this local file will be added to the collection of browsers and adapters already defined in the system browser configuration files.

The last step is to actually place an instance of the BulletedList control with some items on a page, as shown in Figure 3.

Figure 3 BulletedList Control a Page

<%-- File: Default.aspx --%> <%@ Page Language="C#" %> <html xmlns="https://www.w3.org/1999/xhtml" > <body> <form id="form1" runat="server"> <div> <asp:BulletedList ID="_bulletedList" runat="server"> <asp:ListItem Value="1">Item 1</asp:ListItem> <asp:ListItem Value="2">Item 2</asp:ListItem> <asp:ListItem Value="3">Item 3</asp:ListItem> <asp:ListItem Value="4">Item 4</asp:ListItem> </asp:BulletedList> </div> </form> </body> </html>

If you first run the page without the control adapter in place (by removing the .browser file or commenting out the control adapter mapping in the .browser file using standard XML comment syntax), the BulletedList will render as expected, using a <ul> element with <li> subelements:

<ul id="_bulletedList"> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> <li>Item 4</li> </ul>

If you then enable the control adapter, the rendering process changes to the new table-based rendering defined in the custom adapter, with no changes at all to the actual .aspx page (see Figure 4).

Figure 4 Table-Based

<table> <tr> <td> * Item 1 </td> </tr> <tr> <td> * Item 2 </td> </tr> <tr> <td> * Item 3 </td> </tr> <tr> <td> * Item 4 </td> </tr> </table>

As you can see, it is reasonably simple to create an alternate rendering for a control using the control adapter architecture. There are, however, some issues that should be raised at this point. First, note that the simplified rendering of the BulletedList did not look at any of the attributes of the BulletedList control except its items collection. This includes the rather important BulletStyle attribute that determines what type of list to render (unordered, numeric, alphabetic, and so on). It is generally important to honor all of the behavioral attributes of a control for which you're writing a control adapter. On the other hand, it may or may not make sense to incorporate style attributes into your rendering, especially if the target device for which the control adapter is being built does not have any means of displaying a particular style (for example, BackColor on a black and white device). As you will see later, the CSS Control Adapters elect to ignore most style attributes of controls they render, and instead provide a set of CSS classes that can be modified to dictate appearance.

Second, the simplified example of the BulletedListControlAdapter is not very realistic because I didn't actually take advantage of the browser mapping to associate specific user agents with this adapter. Instead I mapped all browser types (by specifying Default in the refID attribute) to use this control adapter. This means that a site with this control adapter installed would never call the standard rendering of the BulletedListControl but would always display the rendering supplied by the control adapter. If the only purpose is to create a control that has a different rendering from an existing control, another approach would be to derive a new control class from the existing control and override the necessary methods to change the rendering. The potential advantage control adapters have over custom control derivatives is that they can be applied to a Web application with no modifications to any of the pages (or even the web.config file). Thus it may be compelling to use them even without browser-contingent rendering in situations where an application is already written and deployed, but a rendering change is needed for a particular control across the entire site.

Browser Recognition

As you've seen, the control adapters are mapped onto controls in a particular site by specifying a configuration element in a .browser file. In ASP.NET 2.0, the browser recognition capability was moved from the <browserCaps> element of machine.config and web.config to a collection of .browser files. The <browserCaps> element is still supported, but the preferred way to specify browser capabilities is in a .browser file located either in the local App_Browsers directory of your application or in the machine-wide %SYSTEM%\Microsoft.NET\Framework\v2.0.50727\Config\Browsers directory. The purpose of browser recognition files, in addition to associating control adapters, is to populate the HttpBrowserCapabilities class that a request can access through the Request.Browser property. Through this class you can find out the name of the requesting browser and whether it supports JavaScript, cookies, and other features. To populate this class, each .browser file contains one or more <browser> elements, each typically with a subelement called identification that contains a regular expression that is applied to the User Agent string or to a specific capability from a parent browser class for each request (with an option to specify an exclusion expression as well). If it matches, the capabilities listed for that browser are used to populate the HttpBrowserCapabilities class. For example, the browser definition for Internet Explorer (found in the machine-wide Browsers directory in the file ie.browser) looks like Figure 5.

Figure 5 Internet Explorer Browser Definition

<browsers> <browser id="IE" parentID="Mozilla"> <identification> <userAgent match="^Mozilla[^(]*\([C|c]ompatible;\s*MSIE (?’version’(?’major’\d+)(?’minor’\.\d+)(?’letters’\w*)) (?’extra’[^)]*)" /> <userAgent nonMatch="Opera|Go\.Web|Windows CE|EudoraWeb" /> </identification> <capabilities> <capability name="browser" value="IE" /> <capability name="extra" value="${extra}" /> <capability name="isColor" value="true" /> <capability name="letters" value="${letters}" /> ...

Figure 5 Browse Type Hierarchy

Figure 5** Browse Type Hierarchy **

Browsers are also grouped hierarchically using the parentID attribute on the <browser> element. In order for a <browser> element that has a parentID referencing another <browser> element to be applied, the regular expression conditions for the parent element must have succeeded. The root of the hierarchy is "Default", which matches all user agent strings and assumes that the browser has minimal capabilities. All other browser types have either Default or one of its children as a parent. The predefined set of browser definitions located in the machine-wide Browsers directory contains a useful categorization of browser types, with associated names that can be used to identify subsets of browsers. For example, if you wanted to apply a control adapter to all requests made from Internet Explorer versions 6.0 and higher, you could use the "IE6to9" browser name in the refId attribute of your local browser element. Figure 6 shows the hierarchy of browser types defined in the system .browser files (excluding mobile devices, which constitute another hierarchy at least twice this size).

You can use any of these browser names in the refID attribute when assigning control adapters. As an example, the CSS Control Adapter Toolkit enables adapters for IE6to9, MozillaFirefox, Opera8to9, and Safari, which covers the subset of browsers that support the CSS features needed by the adapters defined in the toolkit. Other browsers will revert to the standard control renderings.

CSS-Friendly Adapters

In April of 2006, Microsoft released a set of samples entitled CSS-Friendly ASP.NET 2.0 Control Adapters Beta 1.1, a suite of control adapters that change the rendering of several common ASP.NET controls to take better advantage of CSS. The set was designed primarily because developers using ASP.NET needed to specify all of the style attributes, including layout, for their controls using CSS stylesheets instead of server-side attributes. The initial release includes adapters for the Menu, TreeView, DetailsView, FormView, and DataList controls.

Each control adapter included in this collection ships with two accompanying stylesheets, one with a core set of styles defined for the adapter, which should rarely need modification on your part, and a second containing a set of styles you'll almost certainly want to modify to manage the appearance of the control in your application. For example, the MenuAdapter sample comes with a Menu.css file and a MenuExample.css file; the first file contains core styles you would rarely change and the second contains style attributes like background colors, fonts, and offsets that change the appearance of menus rendered with the control adapter. All of the commonly changed style attributes are contained within a top-level CSS class so you can create multiple sets of styles, each under a different class. You specify which set of styles you want a particular control to use with the CssSelectorClass attribute, which is looked at by the control adapter and used to map CSS classes onto the rendered HTML elements for that control. For example, the MenuAdapter defines a collection of styles under the top-level PrettyMenu CSS class in MenuExample.css. To apply this set of styles to a menu when using the MenuAdapter, you would specify PrettyMenu as the CssSelectorClass, as shown here:

<asp:Menu ID="_mainMenu" runat="server" DataSourceID="_siteMapDataSource" CssSelectorClass="PrettyMenu" SkinID="MainNav" Orientation="Horizontal" />

Once the adapter for one of these controls is installed, all instances of the control whose rendering is modified by that adapter will render using the style attributes defined in your .css file and will ignore any style attributes defined directly for the server-side control in its markup. The Visual Studio® designer will not reflect the control adapter rendering, so once you install an adapter, you can no longer set style attributes locally on controls being rendered as adapters and expect them to be honored.

Using the Control Adapter in Your App

To give you an idea of how you would incorporate these control adapters into your ASP.NET application, the following steps describe how you could add the MenuAdapter to an existing Web application. Note that the details of some of these steps may change slightly with future releases of the toolkit samples, but the core elements will undoubtedly remain the same.

The current toolkit ships as a Visual Studio Web project template, so the first step is to create a new Web site using the CSS Friendly ASP.NET Control Adapters template. The resulting project will contain a working project with examples of each control adapter.

Next, copy the MenuAdapter.cs and Utility.cs source code files (or .vb files, if you selected a Visual Basic® project) from the generated project's App_Code directory to your own Web application's App_Code directory. Note that you could also compile these files into an assembly and deploy them globally in the Global Assembly Cache (GAC ) if you want to use them across multiple projects.

Now copy the CSSFriendlyAdapters.browser file from the App_Browsers directory of the generated project into your own Web application's App_Browsers directory. Modify the CSSFriendlyAdapters.browser file to remove references to adapters you aren't using (TreeView, DetailsView, FormView, and DataList in this case), then copy the Basic and Enhanced directories from the generated project's App_Themes directory to your Web application's App_Themes directory. These will contain the .css files needed by the MenuAdapter, as well as supporting graphics files. If you are already using themes in your project, you can also just copy the contents of one of these directories (choose which appearance you prefer) to an existing theme directory that will be applied to all pages containing a Menu control.

If you want, you can now remove .css files from the theme directories you will not be using (for example FormView.css and FormViewExample.css in this case).

Next you'll need to copy the JavaScript directory from the generated project to your Web applications root. The MenuAdapter class relies on the AdapterUtils.js and MenuAdapter.js files being in this directory. Then subscribe all pages with a Menu control to the Basic or Enhanced theme (you may want to do this globally in web.config, especially if you are using Master Pages). This is necessary so that each page with a Menu control has a link to the needed .css files in the theme directory, which themes take care of automatically.

Finally, annotate each of your Menu controls with a CssSelectorClass attribute referencing the top-level CSS class you'd like each control to use for its appearance and layout. As noted, the one that is provided as a sample is called PrettyMenu.

All of your Menu controls should now render using <ul> elements instead of tables, and their appearance will be defined by the CSS styles contained in MenuExample.css. At this point you may want to create a new stylesheet class modeled after MenuExample.css with your own style definitions and then modify the CssSelectorClass to point to the new set of styles.

Wrap Up

With the control adapter architecture, it is possible to completely change the rendering of individual controls based on the type of browser that is being used to make the request, without any modifications to the controls themselves. Although this infrastructure was put in place initially to support alternate renderings for mobile devices, the CSS Control Adapter Toolkit demonstrates a completely different use of the technology, which you as an ASP.NET developer should find quite useful when constructing sites that need to have all styles defined centrally in CSS files.

Send your questions and comments for Fritz to xtrmasp@microsoft.com.

Fritz Onion is a cofounder of Pluralsight, a Microsoft .NET training provider, where he heads the Web development curriculum. Fritz is the author of Essential ASP.NET (Addison Wesley, 2003) and the upcoming Essential ASP.NET 2.0 (Addison Wesley, 2006). Reach him at pluralsight.com/fritz.