Click here to Skip to main content
Click here to Skip to main content
Go to top

A Smart Form Control for ASP.NET URL Rewriting

, 20 Jul 2007
Rate this:
Please Sign up or sign in to vote.
A smart form control that fixes many of the problems that are caused by extensionless URL rewriting in ASP.NET.

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);
     }
  }
}
//From <a href="%22http://msdn2.microsoft.com/en-us/library/ms972974.aspx%22">URL Rewriting in ASP.NET</a> by Scott Mitchell

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

David T Nelson
Web Developer
United States United States
I have been doing web development for about 11 years. I specialize in Dynamic HTML, C#, and SQL database design.

Comments and Discussions

 
GeneralMy vote of 1 PinmemberAmol_Joshi24-May-12 19:59 
GeneralAn easier option is here Pinmemberrobert_fairbrother27-Aug-07 6:45 
GeneralRe: An easier option is here PinmemberAmol_Joshi24-May-12 19:57 
GeneralWeb.Config PinmemberXela22023-Jul-07 15:41 
GeneralClasses derived from HtmlForm break the designer Pinmembernikkilong23-Jul-07 7:08 
GeneralRe: Classes derived from HtmlForm break the designer PinmemberRichard Deeming27-Jul-07 10:04 
A control adapter still won't have access to some of the internals of the HtmlForm control, so the generated HTML will still be missing some of the attributes.
 
The simplest answer is probably to combine the approaches: use a control adapter to modify the output for the form, with a derived HtmlTextWriter class to replace or suppress the action attribute. To minimize the impact, you can use the SetRenderMethodDelegate method to ensure that the original HtmlTextWriter is used to render the child controls.
 
protected override void Render(HtmlTextWriter writer)
{
    if (null != writer && null != this.Control)
    {
        HtmlForm form = this.Control as HtmlForm;
        if (null == form)
        {
            Debug.Assert(false, "Only HtmlForm controls are supported.");
            base.Render(writer);
        }
        else
        {
            // If an explicit action is specified, use it:
            string action = form.Attributes["action"];
            if (null != action)
            {
                form.Attributes.Remove("action");
                if (0 != action.Length) action = form.ResolveUrl(action);
            }
 
            SmartFormHtmlTextWriter formWriter = new SmartFormHtmlTextWriter(writer, action);
            form.SetRenderMethodDelegate(this.FormChildrenRenderMethod);
            base.Render(formWriter);
        }
    }
}
 
private void FormChildrenRenderMethod(HtmlTextWriter output, Control container)
{
    Debug.Assert(null != output);
    Debug.Assert(container == this.Control);
 
    if (container.HasControls())
    {
        SmartFormHtmlTextWriter writer = output as SmartFormHtmlTextWriter;
        if (null != writer) output = writer.InnerHtmlTextWriter;
 
        foreach (Control child in container.Controls)
        {
            child.RenderControl(output);
        }
    }
}
 
...
 
private sealed class SmartFormHtmlTextWriter : HtmlTextWriter
{
    private readonly HtmlTextWriter _innerWriter;
    private readonly string _formActionValue;
    private bool _inFormAttributes;
 
    public SmartFormHtmlTextWriter(HtmlTextWriter innerWriter, string formActionValue) : base(innerWriter)
    {
        Debug.Assert(null != innerWriter);
 
        _innerWriter = innerWriter;
        _formActionValue = formActionValue;
    }
 
    public HtmlTextWriter InnerHtmlTextWriter
    {
        get { return _innerWriter; }
    }
 
    public override void Close()
    {
    }
 
    public override void WriteBeginTag(string tagName)
    {
        _inFormAttributes = string.Equals(tagName, "form", StringComparison.OrdinalIgnoreCase);
        InnerHtmlTextWriter.WriteBeginTag(tagName);
    }
 
    public override void WriteAttribute(string name, string value, bool fEncode)
    {
        if (_inFormAttributes && string.Equals(name, "action", StringComparison.OrdinalIgnoreCase))
        {
            // No need to check any further attributes:
            _inFormAttributes = false;
            
            // If we have specified an action, render it:
            if (!string.IsNullOrEmpty(_formActionValue))
            {
                InnerHtmlTextWriter.WriteAttribute(name, _formActionValue, fEncode);
            }
        }
        else
        {
            InnerHtmlTextWriter.WriteAttribute(name, value, fEncode);
        }
    }
 
// Override the other public virtual methods of the HtmlTextWriterClass,
// and pass the call through to the InnerHtmlTextWriter instance, to
// minimize the chances of breaking anything.

 

"These people looked deep within my soul and assigned me a number based on the order in which I joined." - Homer

GeneralActually the best workaround would be.... PinmemberGevorg23-Jul-07 5:05 
QuestionReverse engineering? PinmemberDario Solera21-Jul-07 1:42 
AnswerRe: Reverse engineering? PinmemberDavid T Nelson23-Jul-07 7:05 
AnswerRe: Reverse engineering? Pinmembernsimeonov24-Jul-07 1:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140916.1 | Last Updated 20 Jul 2007
Article Copyright 2007 by David T Nelson
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid