The <link> saga on ASP .Net

Indeed, there is a saga. Since from the very beginning of my ASP .Net experiences I had to deal with the "CSS problem". This problem occurs when the referenced CSS is not loaded, because the relative path is not given right.

We place in the Master Page all the common html used in the website, including the links to the CSS files. Pages might be placed in different nested folders and because of this, the link to the CSS file needs to be adjusted with the relative prefixes. Since the link is placed in the Master Page and it is not known in advantage the website structure, the solution should solve the problem of how is the right prefix added to the CSS link.

HTML introduced Relative Uniform Resource Locators ( see [Page 11]) which actually solves this problem. Instead of writing the absolute paths (ie. http://www.mysite.com/public/main.css ), it suffices to use the relative paths, like public/main.css. That RFC explains the algorithm of how the prefixes ( /, ./, ../, ../../ so on) are going to be solved.

ASP .Net introduced the notion of the Virtual Directory and Application. For example www.mysite.com/admin might be an application different than the one on www.mysite.com, each with their own purposes and resources. The tilde "~" prefix has been added by ASP .Net to add an extra level of the relativity: to be "relative" to an application.
1. /public/main.css - it is translated as http://www.mysite.com/public/main.css no matter the place where it is referenced (ie from the root application or from the admin application)
2. ~/public/main.css - it is translated as http://www.mysite.com/public/main.css if the calling page is from the root application and http://www.mysite.com/admin/public/main.css if the calling page is from admin application.

Before going to the CSS problem, a small notice about the <head> element. If the runat="server" is specified then it is transformed in a server control. This means the generated code to build the page will contain a code snippet to create an instance of HtmlHead class. If runat="server" is not specified, the generated code will simple contain instructions for the writer to write the text found from (and including) <head> to the next server control or one of the instructions <%= ... %>, <%# ... %>, <%: ... %> or <%#: ... %> . It is important to know this, because if the head is a server control, then the links will be its child controls otherwise simply text passed to the writer.

Now back to the CSS problem, with the following structure I will try to give some explanations.




<head>
<link href='/public/main.css' type='text/css'/>
</head>

ASP .Net it passes this text to the writer (because head is not a server control). Every page will have the relative CSS translated to the following absolute path www.mysite.com/public/main.css. When the master page is located in the admin application and uses the same scheme it won't work as expected. The root is still http://www.mysite.com/ and not http://www.mysite.com/admin/. Here comes the ResolveUrl method.

<head>
<link href='<%= ResolveUrl("~/public/main.css")%>' type="text/css" />
</head>

Again, the text is passed to the writer until <%= instruction. Then the ResolveUrl is evaluated and its result is passed to writer. The output of this method is /admin/public/main.css because the tilde prefix says that it needs to take the application as the root of the page and not http://www.mysite.com.

What happens when head becomes a server control? As I said, ASP .Net won't pass the text to writer, it will auto-generate code for building an HtmlHead. The text inside the html tag it is parsed too and based on the results either it will pass text to the writer, either it will generate code to build HtmlTitle, HtmlMeta and HtmlLink instances. So if it finds <link href="/public/main.css" type="text/css" /> then it generates the following instructions:

@__ctrl = new global::System.Web.UI.HtmlControls.HtmlLink();
@__ctrl.Href = "/public/main.css";
((System.Web.UI.IAttributeAccessor)(@__ctrl)).SetAttribute("type", "text/css");

Now, it makes sense to use tilde (~) here, if it is needed. Also there is no reason to add runat="server" to the link, ASP .Net ignores it. I noticed that the output is different than ResolveUrl method, because ResolveUrl it uses the application name (admin in this case) and the HtmlLink control it renders the relative prefixes in such way the application name is avoided (public/main.css or ../public/main.css or ../../ so on).

Can the ResolveUrl method to be used with link and the html server control? Yes, but we need to trick ASP .Net parser. Let't try it first as "the normal" way, like this:

<head runat="server">
<link href='<%= ResolveUrl("~/public/main.css") %>' type="text/css" />
</head>

Unfortunately this will output some giberish:

<head id="Head1"><link href="&lt;%= ResolveUrl(&quot;~/public/main.css&quot;) %>" type="text/css" />
</head>

And the explanation is: ASP .Net it "sees" the instruction <%= ResolveUrl("~/public/main.css") %> as a plain text and it assigns it to the href attribute.

@__ctrl = new global::System.Web.UI.HtmlControls.HtmlLink();
@__ctrl.Href = "<%= ResolveUrl(\"~/public/main.css\") %>";
((System.Web.UI.IAttributeAccessor)(@__ctrl)).SetAttribute("type", "text/css");

By tricking the parser it means confusing it that link is not a text for HtmlLink.


<head runat="server">
<link <%= "href='" + ResolveUrl("~/public/main.css") + "'" %> type="text/css" />
</head>

What's the best practice? I believe the answer is, as always, it depends.
If the relative point it is the site itself than the / prefix ( it means start from the root of the website) it suffices and it doesn't matter if head is a server control or not ( and it shouldn't).
If the resource is relative to an application and the head is not a server control there is no way than using ResolveUrl method combined with tilde (~), but if the head is a server control there is no need to use ResolveUrl method, because the link is "transfomed" into a HtmlLink control and its Href property is assigned with the href value. If it is really needed to use ResolveUrl method ( or any other ?!, actually to use a <%= instruction inside the link element ), then the ASP .Net parser needs to be tricked.

Comments

Popular posts from this blog

IIS 7.5, HTTPS Bindings and ERR_CONNECTION_RESET

Table Per Hierarchy Inheritance with Column Discriminator and Associations used in Derived Entity Types

Verify ILogger calls with Moq.ILogger