Welcome to MSDN Blogs Sign in | Join | Help

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 http://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.

ActionLink helpers

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.

Published Wednesday, June 17, 2009 8:34 AM by davidebb
Filed under: , , ,

Comments

# re: A new and improved ASP.NET MVC T4 template

Hmm, maybe you could add a custom action to the compile that simply touches the TT file?

Wednesday, June 17, 2009 11:54 AM by barryd

# re: A new and improved ASP.NET MVC T4 template

Nice article. Few bits I'll be using for sure.

I was using extension methods for Redirect Links, sort of like:

RedirectTo<UserController>(u => u.Index());

Same for working out a form url, or building my routes etc...

Your way obviously reads more cleanly though. Definitely like the View("Something") fix. I hadn't done anything for that yet.

Re the above comment, just add a pre build action:

"C:\Program Files\Common Files\Microsoft Shared\TextTemplating\1.2\TextTransform" "$(SolutionDir)\MyTemplate.tt" -out "$(SolutionDir)\Output.cs"

We also run it in our NAnt build scripts, pre compile.

Wednesday, June 17, 2009 12:17 PM by Rob

# re: A new and improved ASP.NET MVC T4 template

This is a massive improvement. I'd tried the same but had been failing at the lack of reflection, I was going to look into the Common Compiler Infrastructure API to see if introspection rather than reflection would have been any more use but I don't need to now - so thanks!

Wednesday, June 17, 2009 12:24 PM by PabloBlamirez

# Pre-build step to transform T4 templates.

Check my blog post for how to set up a pre-build step.

http://melgrubb.spaces.live.com/blog/cns!A44BB98A805C8996!256.entry

Wednesday, June 17, 2009 12:34 PM by MelGrubb

# re: A new and improved ASP.NET MVC T4 template

Thanks for the suggestions on forcing the T4 generation at build time. Unfortunately, they don't quite appear to work:

Barryd suggested 'touching' the .tt file: the problem is that VS only seems to rerun the .tt file when it's modified from within VS. So using an external touch.exe has no effect. Maybe there is a way to ask VS to resave the file.

Rob and Mel suggested running TextTransform.exe as a prebuild step: while this works for some T4 templates, it won't work for this one because it uses services from the VS Host, which are not available when using the cmd line tool.  Specifically, the template calls:

   // Get the DTE service from the host

   var dte = (DTE)((IServiceProvider)Host).GetService(typeof(SDTE));

We'll find some way to make this work! :)

Wednesday, June 17, 2009 1:50 PM by davidebb

# re: A new and improved ASP.NET MVC T4 template

I am glad to see that the design time approach is looking better.  After exploring this myself the design time approach feels better and works with some of the add-ins better than the runtime approach.

Wednesday, June 17, 2009 2:07 PM by Eric Hexter

# re: A new and improved ASP.NET MVC T4 template

A fantastic Article .Is that possible to use the Sql Server for generating the front end and would that solve the problem ?

Thursday, June 18, 2009 12:29 AM by Thanigainathan

# re: A new and improved ASP.NET MVC T4 template

The design time approach is really cool.

Thursday, June 18, 2009 12:39 AM by Shiju Varghese

# re: A new and improved ASP.NET MVC T4 template

well, isn't that deprecated with the help of strongly typed HTML helpers using lambda syntax out there (from contrib to google)?

you can even make some for yourself, I might be wrong but can someone enlighten me why would I use these T4 instead of getting better HTML helpers?

thanks

Thursday, June 18, 2009 8:44 AM by cowgaR

# re: A new and improved ASP.NET MVC T4 template

You should be able to right-click a .tt file in the solution manager and "run custom tool" instead of re-saving, to manually trigger regeneration.

Thursday, June 18, 2009 10:09 AM by Rick

# re: A new and improved ASP.NET MVC T4 template

cowgaR: Lambda based solutions do provide an alternative, but with several drawbacks:

- The syntax is more complex. Personally I don't mind it, but I heard that from many people

- It has some perf issues

- It has no support for View Names

In the end, use what works best for you!

Thursday, June 18, 2009 11:22 AM by davidebb

# re: A new and improved ASP.NET MVC T4 template

Rick: yes, you can right click instead of Saving. In any case, it's still a manual step that would ideally be automated.

Thursday, June 18, 2009 11:23 AM by davidebb

# re: A new and improved ASP.NET MVC T4 template

I have added the Mvc-CodeGen.tt file to the root of my MVC project (it is right in between the Global.asax and Web.config) but I keep getting this error: "Running transformation: Could not find the VS Project containing the T4 file. It needs to be at root of the project."

I am sure that I am doing or missing something stupid but for the life of me, I can't see what it is.

Thanks!

Thursday, June 18, 2009 2:08 PM by Richard

# re: A new and improved ASP.NET MVC T4 template

Richard: I have not seen this, but I haven't tried it on that many projects. If you can zip up your project (or a simplified version of up) and email it to me (david.ebbo @ microsoft.com), I'll be glad to take a look.

Thursday, June 18, 2009 2:22 PM by davidebb

# David Ebbo's blog : A new and improved ASP.NET MVC T4 template

DotNetBurner - burning hot .net content

Thursday, June 18, 2009 9:04 PM by DotNetBurner - ASP.net MVC

# re: A new and improved ASP.NET MVC T4 template

David, thank you very much for your offer but I think that I have found the problem. The MVC project in my solution is in a Solution Folder which seems to be the source of the problem. I made a copy of the project into a new blank solution, in preparation to send it to you. Of course, the T4 template started working. So I created a solution folder and moved the project to the solution folder. Attempting to run the template again caused the same error that I mentioned before about not being at the root of the project.

By the way, I think what you have done is great and I can't wait to get it working.

Friday, June 19, 2009 4:33 PM by Richard

# re: A new and improved ASP.NET MVC T4 template

Richard, thanks for reporting this issue. I put in a fix so it should now work inside solution folders. Please let me know how that works for you.

Friday, June 19, 2009 9:22 PM by davidebb

# re: A new and improved ASP.NET MVC T4 template

Just to confirm. This T4 methods have same performance that HtmlHelpers?

This approach is perfect! I will implement in my MVC applications!!

Resave, and no refactor is a little bit.

Thx

Monday, June 22, 2009 7:06 AM by Felipe Fujiy

# re: A new and improved ASP.NET MVC T4 template

I founded a bug.

If a have a controller AbcController with a method Abc() generated code creates a Abc class, with a method Abc(Member names cannot be the same as their enclosing type)

Monday, June 22, 2009 7:15 AM by Felipe Fujiy

# re: A new and improved ASP.NET MVC T4 template

David, the fix worked and got rid of the project root error when using a solution folder. However, I ran into another issue like the Felipe reported above.

I have a controller supertype that is named such that when a class is generated using the name of the supertype minus the word controller, it results in conflicts with the root namespace of the project:

MVC Project Namespace: SomeName.Mvc

Controller Supertype: SomeNameController

Generated Code Contains Class Named: SomeName

This causes conflicts elsewhere in the generated code.

I can take care of this with renaming or moving the supertype declaration out of the Controllers folder, but I thought that I would mention it just the same.

Still love the work - keep it up!

Monday, June 22, 2009 12:18 PM by Richard

# re: A new and improved ASP.NET MVC T4 template

Felipe: yes, using this T4 should have no perf impact. In fact, it's a bit faster than using anonymous objects.

Felipe and Richard: I fixed the name class issue by appending an underscore when it would conflict.  e.g. the method would be called SomeName_(). Not pretty, but hopefully it's not super common.

Monday, June 22, 2009 9:53 PM by davidebb

# re: A new and improved ASP.NET MVC T4 template

I am trying to get this to work in VS2010 Beta 1 + MVC (installed with the OOB MVC Installer).

I get this error: "Running transformation: System.Runtime.Serialization.SerializationException: Type 'Microsoft.VisualStudio.CSharp.Services.Language.CodeModel.CEnumerator' in Assembly 'Microsoft.VisualStudio.CSharp.Services.Language, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' is not marked as serializable."

After some investigation, the error seems to be propping up in this iteration: "foreach (CodeNamespace ns in GetNamespaces(item.FileCodeModel.CodeElements))" in the ProcessAllControllers method. The iteration simply fails with the above error.

I can confirm this is not anything specific to my project. I tried this on NerdDinner and got the same error.

Has anyone been able to get this working with VS2010 Beta 1?

(I can post a full stack trace if needed.)

Monday, June 22, 2009 11:55 PM by Rahul Ravindran

# re: A new and improved ASP.NET MVC T4 template

Rahul: I had not tried with VS2010, but I just did and I see the same thing as you. It looks like some kind of VS breaking change. I will contact the experts in that area to help investigate.

Tuesday, June 23, 2009 1:10 AM by davidebb

# re: A new and improved ASP.NET MVC T4 template

These are working great, nice work David.

One comment on the organization. Is it feasible to organize the classes in a more hierarchal manner? e.g. MVC.Views.Shared.XYZ and MVC.Controllers.Home.Index() and so forth. I kept finding myself typing it this way and it seems more logical to me since the scope narrows after each dot. Just a thought. Either way, I love it so far and look forward to any enhancements you come up with.

Tuesday, June 23, 2009 3:11 AM by John Sheehan

# re: A new and improved ASP.NET MVC T4 template

John, the reasoning I made is that the controller is the root, in the sense that Actions and Views belong to a Controller (except for Shared views, which break this rule).

But granted there are more than one right way of organizing this. I can certainly change it if there is demand :)

Tuesday, June 23, 2009 11:40 AM by davidebb

# re: A new and improved ASP.NET MVC T4 template

I don't see the implementation of the,

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

I don't think is in the included template.

Regards,

Roberto

Tuesday, June 23, 2009 6:18 PM by Roberto Hernandez

# re: A new and improved ASP.NET MVC T4 template

Roberto, make sure you make your controller class partial to get the RedirectToAction method.  This is mentioned in the post, but I guess it's easy to miss! :)

Tuesday, June 23, 2009 6:25 PM by davidebb

# re: A new and improved ASP.NET MVC T4 template

Thanks for the nice tool. An idea:

How about spitting out the fully ~ qualified path to the view rather than just its name? This would help us avoid some mildly annoying (but handled) runtime FileNotFoundExceptions related to the default path resolution strategy when developing.

Thursday, June 25, 2009 10:17 PM by AdamR

# re: A new and improved ASP.NET MVC T4 template

Hey David, I have a suggestion.

When Controller isn´t partial class, we have another options:

   public static class Example

   {

       public static void RedirectToAction(this Controller controller, ControllerActionCallInfo any)

       {}

   }

The can use: this.RedirectToAction(MVC.MyController.Show("1"))

Friday, June 26, 2009 8:09 AM by Felipe Fujiy

# Supporting view names like "view-name"

The generated class will have errors if you happened to use hyphens in any view names. A straight replacement is simple but this is a bit more thorough (around line 64 of version 0.9.0006):

public static class Views {

<# foreach (string view in controller.Views) { #>

public const string <#= Regex.Replace(

  view,

  @"\W*\b(\w)",

  m => m.Groups[1].Value.ToUpper(),

  RegexOptions.IgnoreCase) #> = "<#= view #>";

<# } #>

}

Friday, June 26, 2009 12:49 PM by Jason Abbott

# re: A new and improved ASP.NET MVC T4 template

Jason: I fixed that character issue and posted the update on CodePlex (now version 2.0.01). I just went with a straight replacement to '_' for now.

Friday, June 26, 2009 4:17 PM by davidebb

# re: A new and improved ASP.NET MVC T4 template

@David: thanks for an answer, I will digg to it today as I have some time, guess I'll start with the first article on the topic.

just a quick check before I understand your approach (if it's worth for me).

Do I get strongly typed checks as I am getting using lambda syntax html helpers (performance is NOT a problem, its drawback is NON EXISTENT in a real world application, where all is about the DB)?

As this is the main advantage, having compiler find any "string related" bugs...you know, kill those pesky strings :)

going to read couple of blog post now, thanks for your work anyway

Saturday, June 27, 2009 4:48 AM by cowgaR

# re: A new and improved ASP.NET MVC T4 template

ah well, judging just by a blog post name I read think I have an answer :)

Saturday, June 27, 2009 4:57 AM by cowgaR

# Visual Studio 2010 update

The enumeration of EnvDTE.CodeElements seems busted in 2010, but the MSDN page for CodeElements shows using a 1-based for loop to access it.  Try changing GetNamespaces to the following:

// Return all the CodeNamespaces in the CodeElements collection

public static IEnumerable<CodeNamespace> GetNamespaces(CodeElements codeElements) {

  List<CodeNamespace> spaces = new List<CodeNamespace>();

  for (int i=0; i<codeElements.Count; i++) {

  CodeNamespace possibleNamespace = codeElements.Item(i+1) as CodeNamespace;

  if (possibleNamespace != null) spaces.Add(possibleNamespace);

  }

  return spaces;

}

Saturday, June 27, 2009 4:47 PM by Chip Paul

# re: A new and improved ASP.NET MVC T4 template

Chip: yes, there is a known issue in VS2010. I have changed the template to use a loop as you suggest. This actually applied to several places in the code. Anyway, it should work fine in VS2010 beta now. Please try the new drop on CodePlex (version 2.0.03). Thanks for you help!

Sunday, June 28, 2009 1:41 AM by davidebb

# re: A new and improved ASP.NET MVC T4 template

hi,

Nice article, i tried your code its working genrally fine except i found one issue

in

GetProjectContainingT4File

on line 187 you should check for projec.projectitems for null it was crashing in my project

 else {

       // It may be a solution folder. Need to recurse.

if(project.ProjectItems != null)

{

foreach (ProjectItem item in project.ProjectItems) {

if (item.SubProject == null)

continue;

Monday, June 29, 2009 3:28 PM by Sajjad

# re: A new and improved ASP.NET MVC T4 template

Sajjad: fixed this issue in new build 2.1.00 (on CodePlex). Thanks!

Monday, June 29, 2009 8:23 PM by davidebb

# re: A new and improved ASP.NET MVC T4 template

thanks man, this is awesome stuff.

Tuesday, June 30, 2009 1:23 PM by hilton smith

# re: A new and improved ASP.NET MVC T4 template

You are a god among men. This is exactly what I needed: I was trying to do something similar to avoid the lambda syntax that upsets people, but this is so far ahead of my primitive attempts that it just isn't funny. Thank you very much.

Wednesday, July 01, 2009 8:35 AM by Alexis Kennedy

# re: A new and improved ASP.NET MVC T4 template

This is awesome...Kudos.  Is it possible to add in an "IgnoreOnRouteCreation" attribute to some parameters.  This would be useful when you are generating the rest of a url in javascript.

Monday, July 13, 2009 2:45 PM by Kelly Bourg
New Comments to this post are disabled
 
Page view tracker