Using Tag Mapping to Fix the Form Control for URL Rewriting

This post describes using tag mapping and a derived form control to fix URL rewriting in asp.net pages.

I had an email exchange with a couple of folks about fixing the form's action attribute as rendered by asp.net that gets in the way of URL rewriting, by rendering out an action attribute pointing to the rewritten URL rather than the prettier original URL as shown on the browser's address bar when the page is initially browsed. I've brought this up for fixing in the past. I know, it is unfortunate that it is still not fixed. I am going to try again, and maybe it'll happen in the next SP... fingers crossed.

In the mean time I thought I'd post my solution here as well. I know various techniques have been blogged about before. I have seen various approaches that I don't quite like personally. For instance I've seen solutions based on control adapters and derived HtmlTextWriter implementations (these suffer from perf overhead of using control adapters, or from having to inspect every attribute rendered out). I have also seen some solutions involving response filtering, and that gets pretty expensive as well.

The solution I use on my own site is a derived form control that renders out a bit of script to essentially rewrite the form's action attribute back to the original URL on the client. Here is my implementation, which also adds support for partial rendering scenarios enabled through the use of ScriptManager, if you're using ASP.NET Ajax:

public class Form : HtmlForm {
    private const string FormActionScript =
@"document.getElementById('{0}').action = window.location.href;
";

    private const string AjaxFormActionScript =
@"Sys.WebForms.PageRequestManager.getInstance().add_endRequest(function() {{
  document.getElementById('{0}').action = window.location.href;
}});
";

    protected override void OnPreRender(EventArgs e) {
        base.OnPreRender(e);

        StringBuilder sb = new StringBuilder();
        sb.AppendFormat(FormActionScript, ClientID);

        ScriptManager scriptManager = ScriptManager.GetCurrent(Page);
        if ((scriptManager != null) && scriptManager.SupportsPartialRendering) {
            sb.AppendFormat(AjaxFormActionScript, ClientID);
        }

        Page.ClientScript.RegisterStartupScript(typeof(Form), String.Empty,
                                                sb.ToString(),
                                                /* addScriptTags */ true);
    }
}

The issue generally cited with this approach is that every page has to be touched, which is pretty painful, and every new page has to use a non-standard <form> tag. Additionally, I would have to implement a custom designer to associate with my derived form control to allow natural editing its contents on the design surface. However, there is a simple solution to both of these issues: I can use the tag mapping feature in ASP.NET to map all occurrences of the HtmlForm control with my derived Form control. Thereafter, as the parser encounters any existing <form> tag, it automatically uses my derived type instead without requiring the developer to make the change explicitly. This makes the approach less intrusive, and quite a bit more natural. All code that accessed the control still thinks its a Form tag.

Specifically, I just need to add this entry in web.config:

<system.web>
  <pages>
    <tagMapping>
      <add tagType="System.Web.UI.HtmlControls.HtmlForm"
           mappedTagType="Site.Form"/>
    </tagMapping>
</pages>

The tag mapping functionality is a mechanism to instruct the parser to subtitute a different derived type implementation whenever it encounters the type being mapped. It was originally invented as part of the WebPart framework so as to allow the ASP.NET WebPartManager to be mapped to the derived Sharepoint WebPartManager implementation without having to change individual pages. However, it is quite powerful and generally applicable.

Tag mapping is one of the esoteric features that comes in handy every once in a while. :-)


[ Tags: ]
Posted on Saturday, 12/8/2007 @ 6:59 PM | #ASP.NET


Comments

11 comments have been posted.

Joe Audette

Posted on 12/9/2007 @ 4:29 AM
Hi,

I've been using a variation on the dreaded attribute inspection technique for a long time so this interests me to see a new solution. However doesn't this client side solution fall down if javascript is disabled or not available?

Best Regards,

Joe Audette

ps painfull captcha

Neil Mosafi

Posted on 12/9/2007 @ 8:56 AM
What a great unknown feature of ASP.NET... I can think of so many places where tag mapping could prove useful, especially in the context of SharePoint. Wish I would have known about this 3 months ago!

By the way, how come I can never read your blog properly with my RSS reader (googlereader)? I only ever get the title a summary parts.

Mike

Posted on 12/9/2007 @ 9:05 AM
Tag mapping provides a solution to one problem introduced with your custom control, but I think that client side script is the weakest solution to the general problem. If performance is a problem with the control adapter solution than what are some of the performance issues with tag mapping and client script?

What's most amazing is that it apparently wasn't fixed in ASP.NET 3.5? Is that actually confirmed, is there a bug on the Connect site for this?

Nikhil Kothari

Posted on 12/9/2007 @ 10:10 AM
There isn't a real runtime perf overhead associated with tag mapping - the parser does the substitution at parse time, and after that things proceed as normal.

You have two options with using a derived Form control. Overriding RenderAttributes and duplicating what the base class does (I didn't want to do that), or using client script. Both do not have the perf overhead associated with the other mechanisms. For me client script was ok. If script was disabled on the client, then, the post would happen to the rewritten non-pretty URL which isn't ideal, but is still functionally correct... so for the relatively smaller % of people who successfully browse with script disabled, I guess I am ok with the slightly degraded browser address-bar experience. They're probably already missing out on a range of other better experiences.

Like I mention in the post, it is unfortunate that it isn't fixed. I have re-started a discussion to get it fixed.

Vesko Kolev

Posted on 12/9/2007 @ 11:28 PM
Nikhil I’m using similar solution and for me it works OK, too. (The difference is that in my case I have a base page class and the script was put there.) But I just want to point that when you are in rewritten page (i.e. with friendly URL) for example www.somesite.com/my/friendly/url and click submit when the form’s action property wasn’t touched this won’t work because the action property uses relative url.
Regards,
Vesko Kolev

Patric

Posted on 12/10/2007 @ 8:23 AM
Why not use the same concept but in OnPreRender use something like
<code>
protected override void OnPreRender(EventArgs e) {
HttpContext.Current.RewritePath(HttpContext.Current.Request.RawUrl);
base.OnPreRender(e);
}
</code>
to rewrite the url back to the original in the form-tag.

Jeff

Posted on 12/10/2007 @ 10:01 AM
I'm not sure why no one else uses IHttpHandlerFactory. I've been doing this for quite some time and I'm really happy with it (see article on uberasp.net, which for some reason is being blocked by the comment system).

Since writing that article I've implemented more frameworky solutions that border on being MVC-like in nature. Because you can stash data in the HttpContext.Items cache (preferably by way of some class that takes care of it), it's surprisingly easy to work with.

Josh Stodola

Posted on 12/11/2007 @ 7:13 AM
I implemented a similiar solution on my blog to take care of that pesky attribute. First learned about tag mapping from a post by Mads Kristensen. The solution I use (which I believe I got from Scott Mitchell) just does not render the action attribute at all. It does not set it with Javascript neither. It still works. The only problem is that it does not validate to XHTML 1.0 strict, but neither does yours. There's gotta be a better solution out there.

suresh

Posted on 12/28/2007 @ 7:45 AM
I'm having problem using tagMapping with HtmlForm. It compiles ok, but it never uses the mappedTagType. It works with other controls, but doesn't work with HtmlForm.

Any ideas on how to solve this?

Nuno Gomes

Posted on 1/2/2008 @ 8:57 AM
Hi Nikhil,
I'm having the same problem as suresh. Can you provide a sample solution?

Jo

Posted on 4/10/2008 @ 1:20 AM
Same problem, is there a solution ?
The discussion on this post has been closed. Please use my contact form to provide comments.