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.

Comments (40)

  1. barryd says:

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

  2. Rob says:

    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 FilesCommon FilesMicrosoft SharedTextTemplating1.2TextTransform" "$(SolutionDir)MyTemplate.tt" -out "$(SolutionDir)Output.cs"

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

  3. PabloBlamirez says:

    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!

  4. MelGrubb says:

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

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

  5. davidebb says:

    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! :)

  6. Eric Hexter says:

    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.

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

  8. The design time approach is really cool.

  9. cowgaR says:

    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

  10. Rick says:

    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.

  11. davidebb says:

    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!

  12. davidebb says:

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

  13. Richard says:

    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!

  14. davidebb says:

    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.

  15. DotNetBurner – burning hot .net content

  16. Richard says:

    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.

  17. davidebb says:

    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.

  18. Felipe Fujiy says:

    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

  19. Felipe Fujiy says:

    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)

  20. Richard says:

    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!

  21. davidebb says:

    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.

  22. 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.)

  23. davidebb says:

    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.

  24. John Sheehan says:

    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.

  25. davidebb says:

    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 :)

  26. 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

  27. davidebb says:

    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! :)

  28. AdamR says:

    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.

  29. Felipe Fujiy says:

    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"))

  30. Jason Abbott says:

    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 #>";

    <# } #>

    }

  31. davidebb says:

    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.

  32. cowgaR says:

    @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

  33. cowgaR says:

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

  34. Chip Paul says:

    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;

    }

  35. davidebb says:

    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!

  36. Sajjad says:

    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;

  37. davidebb says:

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

  38. hilton smith says:

    thanks man, this is awesome stuff.

  39. 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.

  40. Kelly Bourg says:

    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.