One Site, Many Faces

 

Eli Robillard
January 2004

Applies to:
    Microsoft® ASP.NET

Summary: Learn how to build and maintain a site with many interfaces, including XML, HTML, RSS, print-only, and mobile feeds. The core of the approach is a switchboard that handles all incoming URL requests, and locates and serves appropriate content accordingly. The method may be extended for other uses, including providing helpful error pages (after empty searches or 404s) directing the user to related content on your site and elsewhere. (22 printed pages)

Download MultipleOutputFormatsSample.msi.

Contents

Introduction
Planning
Implementation

Introduction

It is popular to build Web sites using page templates with content retrieved from a data store such as Microsoft® SQL Server™ or a set of XML files. The strategy provides a consistent interface to users and makes content management, well, manageable. On sites with many contributors some sort of workflow solution is a necessity, and most use a central data store. A big advantage of a central data store is the ability to construct new views, or "templates."

Page template solutions normally rely upon a "switchboard" process that takes parameters that identify the content to retrieve and perhaps the output mode to use, like so:

  
    http://mysite.com/switchboard.aspx?article=12345&mode=print

  

Returning pages with an .aspx extension is fine for HTML viewed with a browser, but is limiting if you need to deliver content in formats such as RSS, XML, rich text (.rtf), or Microsoft Excel (.xls). For example, while you can change the MIME header to "application/ms-excel" to tell the browser that Excel content is on the way, the "Open or Save" dialog will default to the switchboard.aspx filename and extension. If the user does not remember to rename the file, it will be hard to tell that c:\temp\switchboard.aspx is an Excel file containing a budget summary.

In fact, most machines won't be configured to associate ".aspx" with a browser even for offline HTML viewing. Clearly, relying on the user to rename every file saved or downloaded is not recommended.

There is a better way.

This article will show you how to build a site that applies a template to the content served according to the file name extension requested. If you write templates for RTF or Excel output, you will be able to request this content simply by asking for "myContent.rtf" or "myContent.xls" accordingly. It will also demonstrate how to use the path as a parameter to further distinguish between HTML output for browser, mobile or print output ("/myContent.htm" vs. "/pda/myContent.htm" vs. /"print/myContent.htm").

From there you will be able to build a site that can respond appropriately to a request for any of these:

.../12345.aspx Standard ASP.NET page extension
.../12345.htm Not that you would want to hide the fact you use .NET!
.../12345.rtf Rich Text Format—opens in Microsoft Word
.../print/12345.aspx Printable version
.../pda/12345.aspx Mobile device version
.../rss/12345.xml RSS feed
.../xml/12345.xml XML feed

In keeping with the "one site, many faces" theme, the framework constructed will be named "Sybil" after the famous book on multiple personalities.

Ahead you will find two sections: Planning and Implementation. The Planning section discusses the forest of choices available to you and how to find the most efficient path through. A case study is presented to show how effective planning leads to an efficient solution. If you're only interested in the code, then jump straight to Implementation.

Along the way, this article also discusses the differences in both performance and maintenance between implementing the switchboard in Global.asax, as an HttpModule, and as an HttpHandler. Source code is provided for all three approaches.

Planning

It helps to break the planning process into four steps: identification, entity design, system design, and context. These form a progression where the focus starts by identifying individual data elements and their sources, these are then organized into entities, as the entities relate and communicate they form a system, and finally the system is considered in the context of everything else.

Applied to this article, these steps become:

  1. Identification: identify the goals of your site's users, the data available to display to them, and the data formats that will satisfy their goals.
  2. Entity design: define the sources from where data elements are retrieved, any transformation that needs to happen, and rough out the data formats or templates to meet user needs.
  3. System design: when entities communicate they form a system. In this step, plan the methods and parameters that define how entities communicate.
  4. Context: considering context brings us full circle; it is where you double-check that stakeholder goals from Step 1 are met by the system. Remembering that stakeholders include the organization as well as individuals, ask whether the system is consistent with the goals of the organization, and whether the design is realistic to implement and maintain by the organization.

Identification

Three things are identified: stakeholder goals, available data inputs, and appropriate data to output. As a set, these define the scope of the application.

Stakeholder goals

First, identify the people or groups of people you plan to serve ("stakeholders") and their goals for using your application. You can define these equally well as "user goals," "use cases," or "problems to be solved." These goals will inform your decisions throughout design and implementation so be specific. The clarity with which you define your goals will determine the clarity of your path forward. If you find yourself unable to answer questions later in the design process, go back and clarify the goals.

Note that if you are on a design team, the team needs to be in full agreement about the goals. Minor aberrations here will later turn into major warps. When a team agrees on a written set of clearly defined goals, conflicts disappear. If conflicts arise from varying interpretations of the goals, go back and clarify the goals.

In the present case, identifying stakeholders and their goals will determine which views of the data to provide and in which formats (or devices) to provide these views.

Case Study: As an example, consider a fictitious Norwegian-owned publisher of financial news called "Fjorbes Online." Fjorbes has writers, an editor, and a subscriber base that includes brokers and investors (subscribers are divided into distinct categories for optimal advertising and content delivery). Fjorbes' list of stakeholders, goals and consumption models might look like this:

Stakeholder Analysis

Stakeholder: Writer / Columnist

Goals: To submit articles, and to read his own articles and those of certain fellow writers.

Consumption methods: (a) Use a browser to submit articles, and (b) an RSS client to read articles.

Stakeholder: Editor

Goal: To edit and approve content submitted by writers before that content is fed to brokers and investors.

Consumption methods: Requires interactive formats that allow forms submission to express approval or rejection. Would really like to edit submissions in Microsoft Word. Acceptable solutions: (a) HTML with a Web browser, (b) HTML with a mobile PDA, (c) any format that opens in Word.

Stakeholder: Professional Broker

Goals: To read a daily summary of news about the brokerage industry, plus specific news feeds on areas of interest like fund management, the mortgage industry, or foreign markets.

Consumption methods: (a) View HTML on the Web site, (b) read RSS feeds on specific topics with a news client, (c) receive HTML or plain-text e-mail, and (d) receive feeds on a mobile PDA.

Stakeholder: Private Investor

Goals: The investor wants to track articles that mention certain companies and articles pertaining to particular industries. When studying a new sector for investment, the investor will print annual report summaries for certain companies to compare offline. The investor also wants to read new articles by favorite columnists.

Consumption methods: (a) Use an RSS client to read RSS feeds by company, industry, or writer, (b) view HTML articles on the Web site, and (c) print articles from the Web site for offline comparison.

Stakeholder: Financial News Crawler

Goals: To scrape content from financial news sites for display in an aggregate financial news portal.

Consumption methods: (a) RSS feeds, and (b) HTML scraping.

Data elements available

The next step is to identify all the possible data elements available from all your data providers, regardless of which "domain," "entity," or "stakeholder" they belong to. Go ahead and brainstorm, throw in the kitchen sink; there are no wrong answers at this stage.

The only rule is to be specific. "Data collected in logs" does not describe either the data or a specific source, but citing "Browser IP address, ArticleID, and Date-Time from HTTP Logs," makes it easier to pick out useful relationships with other data.

Data Available to the Fjorbes Website Developer

From Writers: Name, address, e-mail address, short biography (used in article footer), long biography (perhaps a text resume)

From Subscribers: Name, address, e-mail address, job title, categories of interest, content delivery mechanism (such as HTML, RSS, or PDA), subscription start date, subscription expiration date, login history (date-time, IP address, and success or failure of attempt)

From Articles (provided by writers): Article ID, title, writer, category, keywords, body. Data provided by editors: approval to publish.

From the Web site: HTTP logs (including IP address, date-time and URI, from which Article IDs or RSS feeds may be identified), and custom logs (article approval audit record including Article ID, Editor ID, Date-time, and change made)

Data elements to output

The final step is to identify the data to output. From all the available data, focus on the pieces required to meet specific stakeholder goals.

If a goal cannot be met by the available data, or data is prohibitively expensive, then either work out an alternative goal with the stakeholder, or strike the entire goal and its required elements from this phase. Solutions that do not meet their intended goals are useless, and resources poured into unattainable goals are wasted. Focus your resources on results.

Case Study Example: The goal of this article is to help you build a site that emits content in several formats, and not to build a user preference or subscription management engine. Therefore our scope will not include the subscriber's selection of news feeds, or the management of the subscriber lists. The functions of subscriber preferences, and subscription maintenance would be best designed as separate systems by a Web forms designer.

Stakeholder goals suggest new data elements. If the publisher's goal is to protect the revenue model, and revenue is based on a both ads displayed at the Web site and a paid-subscription model which shields the user from ads, then RSS or e-mail feeds might only provide abstracts of the complete articles, and require that users open an authenticated session with the HTML site to view complete articles. This would require a new data element called "Article:abstract" in additional to the usual "Article:body."

Based on all user goals, it is clear that the site should emit content as HTML, RSS, plain text and RTF (which is editable by and usually associated with Word), and that HTML pages might be consumed by desktop browsers, mobile devices, or print devices. A raw XML feed will be built as well, though it will be for purposes of demonstration and is not a practical need of Fjorbes site users.

It is also useful to see what is not needed. No proprietary or industry-specific XML schema are necessary, nor is support for application-specific formats like Microsoft Excel, Microsoft Access, or Adobe Acrobat.

Restrictions on output

Two factors may limit your choices: control of the IIS Server (affects both ASP.NET 1.0 and 1.1), and company policy.

If you are not able to change the configuration of the IIS Server you won't be able to add extension-application mappings for extensions not already processed by the .NET Framework, leaving you with only one real option: .aspx. You can serve printable and mobile versions through the .aspx extension, but will not be able to serve files with extensions of .xml, .htm, .xls, and so on.

Browsers do check a file's MIME header (set with Response.ContentType) as well as its extension to determine how to display it, so writing the header for an Excel file (Response.ContentType = "application/ms-excel") will produce the "Open/Save" dialog you want, though the file will still be confusingly named <yourfilename>.aspx. To aid the user, you can double up extensions and serve such a file as <yourfilename>.xls.aspx. While not perfect solution, it does provide a helpful visual cue.

The other factor is company policy, particularly the protection of the revenue model. Sites reliant on revenue from advertising or user fees rely on the fact that the user is looking at and perhaps authenticated on the company's Web site and not receiving content via a third-party or plain-text feed. Once you make it easy for data to move off the site, how do you control it? How do you count hits?

One solution for ad-based sites is to embed the advertising in the content provided, for example as a standard footer. It may require coordination with your sales department to ensure that advertisers provide ads in all the formats you serve—both graphic and plain text, for example.

Accurate hit counts or a requirement to restrict access to redistributable feeds (like RSS or XML) are harder and may be a showstopper in some companies. Partial solutions include deterring Web crawlers with a robots.txt file, or blocking the IP addresses of sites that redistribute content without permission. In theory these solutions are viable, but in practice are often unwieldy and when neglected quickly become ineffective. If you require user authentication, you still have the option of providing downloadable or printable content directly from your site.

Identified

With the scope identified, and the output formats selected, an effective, efficient solution may be designed. Analysis takes time but there is no better way to see the blind alleys in advance or identify the recurring patterns that will simplify coding and code maintenance.

Entity Design

The goal of this article is not to teach data architecture, so this section is brief. If you did a good job in Identification then you know your stakeholders (users), the content (data store), and the formats which meet their needs (output templates or views). These are the conceptual entities, and it helps to draw pictures of what they are and how they relate. Once you have a conceptual view (and some screens roughed out on paper), it is easier to design a data model to meet its needs.

Click here for larger image

Figure 1. The Users superclass and the specific stakeholder instances; the Content superclass and the data store instances; the Interface superclass and the template instances. (Click image for larger view.)

It helps to role-play the stakeholders through the scenarios identified. This often reveals practical details that need to be included in the design. For example, if some content is confidential and not for all stakeholders to see, then you need to include the necessary fields and logic to control access. If you plan to support a public format like RSS, implementing that feature may require information you would not otherwise store. For example, RSS uses a detailed date-time field (such as "Tue, 29 Jul 2003 01:26:00 GMT") and a language-culture field (such as "en-CA"). At this stage, put some thought into how you will store this information and/or generate it through your template.

In the end, you want a concise picture of the data structure, plus a mapping of it onto your planned views that you can compare with the requirements. This stage exists to sketch out your data structure and think through all possible scenarios to be sure the data meets the needs.

System Design

The system is the infrastructure in which the entities will exist and interact. At its simplest, the system to be built (Sybil) consists of a page request, a switchboard to parse that request and direct it to an appropriate display routine (the template) and then the template itself, which loads and displays the requested content.

A page request may be intercepted with either an HttpHandler or an HttpModule. Both require you to add any new extensions to the IIS App Extension map before the request can reach the ASP.NET engine (aspnet_isapi.dll). After that, the choice of which approach to take is up to you.

The main difference between HttpHandlers and HttpModules is that an HttpHandler contains a single ProcessRequest event and an HttpModule intercepts all application requests and therefore has access to the entire System.Web.HttpApplication event cycle (AuthenticateRequest, BeginRequest, EndRequest, and so on). Global.asax is simply an HttpModule file that is automatically compiled when the application begins. HttpHandlers are really designed for new file extensions (as when creating new media formats) that do not require the HttpApplication event cycle, and HttpModules are more analogous to ISAPI extensions, which merely act as a filter or participant in ASP.NET's processing cycle.

But let's return to the system design. For Sybil there should be a fast way to decide whether Sybil should handle the request at all—you don't want all existing .aspx files on your site to be ignored and the requests passed to Sybil. Including a cue like "/Sybil/" or "/articles/" in the path would be a good indicator.

When you declare an HttpHandler for a particular extension in web.config, you indicate the path to watch like so:

<httpHandlers>
   <add verb="*" path="/Sybil/*.htm" 
      type="SybilHandler.Switchboard,SybilHandler" />
</httpHandlers>

In this setting, SybilHandler.Switchboard identifies the namespace and class of the handler as defined in the assembly SybilHandler (which ASP.NET looks for in /bin).

HttpModules, on the other hand, automatically look at every request that comes through by using logic in either the global.asax file or a precompiled HttpModule which you register in web.config like so:

<httpModules>
   <add type="SybilModule.Switchboard,SybilModule" name="SybilModule" />
</httpModules>

Again, the type parameter identifies the namespace.class of the handler as defined in the assembly named after the comma. The new piece is the name parameter which is the friendly module name used to refer to the HttpModule.

So which to choose for Sybil? When running identical switchboard logic in both an HttpHandler and an HttpModule, the HttpModule outperforms the HttpHandler by about 10% according to Application Center Test (ACT) scripts run on the code provided with this article. You might expect that writing separate, optimized HttpHandlers for each extension would help, but it doesn't. It merely spreads the logic out, decreasing the maintainability of the application. An HttpModule is the best choice for optimum performance and maintainability.

In tests, the performance difference between putting the switchboard logic in global.asax or a separately compiled HttpModule is nil. It is cleaner but not necessary to put the switchboard logic in its own HttpModule, especially if you require other HttpModules in your application (such as for authentication).

Context

Context considers how the application works in the context of its use. At this point the design stage is complete and all that remains is to double-check that the system is as efficient as it can be in terms of construction, integration, performance and maintenance. To the site developer, links to pages served through Sybil will be as easy to construct as any others. Only the paths and extensions will vary. To the site user, Sybil will be transparent; the new functionality it provides will be just a click away. To the organization, the design should not conflict with any policies covering confidentiality, maintenance, or the revenue model.

Aa479005.aspnet-onesitemanyfaces-02(en-us,MSDN.10).gif

Figure 2. Sample page, showing output options

Implementation

IIS Configuration

  1. Open IIS and select the Web site. Right-click the site name, and then click Properties.

  2. If you have not already done so, click Create to declare the site folder as an application. This will enable the Configuration button required in the next step (see Figure 3).

    Aa479005.aspnet-onesitemanyfaces-03(en-us,MSDN.10).gif

    Figure 3. Configuring the application

  3. Click Configuration. The Application Configuration window will open with the Mappings tab selected and the Application Mappings panel displayed. Find the entry for .aspx and double-click it (see Figure 4).

    Aa479005.aspnet-onesitemanyfaces-04(en-us,MSDN.10).gif

    Figure 4. Changing the mappings for the application

  4. The plan is to copy the .aspx settings into all new extension mappings. Note these settings now.

    1. Copy the value for Executable to the clipboard. For example, in ASP.NET 1.1 the value will be similar to this:
      C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll
    2. Note the value(s) in the verbs section for Limit To, which will likely be: GET,HEAD,POST (and possibly DEBUG).
    3. Make sure the Check that file exists checkbox is unchecked.
    4. Click OK.
  5. Repeat this step for each extension you need to handle (.htm, .txt, .rtf, .xml, etc.).

    1. Click Add (see Figure 5).

      Aa479005.aspnet-onesitemanyfaces-05(en-us,MSDN.10).gif

      Figure 5. Adding new mappings

    2. Paste the text you copied in step 4a. into the Executable text box.

    3. In the Extension text box, type the extension to handle (such as .txt). Include the leading period.

    4. Make the Verbs section match what was noted for the .aspx file in step 4b.

    5. Uncheck the Check that file exists checkbox.

    6. Click OK.

The Data Store

The Fjorbes data store is a directory containing a set of XML files. The directory is named in the web.config setting contentPath. The XML content files have the following structure:

<?xml version="1.0" encoding="utf-8" ?>
<article>
   <identifier></identifier>
   <title></title>
   <author></author>
   <pubDate></pubDate>
   <description></description>
   <fulltext></fulltext>
</article>

Though the filenames act as the "unique id," the id should be stored in the identifier field as well. It's a simple thing to do and will make it easier to either import all the content to a database down the line, or to aggregate all the articles in a single XML file with an outer <articles></articles> node.

The Data Interface

The data interface class is found in the XmlContent.cs file, which is compiled with the cscxml.bat file into the \bin\XmlContent.dll assembly. It makes good sense to keep the data interface class separate from the rest of your code. If the data store changes or migrates (for example, to SQL Server), you only need to change one file, or easily plug in a new interface. While the class name ("XMLContent") contains the type-specific indicator "XML," the methods (GetElement and GetElementWithoutTags) do not. Any new interface should implement the same methods so only the type name in the calling code would change, and not every method call.

using System;
using System.Web;
using System.Xml;

namespace Sybil {
   public class XmlContent {
      // The XmlContent class provides an interface to an XML data store.

      private XmlDocument _doc;

There is no reason to access the XML directly, so_docis declared as private. If any new access features are necessary they should be added directly to this class.

      public XmlContent (String strID) {
         // Usage: XmlContent myContent = new XmlContent("myContentFile")

Note that the above constructor takes the ID of the content to grab as a parameter, and not the whole XML filename (for example, "12345.xml"). Again, if you migrate to a new data store, only the type name in the calling code would change, but not the parameters.

         HttpContext objContext = HttpContext.Current;
         String strContentPath = System.Configuration.ConfigurationSettings.AppSettings["contentPath"];
         _doc = new XmlDocument ();
         try   {
            _doc.Load ( objContext.Server.MapPath (strContentPath + strID + ".xml"));
         }
         catch (Exception e) {
            objContext.Trace.Write ("XmlContent Init Error", e.Message);
            objContext.Trace.Write ("XmlContent Init Error Source", e.Source);
         }
      }

The HttpContext object provides access to the Server object, including the MapPath method used here, and allows writing to the trace event. The trace.write lines deliver an error message if the requested XML file fails to open or load.

      public String GetElement (String strElement) {
         try {
            XmlNode xmlNode; 
            xmlNode = _doc.SelectSingleNode ("descendant::" + strElement);
            return xmlNode.InnerXml;
         }
         catch {
            return "Element not found.";
         }
      }

   }
}

The first version of GetElement contained this:

xmlNode = _doc.SelectSingleNode ("descendant::/article/" + strElement);

But hard-coding "/article/" tightly coupled this assembly to the schema, and made this assembly hard to reuse. Removing that hard-coded value and letting the calling code specify the full XPath to the field (GetElement("/article/title")) makes the whole assembly more useful.

The Templates

Fjorbes templates are stored by default in the /templates/ folder. This location may be changed by adjusting the templatePath key in web.config. All templates use a data interface assembly named XmlContent.cs to pull content from the XML data store into local variables. The content is then displayed to the site user in the HTML body using asp:label or asp:literal controls as appropriate. Next up is the naming structure and then the source code.

Template naming and calling

The template naming system is designed to reflect the content served by each file and to make it easy to add new templates as necessary. It also makes it trivial to parse the page request and build the string to call the appropriate template:

Template Filename Construction

Template Folder + "MyTemplate" + Optional Modifier + Type Extension + ".aspx"

Template Folder: Set in web.config.

"MyTemplate": Hardcoded, but this standard prefix could be moved into web.config too.

Optional Modifier: Used when an extension might have multiple views—.htm files might be formatted for browsers (no modifier), print ("/print"), or mobile devices ("/mobile").

Type Extension:Either .aspx, .htm, .rtf, .txt, or .xml.

".aspx": All templates will be served through the standard ASP.NET engine.

Finally, the article requested will be added with the id parameter. Putting it all together, a page request that comes in like this:

http://mysite.com/articles/print/article1.htm

Will result in the following call:

http://mysite.com/templates/MyTemplate.print.htm.aspx?id=article1

Aa479005.aspnet-onesitemanyfaces-06(en-us,MSDN.10).gif

Figure 6. Output in HTML format

Note how the file name in the original request becomes the id parameter, which the template will use to identify the content to load. Also notice how the reference to the /article/ folder is thrown out—it is used only to identify which requests Sybil should handle. Once the switchboard takes over it is no longer needed.

Though the sample code uses XML files for the data store, you could easily create an assembly to load content from a SQL Server database instead, and only need to adjust the templates slightly. There would be no need to change the template names or the method of identifying articles at all.

You might ask why code to load content is repeated from template to template. Yes, this code could be refactored into a new Page class and shared among the templates. This was not done in the sample code to make the differences between the templates clearer. It makes it easier to visually compare source files than to have the logic mixed together in a single class.

The plain html template MyTemplate.htm.aspx also demonstrates how to load a control dynamically (optionPanel.ascx) and customize it with a field from the currently displayed content. In this case, the optionPanel uses the ArticleID to generate links to the other display formats. You could as easily create a user control for a common header or footer and pass it text to display as a title.

Template source walkthrough: MyTemplate.htm.aspx

<%@ page language="C#" trace="false" debug="false" enableviewstate="false" %>
<%@ outputcache varybyparam="id" duration="3600" %>
<%@ register assembly="XmlContent" namespace="Sybil" tagprefix="syb" %>
<%@ register tagprefix="inc" tagname="optionPanel" src="optionPanel.ascx" %>

Since postbacks are not needed, ViewState is disabled. There is no reason you could not remove this and enable postbacks inside a template, for example to handle a search button. No matter how the page is called or the content loaded, a template is just like any other .aspx page you serve.

Loading anything from a data store is time-consuming. For pages with many dynamic sections you might just cache the data, but here you can cache the whole page. The VaryByParam directive indicates to cache a copy of the page for each individual article requested for 3600 seconds (1 hour). This provides a free 50% boost in performance.

Next the XmlContent assembly is registered, providing an interface to the data. The namespace "Sybil" is used when referring to XmlContent controls. The tagprefix should be assigned but is unused—XmlContent is a class and not a tagged control.

The optionPanel.ascx user control is registered next. Since this control is created dynamically the tagprefix and tagname are irrelevant for now.

<script runat="server">
private void Page_Load (object obj, EventArgs e) {
   // Template to generate HTML for regular browsers

   String strContentPath = 
      ConfigurationSettings.AppSettings["contentPath"];
   String strArticleID = Page.Request["id"];

The ubiquitous Page_Load begins. First, strContentPath gets the contentPath from the web.config file. Then the Article ID, as passed in the id parameter is stored as strArticleID.

   Sybil.XmlContent content = new Sybil.XmlContent (strArticleID);
   String strTitle = content.GetElement ("article/title");
   String strAuthor = content.GetElement ("article/author");
   String strDescription = content.GetElement ("article/description");
   String strBody = content.GetElement ("article/fulltext");

A variable of type Sybil.XmlContent is created, and the constructor takes the strArticleID as a parameter. The source code for XmlContent is discussed later. If you were to write your own data interface layer, only this line and the line registering the interface at the top of the template would need to be changed.

The next four lines load content into local vars. Why not load the content directly into page controls? This technique allows use of a field like Title in several places (such as inside <title></title> tags, in header metatags, and perhaps in a page heading) without having to call its GetElement more than once.

   title1.Text = strTitle;
   title2.Text = strTitle;
   desc1.Text = strDescription;
   author1.Text = strAuthor;
   body1.Text = strBody;

And sure enough, Title will appear more than once on this page. These lines load the content into Text properties of the actual page controls.

   // Dynamically load a user control to display all the available formats.
   optionPanel myPanel = new optionPanel ();
   // Send the control the Article ID so it can build appropriate links.
   myPanel.articleID = strArticleID;
   panelMenu.Controls.Add (myPanel);
}
</script>

Finally create a new instance of the optionPanel user control and set its articleID property to the current strArticleID. This will allow the optionPanel to display links to the other template files for alternate views of this same content. The Add method adds this new control to the panelMenu placeholder control, declared down in the HTML section.

The HTML section (below) is typical. To avoid including a .css style sheet (and one more file to wade) with the sample code, fonts and sizes are set in tags. In the real world use style sheets wherever possible.

In the head section an asp:literal control is used to display the title as asp:label controls insist on wrapping text in a <span> tag and that doesn't work here. Also note the asp:placeholder tag. Placeholders are used whenever you load a control dynamically. All other content is displayed and formatted with asp:label tags.

<html>
<head>
    <title>HTML Browser Version: <asp:literal id="title1" runat="server" /></title>
</head>
<body bgcolor="#003366">
<form id="Form1" runat="server">
   <img src="/Sybil/images/fjorbes.gif" alt="Fjorbes Online" />
   <table bgcolor="White" width="640"><tr><td>
      <asp:placeholder id="panelMenu" runat="server" />
      <p>
         <asp:label id="title2" font-name="Tahoma" font-size="24pt" 
         forecolor="#b8860b" runat="server"/><br />
         <asp:label id="author1" font-name="Tahoma" font-size="14pt" 
runat="Server" />
      </p>
      <p><asp:label id="desc1" font-name="Georgia" font-size="12pt" 
font-italic="true" runat="server" /></p>
      <p><asp:label id="body1" font-name="Georgia" font-size="12pt" 
runat="server" /></p>
   </td></tr></table>
</form>
</body>
</html>

The other templates are similar, with two exceptions. The XML template simply streams the raw XML file to the browser. The only differences when emitting a non-html format like rich text, plain text, or XML are to set the appropriate MIME-type header (Response.ContentType = "text/rtf";) and lose the HTML tags.

Aa479005.aspnet-onesitemanyfaces-07(en-us,MSDN.10).gif

Figure 7. Output in RTF format

Literals and Labels work fine without the<form runat="server"></form>wrapper. Only controls using features that rely on submitting the form (such as Datagrid paging and sorting, or Linkbutton events) require the form tag wrapper, and these features would be pointless inside .rtf, .txt, or .xml views.

The Switchboard

The walkthrough will use the global.asax implementation, because it is the simplest. The HttpModule and HttpHandler implementations are interesting to compare, to see how each approach can be translated to the other. The differences lie in the methods required to implement each type, what objects each standard method receives by default, and which ones need to be created.

The switchboard receives a request, parses its contents to identify the template to use, and then calls the template with a single parameter to identify the content to display. Once again, this is how the original request is mapped to the actual invocation:

Aa479005.aspnet-onesitemanyfaces-08(en-us,MSDN.10).gif

Figure 8. How the Switchboard maps the request

And so on to global.asax. . .

<%@ Import Namespace="System.IO" %>
<script language="C#" runat="server">
void Application_BeginRequest(Object obj, EventArgs e) {
    String strCurrentPath = Request.Path.ToLower();

The global.asax, like an HttpModule, can contain code to handle any event in the System.Web.HttpApplication event cycle. The place to make the interception for any incoming request is BeginRequest.

Request.Path provides the full path requested (including filename), and for the sake of comparison this path is converted to lowercase.

    // The cue to process a requested url will be the phrase "/articles/" 
    if (strCurrentPath.IndexOf ("/articles/") > -1) {
      String strCustomPath, strFilename, strExtension;
      StringBuilder sbCustomPath = new StringBuilder ();

      String strDevice = "";
      // Check whether a special folder name is being used to target a specific device or version
      if (strCurrentPath.IndexOf ("/mobile/") > -1) strDevice = ".mobile";
      if (strCurrentPath.IndexOf ("/print/") > -1) strDevice = ".print";
      if (strCurrentPath.IndexOf ("/rss/") > -1) strDevice = ".rss";

Since only one of these three modifiers (mobile, print, rss) will be requested at a time (they are not used in combination), the if. . .thens could be nested, but any gain would be negligible.

      // Extract the filename and extension.
      strFilename = Path.GetFileNameWithoutExtension (strCurrentPath);
      strExtension = Path.GetExtension (strCurrentPath);

      // Build the URL to the template
      // E.g.: [templatePath]/MyTemplate[.device][.ext].aspx?id=[identifier]
      // or literally: ~/views/MyTemplate.mobile.htm.aspx?id="article1"
      sbCustomPath.Append (ConfigurationSettings.AppSettings["templatePath"]);
      sbCustomPath.Append ("MyTemplate");
      sbCustomPath.Append (strDevice);
      sbCustomPath.Append (strExtension);
      sbCustomPath.Append (".aspx?id=");
      sbCustomPath.Append (strFilename);
      strCustomPath = sbCustomPath.ToString () ;
      Context.RewritePath (strCustomPath);
   }
}
</script>

A StringBuilder is always faster and more appropriate for string building. Note also that no strings are concatenated (for example, strDevice+strExtension) in any of the sbCustomPath.Append commands. That sort of string manipulation would defeat the purpose of using a StringBuilder in the first place. Avoid this common mistake. The above code is an order of magnitude faster than string concatenation.

And the final Context.RewritePath calls the actual template that will display the data. In the HttpModule version, this redirection is done with a call to Server.Transfer() instead.

Final Details

It is common for people surfing content on a site to look for a default document in the article path. For example, once Billy reads http://mysite.com/articles/article5.htm, he may try browsing to http://mysite.com/articles/ to see if it contains an index. You can solve this two ways. Either create a physical folder and default file at, for example, /Sybil/articles/default.aspx. Or you may create an entry in your data store with an ID of "default," as provided in the sample code. The sample code also provides a default.aspx document in the /Sybil/ root folder which simply redirects to the default article (/Sybil/articles/default.htm).

Also, the sample code provides a duplicate of the MyTemplate.htm.aspx template with the name MyTemplate.aspx.aspx. This is to allow the .aspx extension to request content in addition to the .htm extension. Of course this could also be handled in the switchboard by simply directing requests for an .aspx extension to the .htm template, but then the code wouldn't look quite so clean and sexy. This additional template was merely provided as an example. It is not really necessary to support both extensions.

About the author

Eli Robillard is a .NET guru based in Toronto, Ontario. He is a recognized leader in the developer community, a member of the Microsoft MVP program, and a founding board member of the ASPInsiders. The ASPInsiders provide feedback to the ASP.NET development team on present and future versions of the ASP.NET platform. Eli and his wife Marcie (a.k.a. Datagrid Girl) are both top .NET consultants providing consulting services, training, and team mentoring to organizations working with .NET-based technologies. Eli's Web site provides more information.

© Microsoft Corporation. All rights reserved.