July 2008 - Posts
On my last post I wrote about a solution for the problem that arises when we try the use path infos and ASP.NET Themes and Skins together.
Raj Kaimal suggested rewriting all LINK HTML elements URLs to the correct URL as seen from the client. Something like this:
void HttpApplicationPreRequestHandlerExecute(object sender, System.EventArgs e)
{
var httpApplication = sender as System.Web.HttpApplication;
var httpContext = httpApplication.Context;
var page = httpContext.CurrentHandler as System.Web.UI.Page;
if ((page != null) && !string.IsNullOrEmpty(httpContext.Request.PathInfo))
{
page.PreRenderComplete += delegate(object source, System.EventArgs args)
{
var p = source as System.Web.UI.Page;
foreach (System.Web.UI.Control headerControl in p.Header.Controls)
{
var link = headerControl as System.Web.UI.HtmlControls.HtmlLink;
if (link != null)
{
link.Href = p.ResolveUrl(link.Href);
}
}
};
}
}
With this approach you still have a problem (which mine didn’t solve) with post backs because the rendering of the ACTION of the HTML FORM is also broken.
Israel Aéce suggested the use of the BASE HTML element to re-base relative URLs. Something like this:
void HttpApplicationPreRequestHandlerExecute(object sender, System.EventArgs e)
{
var httpApplication = sender as System.Web.HttpApplication;
var httpContext = httpApplication.Context;
var page = httpContext.CurrentHandler as System.Web.UI.Page;
if ((page != null) && !string.IsNullOrEmpty(httpContext.Request.PathInfo))
{
page.PreRenderComplete += delegate(object source, System.EventArgs args)
{
page.Init += delegate(object source, System.EventArgs args)
{
var p = source as System.Web.UI.Page;
var htmlBase = new System.Web.UI.WebControls.WebControl(System.Web.UI.HtmlTextWriterTag.Base);
htmlBase.Attributes.Add("href", p.Request.Url.GetLeftPart(System.UriPartial.Authority) + p.Request.CurrentExecutionFilePath);
p.Header.Controls.Add(htmlBase);
};
};
}
}
This seems like the better solution except if your site sits behind several security perimeters and it is not possible to be sure what the domain is as seem from the client side, which was my problem to begin with.
But if you are thinking of calling Server.Execute, Server.TransferRequest or Server.TransferRequest, neither of these two solutions will work.
Updated on 2008.07.28 – The code was done in a hurry and, talking to my friend Luís, I noticed that I had forgotten to make a case insensitive comparison and that the code was not so obvious. So, I updated the code and added an explanation.
On my last post I wrote about the problem that arises when we try the use path infos and ASP.NET Themes and Skins together.
But most of the times you don’t care about the why you can’t. You just want to know how you can.
The way I see it, the right solution would be to render the URLs for the stylesheets rooted.
But since I can’t do that, the next best thing is the serve the wrongly addressed request properly.
But how can we do that?
The only way I could come up with, was an HTTP Module:
public class AppThemesModule : global::System.Web.IHttpModule
{
private const string LocalThemesFolderName = "/App_Themes/";
private static readonly int searchStartIndex;
private static readonly int minimumLenghtForSearch;
static AppThemesModule()
{
int searchStartIndex = System.Web.HttpRuntime.AppDomainAppVirtualPath.Length;
AppThemesModule.searchStartIndex = ((searchStartIndex == 1) ? 0 : searchStartIndex) + 2;
AppThemesModule.minimumLenghtForSearch = AppThemesModule.searchStartIndex + AppThemesModule.LocalThemesFolderName.Length;
}
#region IHttpModule Members
public void Dispose()
{
}
public void Init(System.Web.HttpApplication context)
{
context.BeginRequest += HttpApplicationBeginRequest;
}
#endregion
void HttpApplicationBeginRequest(object sender, System.EventArgs e)
{
System.Web.HttpApplication httpApplication = sender as System.Web.HttpApplication;
string path = httpApplication.Request.Path;
if (path.Length > searchStartIndex)
{
int appThemesStartIndex = path.IndexOf(AppThemesModule.LocalThemesFolderName, searchStartIndex, System.StringComparison.OrdinalIgnoreCase);
if (appThemesStartIndex > 0)
{
httpApplication.Context.RewritePath("~" + path.Substring(appThemesStartIndex));
}
}
}
}
The code starts by initializing the static read-only field searchStartIndex with the start index of the search for the /App_Themes/ pattern. If the length of the application’s virtual path is 1, that means that it’s the root of the site and search start index is 0 instead of 1; otherwise the search start index will be the length of the application’s virtual path. 2 is added because there is no need to start searching the path just after the application’s virtual path (if the pattern was found just after the application’s virtual path, no replacement would be needed).
Than, the static read-only field minimumLenghtForSearch is initialized with the minimum length of the path to search for the pattern. There is no need to search for the pattern on paths shorter than the search start index plus the length of the pattern because, if found, no replacement would be needed.
Besides registering the module, you’ll have to configure your virtual directory so that all the files to be served out of the themes are handled by a StaticFileHandler.
If you ever worked with ASP.NET Themes and Skins, you know that stylesheet links are added to the head section of the HTML document.
The rendered URL to these stylesheets is always relative to location of the page being requested.
So, for a request to:
http://MySite/Section/Default.aspx
you'll get:
<link href="../App_Themes/Default/Styles.css" type="text/css" rel="stylesheet" />
which will make the web browser request for:
http://MySite/App_Themes/Default/Styles.css
and it all works fine.
Well, it works fine until you need to navigate to:
http://MySite/Section/Default.aspx/PathInfo
You'll get the same stylesheet reference and the browser will request for:
http://MySite/Section/Default.aspx/App_Themes/Default/Styles.css
This happens because the web browser has no knowledge of what PathInfos are. It only accounts for the number of forward slashes (/).
I've filed a bug on Microsoft Connect about this.
I find the System.Web.HttpValueCollection class very useful in a wide number of situations that involve composing HTTP requests or any other need to represent name/value collection as a string (in an XML attribute, for example).
As of now (.NET Framework 3.5 SP1 Beta), the only way to create an instance of the System.Web.HttpValueCollection class is using the System.Web.HttpUtility.ParseQueryString method.
I’d like to see it public and available in a more generic assembly like System.DLL to be available on every type of .NET application (Windows Client applications, Windows Service applications, Silverlight applications, Windows Mobile applications, etc.).
If you agree with me, vote on my suggestion on Microsoft Connect.
If you run this code:
System.Collections.Specialized.NameValueCollection queryString = System.Web.HttpUtility.ParseQueryString("noKey&=emptyKey&A=Akey");
queryString will actually have the running type of System.Web.HttpValueCollection.
What's great about this class is that its ToString method output is the collection's content in a nice URL encoded format.
As with its base class (NameValueCollection), there’s a difference between a null string and an empty string key and the parsing treats query string parameters with no parameter specification as having a null string key and parameters with an empty string key having an empty string parameter key.
So, when call ToString on the instance returned by System.Web.HttpUtility.ParseQueryString method you would expect to get the parsed string (or, at least, one that would be parsed into the equivalent collection), right? But what you’ll get instead is this: noKey&emptyKey&A=Akey.
I’ve filed a bug into connect. If you think this is important and must be corrected, please vote.
Today Typemock released version 4.3 of Typemock Isolator. Download it from here.
What’s new?
- Support for Ivonna. For those of you who develop ASP.Net applications, Ivonna is a great tool, built on top of Isolator’s platform, to simplify writing tests for ASP.NET.
- Typemock.Integration.Packs namespace APIs added to support license management through Isolator, the way Ivonna does.
- As announced when it was released, version 4.2 was the last version of Isolator to support .NET 1.1. Version 4.3 only supports the 2.0 runtime and its Visual Studio counterparts: VS2005 and VS2008.
- For 64 bit machines, now there’s a single installer. (don’t forget to uninstall both previous 32 and 64 installers prior to installing 4.3.)
- RecorderManager.GetMockOf(instanceRef) and MockManager.GetMockOf(instanceRef). To retrieve the mock controller object based on a reference to the instance. (more...).
Bug fixes:
- Fixes to DLINQ support. LINQ Queries with data tables now work better, for example with GroupBy.
- Static constructors in Natural Mocks are now invoked correctly.
- A bug that caused an exception to be thrown when mocking interfaces ("Method XX in type IMyInterface has no matching overload that returns TypeMock.Mock+a) was fixed.
- A bug that caused an exception to be thrown when the mocked object was overriding Equals was fixed.
- A bug that caused failure in mocking explicit interface with the same name was fixed.
- A bug occurring im multithreading scenarios in VerifyWithWait was fixed.
- A bug that causes NullReferenceException to be thrown when using Auto Deploy was fixed.
See also: