Introduction
Anyone who has spent time developing URL rewriters will know that these do not always play nicely with AJAX components. This is because the HtmlForm element written out by .NET uses the actual URL for post-backs, and not the page's virtual URL that you are trying to preserve. This article discusses how this can be resolved cleanly, and also shows how control adapters may be set programmatically using Reflection. This is useful for creating plug & play components such as URL rewriters in order to minimise the amount of configuration required.
Background
In order to preserve the rewritten URL on post-back (and fix AJAX components), the HtmlForm's action attribute needs to be changed to the virtual URL.
Without this fix, you may experience the following error when using Asp.Net Ajax. This happens when your virtual URL is in a different directory than the real path.
Sys.WebForms.PageRequestManagerServerErrorException:
An Unknown error occurred while processing the request on the server.
The standard approach might be to extend the HtmlForm and override its Render method so that it alters the action attribute on render. Whereas this would work, it is a poor solution. It requires that all ASPX pages are edited in order to apply the fix. It also requires prior knowledge of the issue when other developers start creating pages, and it makes the URL rewriter component less pluggable.
A better solution would be to use a control adapter. This can be added via configuration settings in order to change the rendered HTML of all HtmlForm elements within an application. This would then not only apply the fix to all existing pages, but also to any future pages.
For anyone new to control adapters, these are discreet classes extended from the ControlAdapter class. These can be mapped to specific .NET controls via config settings located in the App_Browsers special directory. The adapters are invoked by the framework at runtime, and can alter the rendered HTML of any .NET control. A common use is to make the standard .NET controls more CSS friendly.
OK, so sounds like a good solution? Not yet! Imagine we are developing a component to be used by a third party that requires a control adapter. After they have plugged in our component, how do we ensure they add the control adapter to the solution? The standard approach is to provide heaps of documentation, and if it breaks... well... it's user error.
A smarter solution is to set the control adapter programmatically from within our component. In this way, the component is self-contained. It will be attached, when required, automatically, wherever and whenever our component is used. The bad news is, this ability isn't supported in the current framework. The good news is, it can be done, and here's how.
Solution
Firstly, we need a control adapter to correct the action attribute of the HtmlForm element. Here's one I made earlier.
public class HtmlFormAdapter : ControlAdapter
{
protected override void Render(HtmlTextWriter writer)
{
base.Render(new HtmlFormWriter(writer));
}
private class HtmlFormWriter : HtmlTextWriter
{
public HtmlFormWriter(HtmlTextWriter writer)
: base(writer)
{
this.InnerWriter = writer.InnerWriter;
}
public HtmlFormWriter(TextWriter writer)
: base(writer)
{
this.InnerWriter = writer;
}
public override void WriteAttribute(string key, string value, bool fEncode)
{
if (string.Compare(key, "action")==0)
{
value = HttpContext.Current.Request.RawUrl;
}
base.WriteAttribute(key, value, fEncode);
}
}
}
The next step is to set the control adapter programmatically. In order to do this, we use Reflection to access the hidden private _adapter property of the HtmlForm control and set this to an instance of our adapter. In this example, we assume we are running from within a HttpModule that is doing the URL rewriting, and hook the PreRequestHandlerExecute event. We can then hook the page's PreRender event in order to grab the HtmlForm control and attach the adapter.
private void context_PreRequestHandlerExecute(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
if (context.Handler is Page)
{
Page page = (Page)context.Handler;
page.PreRender += new EventHandler(RegisterControlAdapters);
}
}
private void RegisterControlAdapters(object sender, EventArgs e)
{
Page page = (Page)sender;
page.PreRender -= new EventHandler(RegisterControlAdapters);
if (page.Form != null)
{
FieldInfo adapterFieldInfo = page.Form.GetType().GetField("_adapter",
BindingFlags.NonPublic | BindingFlags.Instance);
if (adapterFieldInfo != null)
{
HtmlFormAdapter adapter = new HtmlFormAdapter();
FieldInfo controlFieldInfo = adapter.GetType().GetField("_control",
BindingFlags.NonPublic | BindingFlags.Instance);
if (controlFieldInfo != null)
{
controlFieldInfo.SetValue(adapter, page.Form);
adapterFieldInfo.SetValue(page.Form, adapter);
}
}
}
}
Summary
This is a pretty specific example; however, the point is that using this technique greatly aids the creation of smart components that require minimal configuration. It also provides a greater degree of power to component developers, who are then able to hook into the actual rendering of all the controls in an application from a single line in the web.config.
<add name="RewriteModule" type="CodeKing.Web.UrlRewriter, CodeKing.Web"/>
This leads to more pluggable components and fewer problems when it comes to development cycles or swapping out of components.
Further Reading
History
- 19th June 2008: First published
- 23rd June 2008: Further Reading section updated
- 24th June 2008: Updated demo to use
HtmlTextWriter
| You must Sign In to use this message board. |
|
|
 |
|
|
 |
|
 |
Can you be more specific. Which ajax library you are using? Is the action tag in the form not reflecting the actual url in the browser? This is a server-side technology and shouldn't be effected by the client, however it could be the effected by your ajax implementation.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
My Rewrite code:
dim domain as string = Context.Request.Url.Host.ToString Context.RewritePath(domain & Context.Request.FilePath)
When i using your code, it work in IE, but not in FireFox
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
The sample code attached works in both IE and Firefox. Can you be more specific about which ajax library you are using and which code you are having problems with?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I'm using Visual Studio 2008 to build this sample.
Folder for this sample is:
c:\sample : root c:\sample\bin : contain dll file c:\sample\[domainnam] : domainname: name of request domain. In this folder contain .aspx file.
Ex: when user input: http://test.com/default.aspx -> rewrite to c:\sample\test.com\default.aspx file.
When i using firefox to test, it occur error.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi there,
This doesn't make any sense. Url rewriting is used to map a "virtual" url to another "real" url. You shouldn't be mapping a url to a file system path as this won't work, and is not the purpose of the library. Also you don't appear to be changing the path. Visual Studio development server or IIS should already map /default.aspx to c:\sample\test.com\default.aspx.
You can do:
http://test.com/virtual/default.aspx -> rewrite to http://test.com/default.aspx
Send me your project or some more information about what you are trying to do and I will try and help. At the moment it dosn't look like you are using it correctly.
Mike
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi!
I'm use multi domain with a IP.
Ex:
domain ip
test.com 127.0.0.1 vbcode.com 127.0.0.1
When user access:
url -> Real Folder test.com/default.aspx -> c:\sample\test.com\default.aspx vbcode.com/default.aspx -> c:\sample\vbcode.com\default.aspx
it's work well, but error in firefox.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Sorry this doesn't make sense. This article is about fixing the "action" attribute of the web form to solve a problem with Ajax libraries when using a url rewritting technique. You haven't indicated that you are doing either of these.
The example code included does not demonstrate an alternative to "Response.Redirect" or "HttpContext.RewritePath", and should not be used to map urls to file paths or urls from one application to the url of another application.
You would simply set up IIS with appropriate host headers
c:\sample\test.com\default.aspx -> Host header -> test.com ... and c:\sample\vbcode.com\default.aspx -> Host header -> vbcode.com
Can you be clear about what you are doing, and how it relates to the subject of the article?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
I don't mean that your implementation is wrong, or bad in any way.. I just though that the following implementation is cleaner 
public class FormRewriterControlAdapter : ControlAdapter { protected override void Render(HtmlTextWriter writer) { base.Render(new RewriteFormHtmlTextWriter(writer)); } }
public class RewriteFormHtmlTextWriter : HtmlTextWriter { public RewriteFormHtmlTextWriter(HtmlTextWriter writer) : base(writer) { this.InnerWriter = writer.InnerWriter; }
public RewriteFormHtmlTextWriter(System.IO.TextWriter writer) : base(writer) { this.InnerWriter = writer; }
public override void WriteAttribute(string name, string value, bool fEncode) { if (name == "action") { HttpContext current = HttpContext.Current; if (current.Items["ActionAlreadyWritten"] == null) { value = current.Request.RawUrl; current.Items["ActionAlreadyWritten"] = true; } } base.WriteAttribute(name, value, fEncode); } }
P.S. Code is not mine, took it from Scott Gu's blog )
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I too prefer this approach, however unfortunately it doesn't work with the Ajax.Net library. I actually mention why in the article and in the other comment below. It's related to the way the Web Extensions library casts the HtmlTextWriter class resulting in a null reference exception.
Since this article was written, I have extended the concept further and created the SimpleUrl.Net library which uses a hybrid of both methods in order to get the best of both worlds. This uses the HtmlTextWriter approach above for the inital rendering, but then uses the approach from the article for Ajax based post-backs. This way the the default functionality of HtmlForm is retained (including default button), but Ajax libraries also still work.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hmm.. Strange, but I never had any problems with it whatsoever  I use it in our production server, and we use .NET's AJAX for quite awhile already 
Are you sure there is a problem?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Purhaps you aren't using url rewriting? I can reproduce the error in my demo application by swapping out the control adapter with the HtmlTextWriter approach. This throws a null reference on base.Render. Using reflector this is from the following line in System.Web.Extensions.dll.
internal sealed class PageRequestManager { private void RenderFormCallback(HtmlTextWriter writer, Control containerControl) { ParserStringWriter innerWriter = writer.InnerWriter as ParserStringWriter; innerWriter.ParseWrites = false; ... } }
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
If I wasn't using URL Rewriting there wouldn't be any point of using this control adapter, now would there? )
I'm using UrlRewriter.Net
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
That's a good point! 
I just downloaded and implemented UrlRewriter.Net to make sure I'm not missing something. Definitely getting the null reference exception with this library. Are you using Ajax.Net or another library?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
Actually I've now got it working with the HtmlTextWriter as well. It seems because my implementation didn't have an overload constructor for TextWriter, it was some how leading to that slightly misleading null reference exception. I'll update the article accordingly and demo. Thanks.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
I like the approach. A control adapter seems like a clean way to handle this, and simple to apply in the context of an HttpModule.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Why do the attaching "programmatically"? If you put a file in App_Browsers, the framework automatically do this attachin for you:
<browsers> <browser refID="Default"> <controlAdapters> <adapter controlType="System.Web.UI.HtmlControls.HtmlForm" adapterType="FullTypeName, AssemblyName" /> </controlAdapters> </browser> </browsers>
As an example, i did the following implementation on one of my projects. I hope help somebody:
public sealed class FormsActionAdapter : ControlAdapter { protected override void Render(System.Web.UI.HtmlTextWriter writer) { base.Render(new FormControlTextWriter(writer)); } } class FormControlTextWriter : HtmlTextWriter { public FormControlTextWriter(TextWriter wri) : base(wri) { base.InnerWriter = wri; } public FormControlTextWriter(HtmlTextWriter wri) : base(wri) { base.InnerWriter = wri; } public override void WriteAttribute(string name, string value, bool fEncode) { if (name.ToLower() != "action") { base.WriteAttribute(name, value, fEncode); return;
} HttpContext ctx = HttpContext.Current; base.WriteAttribute(name, ctx.Request.RawUrl, true); } }
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi,
The reason for programmatically attaching the control adapter rather than using the App_Browers is mentioned in the article. It's to enable more pluggable components that require less configuration. If this is all handled by a HttpModule (such is the case with a url rewriter), then it's simply 1 line in the web.config, and much less to go wrong when used by third parties.
I also mentioned the above approach with the HtmlTextWriter for fixing the action attribute. Whereas I much prefer this approach, it does NOT work when using Ajax.Net. It throws an exception when the web extensions library tries to cast the custom writer to ParserStringWriter, resulting in a null reference exception.
Mike
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
You're right, you can declare the control adapter in web.config but I think the point the author was making was that in the broader context of an HttpModule (presumably a Url re-writing module) it is simple to programmatically register a control adapter, saving the user of the library the trouble of having to configure an additional section in web.config.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|