A new and improved ASP.NET MVC T4 template

Update: Please read this post for the newest and greatest.

A couple weeks ago, I blogged about using a Build provider and CodeDom to generate strongly typed MVC helpers at runtime.  I followed up a few days later with another version that used T4 templates instead, making it easier to customize.

And now I’m back with yet another post on this topic, but this time with a much simpler and improved approach!  The big difference is that I’m now doing the generation at design time instead of runtime.  As you will see, this has a lot of advantages.

Drawbacks of the previous runtime approach

Before we go and re-invent the wheel, let’s discuss what the issues with the runtime T4 approach were, and how this is solved by this new approach.

Complex configuration: to enable the runtime template, you had to add a DLL to your bin, modify two web.config files, and drop two T4 files in different places.  Not super hard, but also not completely trivial.  By contrast, with this new approach you just drop one .tt file at the root of your app, and that’s basically it.

No partial trust support: because it was processing T4 files at runtime, it needed full trust to run.  Not to mention the fact that using T4 at runtime is not really supported!  But now, by doing it at design time, this becomes a non-issue.

Only works for Views: because only the Views are compiled at runtime, the helpers were only usable there, and the controllers were left out (since they’re built at design time).  With this new approach, Controllers get some love too, because the code generated by the template lives in the same assembly as the controllers!

Let’s try the new T4 template with the Nerd Dinner app

Let’s jump right in and see this new template in action!  We’ll be using the Nerd Dinner app as a test app to try it on.  So to get started, go to https://nerddinner.codeplex.com/, download the app and open it in Visual Studio 2008 SP1.

Then, simply drag the T4 template (the latest one is on CodePlex) into the root of the NerdDinner project in VS.  And that’s it, you’re ready to go and use the generated helpers!

Once you’ve dragged the template, you should see this in your solution explorer:

image

Note how a .cs file was instantly generated from it.  It contains all the cool helpers we’ll be using!  Now let’s take a look at what those helpers let us do.

Using View Name constants

Open the file Views\Dinners\Edit.aspx.  It contains:

 <% Html.RenderPartial("DinnerForm"); %>

This ugly “DinnerForm” literal string needs to go!  Instead, you can now write:

 <% Html.RenderPartial(MVC.Dinners.Views.DinnerForm); %>

Though it’s wordier, note that you get full intellisense when typing it.

Now open Views\Dinners\EditAndDeleteLinks.ascx, where you’ll see:

 <%= Html.ActionLink("Delete Dinner", "Delete", new { id = Model.DinnerID })%>

Here we not only have a hard coded Action Name (“Delete”), but we also have the parameter name ‘id’.  Even though it doesn’t look like a literal string, it very much is one in disguise.  Don’t let those anonymous objects fool you!

But with our cool T4 helpers, you can now change it to:

 <%= Html.ActionLink("Delete Dinner", MVC.Dinners.Delete(Model.DinnerID))%>

Basically, we got rid of the two unwanted literal strings (“Delete” and “Id”), and replaced them by a very natural looking method call to the controller action.  Of course, this is not really calling the controller action, which would be very wrong here.  But it’s capturing the essence of method call, and turning it into the right route values.  And again, you get full intellisense:

 image

By the way, feel free to press F12 on this Delete() method call, and you’ll see exactly how it is defined in the generated .cs file.  The T4 template doesn’t keep any secrets from you!

Likewise, the same thing works for Ajax.ActionLink.  In Views\Dinners\RSVPStatus.ascx, change:

 <%= Ajax.ActionLink( "RSVP for this event",
                     "Register", "RSVP",
                     new { id=Model.DinnerID }, 
                     new AjaxOptions { UpdateTargetId="rsvpmsg", OnSuccess="AnimateRSVPMessage" }) %>

to just:

 <%= Ajax.ActionLink( "RSVP for this event",
                     MVC.RSVP.Register(Model.DinnerID),
                     new AjaxOptions { UpdateTargetId="rsvpmsg", OnSuccess="AnimateRSVPMessage" }) %>

You can also do the same thing for Url.Action().

Even the controller gets a piece of the action

As mentioned earlier, Controllers are no longer left out with this approach.

e.g. in Controllers\DinnersController.cs, you can replace

 return View("InvalidOwner");

by

 return View(MVC.Dinners.Views.InvalidOwner);

But to make things even more useful in the controller, you can let the T4 template generate new members directly into your controller class.  To allow this, you just need to make you controller partial, e.g.

 public partial class DinnersController : Controller {

Note: you now need to tell the T4 template to regenerate its code, by simply opening the .tt file and saving it.  I know, it would ideally be automatic, but I haven’t found a great way to do this yet.

After you do this, you can replace the above statement by the more concise:

 return View(View_InvalidOwner);

You also get to do some cool things like we did in the Views.  e.g. you can replace:

 return RedirectToAction("Details", new { id = dinner.DinnerID });

by

 return RedirectToAction(MVC.Dinners.Details(dinner.DinnerID));

 

How does the T4 template work?

The previous runtime-based T4 template was using reflection to learn about your controllers and actions.  But now that it runs at design time, it can’t rely on the assembly already being built, because the code it generates is part of that very assembly (yes, a chicken and egg problem of sort).

So I had to find an alternative.  Unfortunately, I was totally out of my element, because my expertise is in the runtime ASP.NET compilation system, while I couldn’t make use of any of it here!

Luckily, I connected with a few knowledgeable folks who gave me some good pointers.  I ended up using the VS File Code Model API.  It’s an absolutely horrible API (it’s COM interop based), but I had to make the best of it.

The hard part is that it doesn’t let you do simple things that are easy using reflection.  e.g. you can’t easily find all the controllers in your project assembly.  Instead, you have to ask it to give you the code model for a given source file, and in there you can discover the namespaces, types and methods.

So in order to make this work without having to look at all the files in the projects (which would be quite slow, since it’s a slow API), I made an assumption that the Controller source files would be in the Controllers folder, which is where they normally are.

As for the view, I had to write logic that enumerates the files in the Views folder to discover the available views.

All in all, it’s fairly complex and messy code, which hopefully others won’t have to rewrite from scratch.  Just open the .tt file to look at it, it’s all in there!

In addition to looking at the .tt file, I encourage you to look at the generated .cs file, which will show you all the helpers for your particular project.

Known issues

T4 file must be saved to regenerate the code

This was briefly mentioned above.  The T4 generation is done by VS because there is a custom tool associated with it (the tool is called TextTemplatingFileGenerator – you can see it in the properties).  But VS only runs the file generator when the .tt file changes.  So when you make code changes that would affect the generated code (e.g. add a new Controller), you need to explicitly resave the .tt file to update the generated code.  As an alternative, you can right click on the .tt file and choose “Run Custom Tool”, though that’s not much easier.

Potentially, we could try doing something that reruns the generation as part of a build action or something like that.  I just haven’t had time to play around with this.  Let me know if you find a good solution to this.

No refactoring support

This was also the case with the previous template, but it is worth pointing out.  Because all the code is generated by the T4 template, that code is not directly connected to the code it relates to.

e.g. the MVC.Dinners.Delete() generated method results from the DinnersController.Delete() method, but they are not connected in a way that the refactoring engine can deal with.  So if you rename DinnersController.Delete() to DinnersController.Delete2(), MVC.Dinners.Delete() won’t be refactored to MVC.Dinners.Delete2().

Of course, if you resave the .tt file, it will generate a MVC.Dinners.Delete2() method instead of MVC.Dinners.Delete(), but places in your code that call MVC.Dinners.Delete() won’t be renamed to Delete2.

While certainly a limitation, it is still way superior to what it replaces (literal strings), because it gives you both intellisense and compile time check.  But it’s just not able to take that last step that allows refactoring to work.

It is worth noting that using Lamda expression based helpers instead of T4 generation does solve this refactoring issue, but it comes with a price: less natural syntax, and performance issues.

Final words

It has been pretty interesting for me to explore those various alternative to solve this MVC strongly typed helper issue.  Though I started out feeling good about the runtime approach, I’m now pretty sold on this new design time approach being the way to go.

I’d be interested in hearing what others think, and about possible future directions where we can take this.