Fixing Microsoft's Bugs: Url Rewriting
Yet another day, yet another ASP.NET flaw to work around. If you've ever attempted url rewriting with the .NET framework, two things will quickly become apparent:
1) It is amazingly easy.
2) It is amazingly useless.
Why useless you might ask? Well, the issue is that as soon as you start rewriting urls, postbacks start breaking. The root of this evil is the HtmlForm tag, in which some idiot who probably hasn't been fired yet made the decision that he was going to hard code the “action” tag output so that you cannot set it on your own. This would have been not so bad in itself, at least if someone had pointed out that HttpContext has this little member called RewritePath which blows his lame ass implementation to bits. Unfortunately, there is very little you can do about this and the most common solution is to change the form action attribute with javascript. Well, if you ask me that is idiotic (not to mention error prone, because if there is a script error or the browser doesn't support javascript you are screwed). So, as our upcoming CMS product needs to fully support Url rewriting, I needed a better fix than that. After a little bit of code spellunking with Anakrino, we discovered the root of the problem (HtmlForm.RenderAttributes), which looks like something like this:
writer.WriteAttribute("name", this.Name);
this.Attributes.Remove("name");
writer.WriteAttribute("method", this.Method);
this.Attributes.Remove("method");
writer.WriteAttribute("action", this.GetActionAttribute(), true);
this.Attributes.Remove("action");
local0 = this.Page.ClientOnSubmitEvent;
if (local0 != null && local0.Length > 0) {
if (this.Attributes.get_Item("onsubmit") != null) {
local0 = local0 + this.Attributes.get_Item("onsubmit");
this.Attributes.Remove("onsubmit");
}
writer.WriteAttribute("language", "javascript");
writer.WriteAttribute("onsubmit", local0);
}
if (this.ID == null)
writer.WriteAttribute("id", this.ClientID);
this.RenderAttributes(writer);
So, the question of the day is, what the hell can you do about this? Well, it just so happens that HtmlTextWriter (which is a method parameter here) is not a sealed class and WriteAttribute is a virtual method. So, if you can get your own HtmlTextWriter into this method, you could trap this funky behavior and write your own form tag (it is actually slightly more complicated than you might think, because HtmlTextWriter isn't a nice abstract base class like you are hoping for and there is no interfaces to implement either). So, a simple implementation is going to look something like this:
public MyModuleThatRewritesUrls : IHttpModule
{
void Application_BeginRequest(object sender, EventArgs e)
{
HttpContext.Current.Items[“VirtualUrl“] = Request.Path;
HttpContext.Current.RewritePath(newUrl);
}
}
public class MyPage : Page
{
protected override Render(HtmlTextWriter writer)
{
string action = (string)HttpContext.Current.Items[“VirtualUrl“];
if(action != null) writer = new MyHtmlWriter(writer, action);
base.Render(writer);
}
}
Now, in your custom HtmlWriter you will want to do something like this: public class FormFixerHtmlTextWriter : HtmlTextWriter
{
...
bool _inForm = false;
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 = _formAction;
}
base.WriteAttribute (name, value, fEncode);
}
}
There you have it. Now you can rewrite urls all day long and you don't have to worry about client side javascript issues (I guess you could also post-process all content with RegEx if you really wanted, but my guess is that would be significantly slower). If you really want to get funky, you can modify the browserCaps info for the machine to automatically use your HtmlTextWriter implementation (”tagwriter” member), and then you don't have to override the Render method in your page classes. Be forewarned though, there are currently two versions of HtmlTextWriter (HtmlTextWriter and Html32TextWriter), so you should provide overridden implementations of each regardless of the way that you choose to hook into ASP.NET. Our current implementation uses a render method override which looks a little something like this:
if(EmpowerContext.Current.VirtualUrl != null)
{
string url = EmpowerContext.Current.VirtualUrl;
if(writer.GetType() == typeof(Html32TextWriter))
{
writer = new FormFixerHtml32TextWriter(writer.InnerWriter, " ", url);
}
else if(writer.GetType() == typeof(HtmlTextWriter))
{
writer = new FormFixerHtmlTextWriter(writer.InnerWriter, " ", url);
}
}
base.Render (writer);