Introduction
One of the websites I work on uses URL rewriting, i.e., when you type in an address www.somesite.com/Products/WidgetA/, you are really browsing to an ASP.NET page somewhere else in the website, say: www.somesite.com/ProductDisplay.aspx?page=Products/WidgetA/.
For a full explanation of URL rewriting along with a very usable rewriting engine, go to 15seconds.com and read Rewrite.NET -- A URL Rewriting Engine for .NET, by Robert Chartier.
Background
One problem with this, and believe me, there are many problems with URL rewriting, is that it breaks the form
tag. The action
of the form
tag will be the real address of the .aspx page and not the nice friendly address you wanted. To workaround this problem, MSDN suggests that you create an "Action-less form", meaning, you create a custom form
tag that simply doesn't write the action
attribute on the form
tag.
namespace ActionlessForm {
public class Form : System.Web.UI.HtmlControls.HtmlForm
{
protected override void RenderAttributes(HtmlTextWriter writer)
{
writer.WriteAttribute("name", this.Name);
base.Attributes.Remove("name");
writer.WriteAttribute("method", this.Method);
base.Attributes.Remove("method");
this.Attributes.Render(writer);
base.Attributes.Remove("action");
if (base.ID != null)
writer.WriteAttribute("id", base.ClientID);
}
}
}
The big problem with the above code is that it completely breaks the form
tag for any server control that hooks into the postback cycle, validators for instance. If you look at the source code for the RenderAttributes
method the HtmlForm
class in the .NET Framework using the excellent Lutz Reflector, you will see that there is a whole lot going on in the .NET Framework's HtmlForm
class that Scott's version does not duplicate. Besides that, what if the HtmlForm
tag was changed in a future version of the .NET Framework?
If we examine exactly how the action
attribute gets written, we can see that because the GetActionAttribute
is private
, there is no way to directly change the action
attribute value before it gets to the HtmlTextWriter
.
Also, some of the logic in the RenderAttributes
method simply can't be reproduced using a derived class because many of the necessary methods and attributes are private
.
writer.WriteAttribute("action", this.GetActionAttribute(), true);
So, we need to take a different approach to take control over if / how the Form
tag action
attribute gets written.
Using the code
My approach was to intercept the attribute before it was written to the HtmlTextWriter
. To do this, I created a derived class called SelectiveHtmlTextWriter
and overrode the WriteAttribute
method:
public class SelectiveHtmlTextWriter : HtmlTextWriter
{
...
public override void WriteAttribute(string name, string value, bool fEncode)
{
base.WriteAttribute(name, "/newpagename/goes/here/", fEncode);
}
}
public class SmartForm : HtmlForm
{
...
protected override void RenderAttributes(HtmlTextWriter writer)
{
SelectiveHtmlTextWriter customWriter = new SelectiveHtmlTextWriter(writer);
base.RenderAttributes(customWriter);
}
}
A new SelectiveHtmlTextWriter
object is created and passed into the HtmlForm
class RenderAttribute
method in place of the normal HtmlTextWriter
.
This works because of polymorphism. All the HtmlForm
class cares about is that it gets a reference to an HtmlTextWriter
. However, when it calls RenderAttribute
, my override gets called instead.
This works great, but I have never been fond of hard-coded values, so in order to make this more extensible, I added an event to the new SelectiveHtmlTextWriter
called WritingAttribute
which the SmartForm
class subscribes to. This allows any attribute, specifically action
, to be modified before its value is written. In this case, I am substituting the Request.RawUrl
value which holds the actual URL that the request came from.
public class SmartForm : HtmlForm
{
public SmartForm()
: base()
{
}
protected override void RenderAttributes(HtmlTextWriter writer)
{
SelectiveHtmlTextWriter customWriter = new SelectiveHtmlTextWriter(writer);
customWriter.WritingAttribute +=
new SubstituteValueEventHandler(customWriter_WritingAttribute);
base.RenderAttributes(customWriter);
}
void customWriter_WritingAttribute(object sender, SubstituteValueEventArgs e)
{
if (e.Name == "action")
{
e.NewValue = Context.Request.RawUrl;
}
}
}
In conclusion, if I had a choice, I would not use URL rewriting at all. I would find some other way to make the client happy. But, the SmartForm
that we outlined here will ease the pain quite a bit. One last caveat, using the SmartForm
breaks the Visual Studio designer. I have been unsuccessful at creating a custom designer for this. If anybody comes up with a designer for this, email me and I'll add it to the article with the appropriate credits.