Edit: I have made some corrections to this post in a subsequent post.
I read an interesting article by David Ebbo on generating strongly typed helpers for ASP .Net MVC using T4 templates. I have used ASP .Net MVC on a recent project and while I really like the framework (and the testability) the use of string literals made major changes difficult and error-prone.
T4 templates seem ideal for this type of scenario – if we can generate classes for the content that we are referencing via strings, we should be able to use strongly typed links instead of string literals.
In the link I mentioned above David takes a look at generating links using strongly-typed helpers. I will be taking a look at referencing static content such as images, scripts or stylesheets.
What we are trying to achieve
At the moment, when we wish to reference static content such as a stylesheet, we need to do the following.
The obvious problem comes in when we move this content – because our links are not strongly typed, we can easily introduce errors. This is an even bigger problem with changing controller names and actions.
What I would like to do is the following.
Note that I’m not replacing the Url.Content call – this is actually an instance of the UrlHelper class which is instantiated with the correct RequestContext – we can’t replace it. We could use extension methods, but the syntax is much neater using properties and static classes.
Using T4 templates
T4 templates are a built into Visual Studio and is incredibly useful in all kinds of code generation scenarios. When compared to the CodeDOM model of code generation, I find the concept of T4 templates to be simpler but much harder to implement.
If you’ve never used T4 templates before, keep in mind that Visual Studio treats them as ordinary text files. This means you have no intellisense or code completion. However, there are a whole bunch of Visual Studio plug-ins that add this functionality – the one that I used is the T4 Editor from Tangible Engineering.
Let’s write some code
The first thing we need to do is to find the Visual Studio project that our T4 template file resides in. This may seem a little confusing at first, but it makes sense – we could be using this template to generate anything – in this case I know the template file will be in the root folder of the web project. The code here may seem rather tricky, but if you’ve created a Visual Studio plug-in before it should be pretty easy. Most of the credit for this section goes to David Ebbo.
<#
// First just get the visual studio project this file belongs to
var dte = (DTE)((IServiceProvider)Host).GetService(typeof(SDTE));
Project project = GetProjectContainingT4File(dte, Path.GetFileName(Host.TemplateFile));
GenerateLinksForFolder(project, "Scripts");
GenerateLinksForFolder(project, "Content");
#>
Once you have the project object, simply select the folder that you want to generate static classes for.
void RecursivelyIterateThroughFolder(ProjectItem item, string currentPath)
{
var fileNameWithoutExtension = Sanitize(Path.GetFileNameWithoutExtension(item.Name));
var fileName = Path.GetFileName(item.Name);
if (item.ProjectItems.Count == 0)
{
#>
public const string <#=fileNameWithoutExtension#> = "<#=currentPath + fileName#>";
<#+
}
else
{
#>
public static class <#=fileNameWithoutExtension#>
{
<#+
foreach(ProjectItem childItem in item.ProjectItems)
{
RecursivelyIterateThroughFolder(childItem, currentPath + fileName + "/");
}
#>
}
<#+
}
}
The generated code looks like this (I fixed the indentation, which is difficult to get right with the code generation).
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";
}
And now we can generate images using strongly typed links.
Pretty neat. The obvious next step would be to create a generic template that you can include in any MVC project (for content, controllers, views, etc).
There are 2 issues with this approach:
- The generated content is only created when the T4 template is saved. I tried to add a post-build event to generate the content when compiling, but my use of the host object caused issues. I need to try and find a neat way of doing this.
- When publishing the website, we obviously don’t want to include the template file – I want to try and see what options I have here. It might be as simple as excluding the template file when doing a deployment.
You can find the source code here.
7 comments:
Nice! I might steal back some of your ideas and integrate them into my template :)
Your issue #1 is exactly the same one I'm struggling with. I'm trying to get help from the VS experts. I'll let you know if they find a good solution.
I'll create a Code Gallery project for it soon to make it easier to discuss and improve.
David
Thanks - good idea to create a Code Gallery project, let me know when it's up.
I'm going to try and fix that issue tonight - see if I can load the project file itself...
Warning: This is my first time working with T4 :)
I've been fighting for about an hour with compile errors and debuging until I found my problem is that I want to put the .tt file some place else beside the root. Specificly; I am tring to put it in ~/Core/Templates/*.tt
Ouch - debugging is probably the main problem with T4 templates. That's what I meant when I said the concept is simple (similar to Asp), but the implementation is very tricky.
This was my first time working with T4 as well :)
Is the Site.css link as you have it above working for you? It *shouldn't* work because I don't think code is allowed in this context (link tags are parsed differently). It ends up getting turned into something where the < and % get encoded, so the css is broken.
Well spotted - it doesn't work - the < % tags actually get rendered. Strange - if I change it back to
Url.Content("~/Content/Site.css")
it gets rendered correctly... But that's still code being executed on the server - what gives?
Ah, I get it. 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.
Post a Comment