URL Re-Writing in ASP.NET Requires Form Action to be Re-written

Thoughts from the team
By    | June 4, 2010 | Code Snippets,

On a project I was working on recently we ran into a problem where the combination of URL re-writing and  postbacks caused the page to post back to the wrong URL.  When you create an ASP.NET page with a <form runat=”server”> tag, ASP.NET will automatically output the action attribute to be the URL of the current page.  However, the URL that is used is not the original URL of the request, but instead the real URL of the page.  For example, when you are on the page “/services/web-design” the real request might be to “/services.aspx?service=web-design”.  When you do a postback, you will be returned to the ugly URL.

Re-Writing the ASP.NET PostBack URL

So, what do we do about this?  We use the ASP.NET Control Adapter framework to change the way the action attribute of our <form> tag is rendered.  This doesn’t require you to change any code on any pages, we are simply overriding a part of the standard ASP.NET page rendering pipeline which serves all pages.

To get started we need two things:

  1. A .browser file in the /App_Browsers directory of your application
  2. A class which inherits ControlAdapter to do the work.

First up, lets look at the contents of the browser file.  I’m calling mine Form.browser.

<browsers>
    <browser refID="Default">
        <controlAdapters>
            <adapter controlType="System.Web.UI.HtmlControls.HtmlForm"
                     adapterType="Storm.ControlAdapters.FormRewriteControlAdapter" />
        </controlAdapters>
    </browser>
</browsers>

In this file we are registering a new control adapter for the HtmlForm Control.  My adapter is in Storm’s common library for easy reuse across projects.

Next, lets take a look at the FormRewriteControlAdapter

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

    /// <summary>
    /// Used to rewrite the form action attribute when using URL re-writing to the
    /// RawUrl so we don't get in a big mess!
    /// </summary>
    public class FormRewriteControlAdapter : System.Web.UI.Adapters.ControlAdapter
    {
        /// <summary>
        /// Override the render method
        /// </summary>
        /// <param name="writer">The text writer</param>
        protected override void Render(HtmlTextWriter writer)
        {
            base.Render(new RewriteFormHtmlTextWriter(writer));
        }
    }

There’s nothing terribly exciting here – we are inheriting System.Web.UI.Adapters.ControlAdapter and then simply call the base Render method with a custom TextWriter implementation.  So let’s move on and take a look inside the TextWriter and see how the magic happens:

    /// <summary>
    /// The internal text writer to use in FormRewriteControlAdapter
    /// </summary>
    public class RewriteFormHtmlTextWriter : HtmlTextWriter
    {
        /// <summary>
        /// Initializes a new instance of the RewriteFormHtmlTextWriter class
        /// </summary>
        /// <param name="writer">Html text writer</param>
        public RewriteFormHtmlTextWriter(HtmlTextWriter writer)
            : base(writer)
        {
            this.InnerWriter = writer.InnerWriter;
        }

        /// <summary>
        /// Initializes a new instance of the RewriteFormHtmlTextWriter class
        /// </summary>
        /// <param name="writer">An IO text writer</param>
        public RewriteFormHtmlTextWriter(System.IO.TextWriter writer)
            : base(writer)
        {
            this.InnerWriter = writer;
        }

        /// <summary>
        /// Do the actual attribute writing of the action attribute
        /// </summary>
        /// <param name="name">The name of the attribute</param>
        /// <param name="value">The value of the attribute</param>
        /// <param name="fEncode">Dont know</param>
        public override void WriteAttribute(string name, string value, bool fEncode)
        {
            if (name.Equals("action"))
            {
                var context = HttpContext.Current;

                if (context.Items["ActionAlreadyWritten"] == null)
                {
                    // Set the action to the raw url
                    value = context.Request.RawUrl;
                    context.Items["ActionAlreadyWritten"] = true;
                }
            }

            base.WriteAttribute(name, value, fEncode);
        }
    }

The key part here is the WriteAttribute method. This is called for each attribute that appears on the <form> tag.  First we check to see if we are working on the ‘action’ attribute and then check that we haven’t already written it.  Now we get to the good part.  We grab the RawUrl of the request – this should contain the Url that was first requested by the browser (i.e. our nice, clean re-written Url) – and overwrite the value parameter.  We then call the base WriteAttribute method with the new value and the ASP.NET rendering pipeline takes care of the rest.

Thanks to Scott Guthrie for getting me started fixing this!  If you’re working in VB.NET, Scott provided a sample project.

More like this