Sun, Aug 21 2005 3:41 AM
Erwyn van der Meer
Integrating ASP.NET with a CMS and hiding the .aspx extension
Please read my earlier post on generating XHTML output from ASP.NET 1.1 before reading this post.
- We use custom controls to generate XHTML 1.0 Strict markup.
- We parse the ASP.NET output as XML.
- We tweak the form element and an input element to make the entire page XHTML 1.0 Strict.
- We map parts of the XHTML tree of the ASP.NET output into an XHTML 1.0 Strict static page produced by a content management system (CMS).
The last point is our answer to how to ensure and maintain a common look-and-feel between the static XHTML pages published by the CMS and the dynamic ASP.NET pages. In this post I will give more background and detail on this design decision. Because it's not the only approach possible.
The CMS we use is of Dutch origin. Unlike Microsoft's CMS it publishes pages to the web server that have no runtime dependency on the CMS. So they are static from the CMS perspective. The choice for the CMS in question was fixed before ASP.NET entered the picture. One option to make some pages dynamic is to publish .asp, .aspx or .jsp pages that contain code to be executed on the web server. But they will still be static from the CMS perspective. Anything that the CMS does, like creating a common layout and helping with links among web pages and to resources like images, is done at publication time. Not when the page is served to a browser by the web server.
The old style approach would have been to use frames. The static part of the page with the common look-and-feel would be published by the CMS and the dynamic part would be framed inside it. Using frames has well-known drawbacks so this approach was quickly ruled out.
We thought about letting the CMS publish the .aspx pages for the .NET modules. This way the XHTML markup for the common look-and-feel and navigation menus on the .aspx pages can be build by the CMS from the same building blocks as the static XHTML pages. However a CMS is good at publishing content and not really good at managing and publishing source code. In the ASP.NET case you have depencies on assemblies containing the compiled code-behinds. Do you want these to be published by the CMS as well? Do you want to run the risk of a content editor messing up the markup of your ASP.NET page and breaking the close relationship with the code-behind assemblies which they cannot edit?
So we decided to take a different approach. The ASP.NET pages and the assemblies live separate from the CMS content on the web server and are not managed or published by the CMS. The ASP.NET page knows what content template it should use, fetches it and maps its own output into container elements (like divs) in the XHTML content template. The resulting XHTML document is what gets sent to the browser.
Well it is not really the ASP.NET page itself that performs the mapping, but an IHttpModule which I will call the content mapper. So how does it know what content template to use? One of the requirements was that a single ASP.NET page might be mapped into several different content templates depending on the context in which the .NET module in question was being used.
The .NET modules are linked to from static XHTML pages published by the CMS. A static page that links to a .NET module represents a navigation state which I will call the navigation context for the .NET module. You can see the navigation state as the highlighting of menu items and in the folder structure in the URL. It shows you where you are in the navigation structure of the site. One of the requirements was that the .NET modules would keep the visual navigation state intact. They should support different navigation contexts, i.e., a .NET module should adapt like a cameleon to the page that linked to the module. In the old days the easiest way to solve this would be to use frames. As I mentioned before these are out of the question.
The first idea to keep track of the navigation state was to read the HTTP referrer header value to see which page linked to the .NET module. This value was to be passed along in a hidden field if the .NET module itself has multiple pages. But I didn't like the strong dependency on an HTTP header that is sometimes blocked by proxy servers or personal firewalls. I wanted it to be possible to explicitly tell the .NET module what navigation context to use. Also I disliked the hidden field idea. It puts an extra burden on developers of .NET modules to pass it along. You always have to use HTML forms to pass it along, and you have to use POSTs instead of GETs for those forms or you'll end up with all form fields in the URL and not just the navigation context parameter.
I wanted the whole content mapping thing to be as much a black box as possible for the developers. I felt the best place to keep track of where you are in the site is the URL you see in the address bar of your browser. And I don't mean the query part, but the path part of the URL. So I decided to solve this issue together with the requirement that URLs to pages in the site should not include extensions like .aspx or .html.
We settled on a URL format that looks like this: http://www.finance.nl/advice/savings/context/information/savingsacounts/. And this URL actually points to the same .NET module but in a different context: http://www.finance.nl/advice/savings/context/thisweektips/. The .NET module is deployed in a single virtual directory: /advice/savings/. The part after that in the URL does not correspond to a real virtual directory. It just identifies the navigation context. The virtual directories /thisweekstips/ and /information/savingsaccounts/ do exist. These are the locations that the content mapper uses to fetch the content template for the navigation context from.
The trick to accomplish this is to configure a wildcard mapping in Internet Information Server (IIS) for the virtual directory /advice/savings/. This means configuring the .* extension to use the ISAPI extension aspnet_isapi.dll. This way all requests for URLs within that folder will be handled by ASP.NET.
ASP.NET uses the machine.config and web.config files to determine what IHttpHandler to invoke. An ASP.NET Page is an example of an IHttpHandler. We configure a wildcard mapping for * in the web.config file. But instead of specifying a concrete IHttpHandler, we specify a class that implements IHttpHandlerFactory. I will call this class I wrote the CustomPageHandlerFactory. It parses the URL and determines what the navigation context is and what .aspx page to invoke. For the two URLs above this would be a configurable default page in the savings directory, but for the URL http://www.finance.nl/advice/savings/page2/context/information/savingsacounts/ it would be page2.aspx. After parsing the URL the CustomPageHandlerFactory puts the navigation context in the current HttpContext. This way the content mapper can use it later on in the page processing. Finally the CustomPageHandlerFactory gets the IHttpHandler for the .aspx page by calling PageParser.GetCompiledPageInstance. PageParser is part of the ASP.NET infrastructure code. It's not intended to be called by user code, but it's a public class and works well. The IHttpHandler is returned to the ASP.NET runtime which invokes it to process the request.
On a closing note. I have left out some details and cannot share my code as it is owned by the client I work for. While googling for some pages to link to I found a blog post detailing a similar approach. Matias has a simple implementation of a PageHandlerFactory that you can download.
Filed under: .NET, Architecture and Design, Work