A T4 based approach to creating ASP.NET MVC strongly typed helpers
Update: Please see this newer post for the latest and greatest MVC T4 template
Earlier this week, I wrote a post on using a BuildProvider to create ActionLink helpers. That approach was using CodeDom to generate the code, and there was quite some user interest in it (and Phil blogged it, which helped!).
Then yesterday, I wrote a post on the Pros and Cons of using CodeDom vs T4 templates for source code generation. They are drastically different approaches, and while both have their strengths, T4 has definitely been getting more buzz lately.
The logical follow-up to those two posts is a discussion on using T4 templates to generate MVC strongly typed helpers. The general idea here is to use the existing ASP.NET extensibility points (BuildProvider and ControlBuilder), but rely on T4 templates to produce code instead of CodeDom. Hence, I called the helper library AspNetT4Bridge (I’m really good at naming things!).
As far as I know, this is the first time that T4 templates are executed dynamically inside an ASP.NET application, so let’s view this as an experiment, which has really not been put to the test yet. But it is certainly an exciting approach, so let’s see where it takes us!
What scenarios are we enabling
Before we get too far into the technical details, let’s look at the various MVC scenarios that this is enabling. But the great thing to keep in mind is that since T4 templates are so flexible, it is very easy to tweak the generated code or add additional scenarios. Hence it is best to view those scenarios as examples of the type of things that you can achieve with this model, rather than as a final end result.
ActionLink helpers
First, we have the same scenario that my previous post covered, except that we’re now using T4 instead of CodeDom. Instead of writing:
<%= Html.ActionLink("Edit", "Edit", new { id = item.ID })%>
You can write the more strongly typed (here, ‘Test’ is the name of the controller):
<%= Html.ActionLinkToTestEdit("Edit", item.ID) %>
And note that you get full intellisense in VS for those helpers!
Action Url helpers
This is similar to the previous section, except it covers the case where you need to generate raw URL’s rather than HTML <a> tags. Instead of writing:
<%= Url.Action("Edit", new { id = item.ID }) %>
You can write:
<%= Url.UrlToTestEdit(item.ID) %>
Constants for View Names
That’s another case where you often end up hard coding view names as literal string, e.g.
<% Html.RenderPartial("LogOnUserControl"); %>
With the generated helpers, you get to write:
<% Html.RenderPartial(Views.Shared.LogOnUserControl); %>
And again, you get full intellisense:
Rendering helpers for strongly typed views
Assume you are using a strongly typed view. You might write something like this (which is what the View wizard generates by default):
<label for="Name">Name:</label>
<%= Html.TextBox("Name", Model.Name) %>
<%= Html.ValidationMessage("Name", "*") %>
With the helpers, you can instead write:
<%= ViewModel.Name.Label() %>
<%= ViewModel.Name.TextBox() %>
<%= ViewModel.Name.Validation() %>
Here ViewModel is some name I came up with (well, Fowler did!) as the class that holds the helpers. There may be better names for it!
Note that the helpers can be smart about what they offer based on the data type. e.g. for a boolean field, you typically want a check box instead of a text box, and the helpers account for this:
What do you need to do to enable this in your MVC app?
There are five relatively straightforward steps you need to take to enable this.
1. Copy AspNetT4Bridge.dll into the bin folder of your app (you’ll get it by building the attached sample).
2. In the web.config file at the root of your app, register the BuildProvider for .tt files
<compilation> ...
<buildProviders>
<add extension=".tt" type="AspNetT4Bridge.AspNetT4BridgeBuildProvider"/>
</buildProviders>
</compilation>
3. In the web.config in the Views folder (which you didn’t know existed, did you?), change the pageParserFilterType as follows:
pageParserFilterType="AspNetT4Bridge.Mvc.ViewTypeParserFilter"
4. Copy the Templates and App_Code folders (each containing a .tt file) from the attached sample app to the root of your app. In VS, blank out the Custom Tool for the .tt files if you see it set to TextTemplatingFileGenerator (see this post for some details on this).
5. If you have any strongly typed views (i.e. views the inherit ViewPage<T> and not just ViewPage), you need to change the Inherits attribute from System.Web.Mvc.ViewPage<...> to AspNetT4Bridge.Mvc.ViewPage<...>. I know that’s a bit ugly, and we can think of ways to clean that up later.
So what about those T4 templates?
This post is supposed to be about using T4 templates, and so far we haven’t said a whole lot about them. They are certainly the magic piece that makes all this work. We are actually using two different .tt files, which cover two distinct scenarios:
Top level code
This applies to generated code that every page is referencing, and is used for the first three scenarios above (Action Links, Urls and View name constants). The T4 file that does this is App_Code\StrongTypedLinkExtensions.tt.
As an aside, many people have a misconception that App_Code is just for web sites and not for web apps. However, while it is rarely used, is is fully available. It’s built with a reference to the bin assemblies, and all pages are built to a reference to it.
The way we make App_Code work with T4 templates is that we have a BuildProvider registered for the .tt extension. This BuildProvider uses its own ITextTemplatingEngineHost in order to process the T4 templates into source code. It then uses a CodeDom CodeSnippetCompileUnit to wrap this code into something CodeDom understands. That’s why I called this AspNetT4Bridge, as it really bridges the CodeDom world of ASP.NET with the T4 world. Fine, you come up with a better name! :)
Look for this logic in AspNetT4BridgeBuildProvider.cs.
Page level code
This applies to code that is specific to a given page, and that we just want to inject in there. This is what’s uses for the view helpers like ViewModel.Name.TextBox() described above. The T4 file that does this is Templates\MvcHtmlRenderHelpers.tt.
This works by using a ControlBuilder and its ProcessGeneratedCode method, which I discussed in a previous post. The situation is similar to the App_Code case: we need to come up with some source code, we use T4 to generate it, and we wrap it in a CodeDom construct (in this case it’s a CodeSnippetTypeMember, because the code goes inside the class).
Look for this logic in T4CodeGenerator.cs.
But what the heck is in those T4 templates?
So I’ve talked about how they get hooked up and executed, but still haven’t said a word about what’s in them. Well, I told you where they are, so go ahead and take a look! e.g. check out App_Code\StrongTypedLinkExtensions.tt.
There is nothing really hard about how it works, but the mix of generator code and generated code certainly makes it confusing at first. Do yourself a favor and install the free Clarius Community T4 editor. It doesn’t do a whole lot, but the fact that it shows the two types of code (generator and generated) in different color goes a long way to make things clearer.
The way the Action Links and Url helpers are generated is fairly straightforward:
- It gets the list of referenced assemblies from the Host
- It uses reflection to figure out what types are controllers
- For each controllers, it finds that Action methods
- For each Action method, it generates both an ActionLine and a Url helper, custom made for that action method (hence strongly typed).
The View Name Constant helpers are also fairly simple. They just look at what directories and files are under the Views folder, and generate constants accordingly. I’ll show this part here as an example:
namespace Views {
<#
var viewsDir = HostingEnvironment.VirtualPathProvider.GetDirectory("~/Views");
foreach (VirtualDirectory dir in viewsDir.Directories) {
#>
public static class <#= dir.Name #> {
<# foreach (VirtualFile file in dir.Files) {
string viewName = Path.GetFileNameWithoutExtension(file.Name);
#>
public const string <#= viewName #> = "<#= viewName #>";
<# } #>
}
<# } #>
}
Go ahead, try changing them!
One of the big reason to use T4 over CodeDom for this code generation is to not ‘lock’ the kind of code that get generated into a binary. Instead, you can very easily tweak it, remove things you don’t care about, and generate entirely new things. All this really takes is an understanding of <# #> and <#= #> blocks, which is pretty easy when you already know how <% %> and <%= %> blocks work, since they’re pretty much the same thing!
Final thoughts
So here we are, dynamically executing T4 templates at runtime in an ASP.NET app. One big caveat that I mentioned in my previous post is that you’re not really supposed to do that! Copying from there:
CodeDom is part of the framework, while T4 is not. That means that if you need to dynamically do this at runtime (as opposed to within VS), then T4 is not really an option. Technically, it is possible to use it at runtime outside VS, but you either have to run on a machine with VS installed, or you have to copy Microsoft.VisualStudio.TextTemplating.dll into your project (which works, but is not officially supported – hopefully it will be at some point!).
So I wrote that it wasn’t an option, and next thing I blog about doing it. What’s the deal? Well, there are some things that are changing in VS 2010 that will help. In particular, they are adding the concept of preprocessing T4 templates, while today you can only fully process them all at once. With preprocessing, we’ll be able to:
- Let VS preprocess the T4 templates into some intermediate source code (that itself doesn’t requite the T4 runtime.
- Run this code at ASP.NET runtime to generate the code we care about and add it to the compilation (e.g. in App_Code as above). This takes no dependency on the T4 runtime.
Another caveat I need to mention is that T4 templates can’t be fully executed in medium trust (because they need to compile code). However, with the VS 2010 changes, that will no longer be an issue.
So while in the short term, we are breaking the rules a bit by doing this and there are some tough edges, it demonstrates some very useful concepts that can be fully supported later.
And if all else fails, I had a lot of fun playing with it all!
Comments
Anonymous
June 04, 2009
PingBack from http://aspdotnetmvc.com/buzz/default.aspxAnonymous
June 05, 2009
Earlier this week, I wrote a post on using a BuildProvider to create ActionLink helpers .  ThatAnonymous
June 05, 2009
Nice! Is there a way to invoke this as a post build step and not through web.config?Anonymous
June 05, 2009
Pat, the mechanism used here (BuildProvider and ControlBuilder) only exist at ASP.NET runtime, so there is no easy way to do it in a post build step. But there are some related approaches that process the T4 files at build time that are worth investigating as well.Anonymous
June 05, 2009
The comment has been removedAnonymous
June 06, 2009
Is it possible to make these helpers act as a fluent interface ? for example: <%=ViewModel.Age.Label().TextBox().Validation() %> ?Anonymous
June 06, 2009
Tad, the problem I see with this fluent approach is that it would output the three pieces of UI right one after the other, whereas in practice you may want them separated by some random HTML. As far as whether it's possible, I think it should be by generating the write glue. Basically, if you can write helpers by hand that will do this, you can write a T4 template that will generate such helper!Anonymous
June 06, 2009
A wonderful approach! - Such a shame Intellisense is not available when using ReSharper :(Anonymous
June 06, 2009
Ref: Resharper Intellisense Pressing Ctrl+8 to enable/disable code analysis in current file will allow you to benefit from Intellisense :)Anonymous
June 07, 2009
jcliff: glad to hear that! So I take it doing this switches back to the standard VS intellisense engine instead of ReSharper's.Anonymous
June 08, 2009
Thank you for submitting this cool story - Trackback from DotNetShoutoutAnonymous
June 17, 2009
A couple weeks ago, I blogged about using a Build provider and CodeDom to generate strongly typed MVC