Tuesday, October 12, 2004

Adventures In Url Rewriting

A project I'm currently working on for my employer using ASP.NET 1.1 requires a coherent mechanism to maintain a consistent look and feel throughout the entire site. Now, as ASP.NET 2.0 isn't here yet (Master Pages would have been a perfect fit for this project) I decided to use a mechanism based on URL rewriting.

In short, all the main 'page' functionality is contained within User Controls. There is a single .aspx page that loads the required user control and instantiates it for display within the HTML template. Using a regular expression based url rewriter mechanism this templating system is hidden from the end user; for example, a request for:

http://localhost/MyApplication/MyPage.aspx

is mapped under the hood to:

http://localhost/MyApplication/Template.aspx?UserControl=Controls/MyPage.ascx

All well and good - I had the basic URL rewriting mechanism in place in a couple of hours, and the start of a working template mechanism a couple of hours after that. However, then the problems started cropping up.

The first major issue cropped up with postbacks. In short, they didn't work. The reason for this is that the ASP.NET framework fills in the action attribute with a canonicalized path to CurrentExecutionFilePath, which is not what's wanted here.

Easy then - change the action in the template page. Wrong; whichever brain dead idiot designed the System.Web.UI.HtmlControls.HtmlForm class hid the implementation of the action attribute. So, you've got to jump through hoops to change the functionality.

My eventual solution was to subclass the HtmlForm and replace the instance in my template page with my new class, ActionForm. ActionForm overrides the RenderAttributes function, which creates a new HtmlTextWriter, my own subclassed ActionFormHtmlTextWriter. This class in turn overrides WriteAttribute which checks to see what attribute it's writing out - if it's "action", then the value is changed to the originally requested Url. Control is then passed back to the original HtmlTextWriter to write the value out. Anyway, if it gets someone else out of the mire, then great.

public class ActionForm : HtmlForm
{
    protected override void RenderAttributes(HtmlTextWriter writer)
    {
        string action = (string)Context.Items["VirtualUrl"];

        if (action == null)
        base.RenderAttributes(writer);

        using (ActionFormHtmlTextWriter virtualWriter = new ActionFormHtmlTextWriter(writer))
        {
            virtualWriter.ActionUrl = action;
            base.RenderAttributes(virtualWriter);
        }
    }

    private class ActionFormHtmlTextWriter : HtmlTextWriter
    {
        private string actionUrl;

        public ActionFormHtmlTextWriter(HtmlTextWriter writer) : base(writer)
        {
        }

        public ActionFormHtmlTextWriter(HtmlTextWriter writer, string tabString) : base(writer, tabString)
        {
        }

        public string ActionUrl
        {
            get { return actionUrl; }
            set { actionUrl = value; }
        }

        public override void WriteAttribute(string name, string value, bool fEncode)
        {
            if (value != null && String.Compare(name, "action", true) == 0)
                value = ActionUrl;

            HtmlTextWriter writer = (HtmlTextWriter)InnerWriter;
            writer.WriteAttribute(name, value, fEncode);
        }
    }
}