Generating strongly typed content links in ASP .Net MVC with T4 templates – Part 2

In my previous post I took a look at generating the static content links we use in ASP .Net using T4 templates.  There were a couple of issues which I pointed out and some others which were pointed out to me.

<head runat="server">
  <link href="<%= Url.Content(Content.Site) %>" rel="stylesheet" type="text/css" />
</head>

My code is wrong

In more than one way.  Take a look at the generated code:

public static class Content
{
    public static class Images
    {
        public const string Add = "~/Content/Content/Images/Add.png";
        public const string Check = "~/Content/Content/Images/Check.png";
        public const string Delete = "~/Content/Content/Images/Delete.png";
        public const string Edit = "~/Content/Content/Images/Edit.png";
        public const string FolderBackground = "~/Content/Content/Images/FolderBackground.gif";
        public const string FolderBottom = "~/Content/Content/Images/FolderBottom.gif";
        public const string FolderTop2 = "~/Content/Content/Images/FolderTop2.gif";
        public const string Guy = "~/Content/Content/Images/Guy.png";
        public const string Mail = "~/Content/Content/Images/Mail.png";
        public const string Papers = "~/Content/Content/Images/Papers.jpg";
    }

    public const string Site = "~/Content/Content/Site.css";
}

Yup, I missed it – the generated links are actually wrong. I obviously got too excited that it was actually working (after fighting the T4 monsters for quite a while). It’s an easy fix. The root path should simply change from

void GenerateLinksForFolder(Project mainProject, string folderName)
{
    var projectItem = GetProjectItem(mainProject, folderName);
    RecursivelyIterateThroughFolder(projectItem, string.Format("~/{0}/", folderName));
}

to

void GenerateLinksForFolder(Project mainProject, string folderName)
{
    var projectItem = GetProjectItem(mainProject, folderName);
    RecursivelyIterateThroughFolder(projectItem,"~/");
}

The next mistake I made was with the rendering of the link tag for the stylesheet.

<head runat="server">
  <link href="<%= Url.Content(Content.Site) %>" rel="stylesheet" type="text/css" />
</head>

The rendered link looks like this:

RenderedLink

David Ebbo pointed out what was happening as well as an obvious workaround.

When you use the expression with the double quotes, the parser gets confused because they conflict with the double quotes for the href attribute. So it doesn’t treat the link tag as anything special. But with the improved expression, there are no double quotes. Workaround? Add +””. I know, not pretty, there may be a better one.

The problem is only specific to the head section – if I put the exact same code in the body section the rendered code is correct.

In my previous post I also mentioned that I’m not replacing the entire Url.Content call – I’m simply trying to remove the dependency on strings.  However, in this instance I think replacing the entire link tag would be the best option.  I simply added 2 extension methods.

public static class ContentLinkHelpers
{
    public static string Stylesheet(this UrlHelper helper, string content)
    {
        return string.Format("<link href=\"{0}\" rel=\"stylesheet\" type=\"text/css\" />", helper.Content(content));
    }

    public static string Script(this UrlHelper helper, string content)
    {
        return string.Format("<script src=\"{0}\" type=\"text/javascript\"></script>", helper.Content(content));
    }
}

This allows us to render stylesheet (and scripts) using the helper methods.

<head runat="server">
  <%= Url.Stylesheet(Content.Site) %>
  <%= Url.Script(Scripts.Jquery132) %>
</head>

I usually prefer not to use helpers which generate complete html elements since (in my opinion) it makes the code less readable, but in this case the syntax works very nicely.

Sanitize the input

Something that I didn’t mention (which actually works correctly, * gasp *) is the need to sanitize the input.  For example, the script named Jquery-1.3.2-Vsdoc.js needs a variable name similar to Jquery132Vsdoc.  I did the transformation using a regular expression.

return Regex.Replace(name, "[^a-zA-Z0-9_]", string.Empty);

While this will work for the Jquery-1.3.2-Vsdoc.js example, we still need to exclude all C# keywords.  For example, an image named ‘lock’ will generate compilation errors since ‘lock’ is a reserved word.

Unfortunately there is no way to access a list of C# keywords programmatically – the best we can do is to maintain a list in code and check against this list. 

foreach(var keyword in keywords)
{
    if (keyword.Equals(sanitized))
    {
        return "@" + sanitized;
    }
}

return sanitized;

This will correctly generate variable names for any content.

public const string @lock = "~/Content/Images/lock.png";

Using MVC T4 templates

As I mention, the obvious next step is for a single template that can be included in any project. Thanks to David Ebbo the MVC T4 template (now named T4MVC) is now available on CodePlex, as one of the downloads in the ASP.NET MVC v1.0 Source page.