Templated Pages using RewritePath and User Controls

[Update April 2005] - At 5999 hits, I want to thank everyone that has visited.  I changed the title to “Templated Pages etc...“ since there are so many searching for assistance on actual ASP.Net Master Page™ technology.

Over the last year using the technology described in this article, I've realized the big advantage: With this technique, your single web application can scale it's data to the level of granularity your visitor is expecting.  The day of the global, newspaper-style website is over.  Simplicity, specificity in your web application is what's going to keep your users interest piqued.  The technology listed here allows you to create an abstract website... then, through parsing out the RequestPath, determine the granularity of the visitor's destination. 

As a quick example, you can host domains for MyCoGlobal, MyCoWisconsin, MyCoMilwaukeeWI, MyCoEastSideMilwaukee, each of which will be crawled separately, and may contain data (news?  classifieds?  adwords?) scoped to the appropriate level.  This is accomplished through the development of a single web application.  I've probably read a hundred articles on the nextgen web frameworks, and I'm sure that there are other options out there for such a solution, but this solution has made things incredibly easy for creating a domain-scalable architecture.

Though the article describes loading controls dynamically and rewriting your RequestPath, it implicitly describes how to parse out your url, rewrite it on the fly, and handle it appropriately.  The path rewriting in this article explains how to use rewriting to represent user controls as pages, but in practice, it has just as easily allowed the single web application to behave according to the url requested.

Overview

Master pages are still awhile away, so there are some interesting solutions popping up to hold us over in the meantime.  I'm working on a project that requires ease of design (a single page holds the layout, with a placeholder for the content), and maximum “crawlability“ by search engines.  The solution requires some advanced techniques in ASP.Net to come up with a "master pages" 1.1 solution for a project I'm working on.  This solution revolves around 2 techniques: loading user controls dynamically at runtime, and rewriting urls.

  • Load user controls dynamically into a master page by parsing out control name in the querystring.  Retain all other querystring info as args to the control.
  • Rewrite URL paths so client's page requests get passed as querystrings to the master page, example: www.asdf.com/faq.aspx maps to www.asdf.com/default.aspx?faq.aspx

Loading Controls Dynamically
Ultimately, we want to take the name of a “content control” in a querystring, and load it dynamically into a page.

I have created a directory to store my content controls, called “content.“  On the main page is a pnlContent panel into which the control will be loaded when the page loads.  The name of the content control to load is passed to the page in the QueryString (passed as “p“ in this case).  If the page sees www.asdf.com/default.aspx?p=faq, it knows to look for faq.ascx and load it into the content panel on the page.  If no such control can be found, or if there is an error, then a custom 404 is displayed in the panel.

private void Page_Load(object sender, System.EventArgs e)
{
   //Parse out control name from qs and load control into the page's pnlContent panel
   string ucname=Request.QueryString["p"];
  
if(ucname == null || ucname == String.Empty || ucname.ToLower()=="default")
      ucname="introduction";
   if(new System.IO.FileInfo(this.MapPath("~/content/"+ucname+".ascx")).Exists)
     
this.pnlContent.Controls.Add(LoadControl("~/content/"+ucname+".ascx"));
   else
      this.pnlContent.Controls.Add(LoadControl("~/content/PageNotFound404.ascx"));
}

Rewriting URL Paths
There is a bug in the HttpContext.RewritePath method.  Jesse Ezell has a good description of  the issue here (apparantly a MS design flaw).

The workaround in making RewritePath work for you involves:

  • overriding the Render method of your page
  • writing a custom HtmlTextWriter class to modify the action atrribute of your web form (where the problem resides).

 

Before we get started with these things, let's get the URL rewrite started.  This takes place in the global.asax, inside the Application_BeginRequest method.  The process is roughly as follows:

The code that does just this is below.

protected void Application_BeginRequest(Object sender, EventArgs e)
{
   //string pretoken="default.aspx/"; //text that precedes the page arg
   string pretoken = ConfigurationSettings.AppSettings["pagename_pretoken_string"];
   HttpContext c = HttpContext.Current; 
   string pagename=""; //destination page
   string path = c.Request.Url.ToString();
   string qsparams = ""; //additional querystring args if applicable
   //mark start and finish indices of path to substring page name from request
   int start=path.IndexOf(pretoken)+pretoken.Length;
   int numchars=path.IndexOf(".",start)-start;
   pagename=path.Substring(start,numchars);
   if(pagename!="default")
   {
      if(path.IndexOf("?")!=-1) //there are additional querystring args
         qsparams="&"+path.Substring(path.IndexOf("?")+1);
      string newPath = "default.aspx?p="+pagename+qsparams;
      c.Items["requestpath"]=newPath; //this will be needed for the RewritePath bugfix (below)
      c.RewritePath(newPath);
   }
}


A note regarding Configuration in ASP.Net
Many may be familiar with web.config and it's behaviour in ASP.Net's architecture, but for those that aren't, I wll post a quick paragraph on the process.  While it's most common to have web.config residing in your application root, it is possible to place more web.config files in subdirectories of your application.  They are processed in a top-down fashion, in the following order: machine, site, application, and subdirectory.  Note that machine.config is the only config that is required in the ASP.Net framework.  All other configs only exist to store changes from the default machine.config file.

In this application, the web.config in the site contains the configuration key to contain the pretoken.

<xml version="1.0" encoding="utf-8" ?>
   <
configuration>
      <
appSettings>
         <
add key="pagename_pretoken_string" value="asdf.com/" />
      <appSettings>
   <
configuration>

Reading the configuration settings in an application is very simple.  Inside .Net's System.Configuration namespace is the ConfigurationSettings class, and the AppSettings indexer that is able to reference the appSettings elements in your config file.  Make sure you have a reference to System.Configuration, and you can access appSettings elements very easily:

string pretoken = ConfigurationSettings.AppSettings["pagename_pretoken_string"];

Finally - the BIG RewritePath issue
The solution is here, though I found it rather vague.  It took some work to take Jesse's notes and turn them into a real solution.  The 2 major steps follow. 

Note! - The IHttpModule in Jesse's solution is used in place of the global.asax Application_BeginRequest  method we're using in this solution.  You can do your rewrites using a custom IHttpModule if you wish, but it is an alternative to the rewrite method below.

1. Write a custom HtmlTextWriter class to modify the action attribute of your web form (where the problem resides).
This looks for the form tag, and finds the action attribute inside, changing the action to the correct address.

using System.IO;
using System.Web.UI;

public class FormFixerHtmlTextWriter : HtmlTextWriter

   private bool inForm = false;
   private string _action;
   public FormFixerHtmlTextWriter(TextWriter w, string a) : base(w)
   {
      _action=a;
   }
   public override void RenderBeginTag(string tagName) 
   {
      inForm = String.Compare(tagName,"form") == 0;
      base.RenderBeginTag (tagName);
   }
   public override void WriteAttribute(string name, string value, bool fEncode)
   {
      if(String.Compare(name,"action",true)==0)
         value = _action;
      base.WriteAttribute (name, value, fEncode);
   }
}

2. Override the Render method of your page to use the new custom HtmlTextWriter.  Note that the HttpContext's RewrittenPath item value was stored in the global.asax Application_BeginRequest method as part of the rewriting process.

protected override void Render(HtmlTextWriter writer)
{
   string action = (string)HttpContext.Current.Items["requestpath"];
   if(action!=null
      writer =
new FormFixerHtmlTextWriter(writer,action);
   base.Render(writer);
}
 
This article has quickly described how to leverage current ASP.Net techniques to enable easy, flexible page development, while providing a client-side environment that is simple and easy to navigate by end-users and search-engines.  ASP.Net provides us with numerous techniques for configuration, page and control handling, and url manipulation.
 
Please provide feedback below, or e-mail me at grh@whdlaw.com.
 
Thank you.
Print | posted on Thursday, May 27, 2004 8:48 PM

Feedback

# re: Master Pages using RewritePath and User Controls

left by Gianca at 7/27/2004 2:40 AM Gravatar
Bello bello!!

# re: Master Pages using RewritePath and User Controls

left by Senthil at 7/27/2004 2:52 PM Gravatar
Rewrite technique is cool as long as you want to meddle with the page name.

I am wondering if there is any solution for the following problem.

Say url to my website is

www.mywebsite.com

I want let my customers to type a customized url

www.mywebsite.com/abc (but I don't have a virtual directory called abc)

I cannot write a HttpModule which could read the URL and "Rewrite" the path as IIS does not pass the request to ASP.NET

However if I type in the following url IIS passes the request to ASP.NET

www.mywebsite.com/abc/default.aspx

provided you uncheck the "Verify File Exists" check box in the IIS configuration for .aspx extensions.

Currently I am handling this case using an ISAPI filter. I would like to get rid of the ISAPI filter for performance reasons.

Any thoughts on this?

# re: Master Pages using RewritePath and User Controls

left by Chad at 7/27/2004 6:02 PM Gravatar
Senthil, maybe a custom error page could perform what you are trying to do?

# re: Master Pages using RewritePath and User Controls

left by soori at 7/27/2004 9:58 PM Gravatar
you would fine stipulate the master page handling.
thanks

# Master Pages using RewritePath and User Controls

left by SampoSoft .NET Blog at 8/1/2004 9:44 AM Gravatar

# re: Master Pages using RewritePath and User Controls

left by Senthil at 8/5/2004 1:06 PM Gravatar
Ok guys I got the solution thru' one of my contacts at Microsoft.

I'll provide a sample program probably tomorrow as I am busy with something right now.

However the solution is you need to do a wild card mapping so that any non-specific request to IIS could be routed to that handler.

To do the wildcard mapping in IIS follow the following steps

Open IIS Manager
Properties (of the website you are interested) -> Home Directory / Virtual Directory -> Configuration

In the Application Configuration window click on the Mapping Tab

Click on the Insert button on the Wildcard application maps

Choose the Executable as <System Drive>:\WINNT\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll

IMPORTANT: Don't forget to uncheck the "Verify File Exists" checkbox

Note: What this means is all the non-specific requests are going to be routed to the asp.net handler and IIS will not check whether that directory exists or not

Now add an HttpModule to your application which is in this directory

(Make sure you add that in the web.config too)

<httpModules>
<add name="URLFilter"
type="Mycompany.Branding.URLFilter, MyCompany.Branding"/>
</httpModules>


Now in the HttpModule make sure you remove the "extra" information and rewrite the path


HttpContext c = HttpContext.Current;
string path = c.Request.Url.ToString();

//string newPath = You logic goes here
In this case you don't need the following line of code
//c.Items["requestpath"]=newPath; //this will be needed for the RewritePath bugfix (below)

c.RewritePath(newPath);

Thanks

# Master Pages using RewritePath and User Controls

left by SampoSoft .NET at 9/14/2004 7:36 PM Gravatar

# re: Templated Pages using RewritePath and User Controls

left by Christopher Pietschmann, MCSD, M at 6/26/2005 3:46 PM Gravatar
Thanks!
I'm working on a new site in v1.1 and I needed to have Master Page like functionality. This works perfect.

# ASP.NET 2.0: Rewriting URL Paths just got a whole lot easier

left by An MCSD from Wisconsin at 6/29/2005 12:44 PM Gravatar

# re: Templated Pages using RewritePath and User Controls

left by Sam at 9/21/2005 12:20 AM Gravatar
El neato!

# re: Templated Pages using RewritePath and User Controls

left by Sam at 9/26/2005 7:41 PM Gravatar
I got around the annoying empty directory issue by setting the custom error page for 403.14 to /default.aspx. My url rewriter then intercepted this request and did this:

protected override void OnRewritePath(string requestPath, HttpApplication app) {
HttpContext context = app.Context;
//Check for empty directory
string queryString = context.Request.ServerVariables["QUERY_STRING"];
if (queryString.StartsWith("403;")){
requestPath = ReCalculatePath(queryString);
}
//...
}

string ReCalculatePath(string queryString){
string path = queryString.Substring(4);

if (path.IndexOf("?") > -1){
//Original path had a querystring argument
path = path.Insert(path.IndexOf("?"), "default.aspx");
}
else
path += "default.aspx";

return path;
}

It’s a little inflexible in that it assumes that the default document for the directory u request was default.aspx but it's certainly a starting point to add additional features.

# re: Templated Pages using RewritePath and User Controls

left by Sam at 9/27/2005 12:43 AM Gravatar
Note: if you use the above method, url authorization will occur on the redirecturl that 403.14 points to and not to the url you rewrite to... bummer. Not sure what to do about it, as always it'd be nice to call some methods out of the framework to help but bs mf's M$ made everything useful internal. grr...

# re: ASP.NET 2.0: URL Mapping with RegEx Support

left by An MCSD from Wisconsin at 1/2/2006 12:58 PM Gravatar
Title  
Name
Email (never displayed)
Url
Comments   
Please add 2 and 6 and type the answer here: