The MVC T4 template is now up on CodePlex, and it does change your code a bit


Short version: the MVC T4 template (now named T4MVC) is now available on CodePlex, as one of the downloads in the ASP.NET MVC v1.0 Source page.


Go to T4MVC home page


Poll verdict: it’s ok for T4MVC to make small changes


Yesterday, I posted asking how people felt about having the template modify their code in small ways.  Thanks to all those who commented!  The fact that Scott Hanselman blogged it certainly helped get traffic there 🙂


The majority of people thought that it was fine as long as



  • It’s just those small changes: make classes partial and action methods virtual. Don’t mess with ‘real’ code!

  • It asks for permission, or at least tells you what it’s doing

I started looking for a way to pop up a Yes/No dialog, but ended up going with a slightly different approach: T4MVC adds a warning line for every item it modifies.  e.g. when you run it, you might see these in the warnings area:



Running transformation: T4MVC.tt changed the class DinnersController to be partial
Running transformation: T4MVC.tt changed the action method DinnersController.Index to be virtual


Some people were worried about version control.  I tried using TFS, and everything worked fine.  i.e. when the template modifies files, VS automatically checks them out.  We’ll need to see how that works for folks using different systems.


What’s new in this version?


The template on CodePlex (version 2.0.01 at the top of the file) supports what I described in my previous post, plus some new goodies.


Refactoring support for action methods


One of the big issues before was the lack of refactoring support.  e.g. when you wrote:

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

This looked like a call to you Details controller action, but it was actually an unrelated method by the same name.  Hence, if you renamed your action method and refactored, this call was not modified.  It would give a compile error, and had to be hand fixed.


Now the template takes a drastically different approach:



  • It extends the controller class

  • It overrides the action method (hence the need for it to be virtual!)

  • The override never calls the base (that would be very wrong), but instead returns a special ActionResult which captures the call (controller name, action name, parameter value).

  • The template emit a new RedirectToAction (or ActionLink, …) overload which understands this special ActionResults, and turns the call data into a ‘regular’ RedirectToAction call.

Pretty tricky stuff, but it works quite well.  Some credit to my manager Mike Montwill for coming up with this crazy idea!


Because the method you call is an override of the real action method, refactoring works perfectly.  Also, if you F12 (Go To Definition) on the call, it’ll go straight to your Action method and not some generated code.


Unfortunately, Visual Studio doesn’t support refactoring in Views, but 3rd party tools like Resharper and CodeRush do, so if you use one of those, you’re fully covered.


The T4 file automatically runs whenever you build


This was the other big painful issue I was up against: every time you made a change to your code that affect the generated code (e.g. new Action, new View, …), you had to manually save the .tt file to cause it to regenerate the new helper code.


This was a really hard issue, and I must warn you that what I ended up with is more of a workaround than a fix.  However, it is pretty effective, so until we find a better solution, it’ll have to do.


Here is how it works.  Warning: reading this has been shown to cause headaches in lab rats:



  • As part of its execution, the T4 file finds itself in the VS project system (it had to do that anyway)

  • It then runs the magic instruction ‘projectItem.Document.Saved = false;’, which causes it to become dirty.

  • It then proceeds to do its code generation, leaving its file in an unsaved state

  • Next time you Build your project, VS first saves all the files

  • This causes the ‘dirty’ T4 template to execute, mark itself as dirty again, and redo its code generation

  • You get the idea!  If you feel like the lab rats, this may help.

One caveat is that you have to initiate the cycle by opening and saving T4MVC.tt once.  After you do that, you don’t need to worry about it.


Support for strongly typed links to static resources


Credit for this idea goes to Jaco Pretorius, who blogged something similar.


The template generates static helpers for your content files and script files.  So instead of writing:

<img src=”/Content/nerd.jpg” />

You can now write:

<img src=”<%= Links.Content.nerd_jpg %>” />

Likewise, instead of

<script src=”/Scripts/Map.js” type=”text/javascript”></script>

You can write:

<script src=”<%= Links.Scripts.Map_js %>type=”text/javascript”></script>

The obvious benefit is that you’ll get a compile error if you ever move or rename your static resource, so you’ll catch it earlier.


Another benefit is that you get a more versatile reference.  When you write src=”/Content/nerd.jpg”, your app will only work when it’s deployed at the root of the site.  But when you use the helper, it executes some server side logic that makes sure your reference is correct wherever your site is rooted.  It does this by calling VirtualPathUtility.ToAbsolute(“~/Content/nerd.jpg”).


One unfortunate thing is that for some reason, VS doesn’t support intellisense in the view for parameter values.  As a workaround, you can type it outside of the tag to get intellisense and then copy it there.


More consistent short form to refer to a View from a Controller class


Previously, it supported an _ based short form inside the controller:

return View(View_InvalidOwner);

That was a bit ugly.  Now, the short form is:

return View(Views.InvalidOwner);

Here, Views.InvalidOwner is the same as MVC.Dinners.Views.InvalidOwner, but can be shortened because ‘Views’ is a property on the controller.


Many bug fixes


I also fixed a number of bugs that people reported and that I ran into myself, e.g.



  • It supports controllers that are in sub-folders of the Controllers folder and not directly in there

  • It works better with nested solution folder

I’m sure there are still quite a few little bugs, and we’ll work through them as we encounter them

Comments (53)

  1. Chad Kranz says:

    Thanks for this!  I did uncover one bug (kind of).  When creating copies of files in the project the template doesn’t deal with spaces in file names.  Also, some of Rob Connery’s subsonic mvc templates use a ‘ui-lightness’ directory under the Scripts folder.  Here is my change, it was just a quick hack to get it done 🙂  I’ve just posted it here for reference, do with it what you wish…

    void ProcessStaticFilesRecursive(ProjectItem projectItem, string path) {

       if (IsFolder(projectItem)) {

           WriteLine("[CompilerGenerated]");

           string _projectCleanName = projectItem.Name.Replace(" ", "");

           _projectCleanName = projectItem.Name.Replace("-", "_dash_");

           WriteLine(String.Format("public static class {0} {{", _projectCleanName));

           PushIndent("    ");

           // Recurse into all the items in the folder

           foreach (ProjectItem item in projectItem.ProjectItems) {

               ProcessStaticFilesRecursive(item, path + "/" + projectItem.Name);

           }

           PopIndent();

           WriteLine("}");

           WriteLine("");

       }

       else {

           WriteLine(String.Format("public static string {0} {{ get {{ return VirtualPathUtility.ToAbsolute("{1}"); }} }}",

               GetConstantNameFromFileName(projectItem.Name),

               path + "/" + projectItem.Name));

       }

    }

  2. Chad Kranz says:

    Ok, there are a few other issues I didn’t see before my last post.  To replicate, add a file with a space in it (such as Copy of site.css) and a folder with a dash in it (such as Contentui-lightness).  Then re-run the T4 template and you’ll see the errors generated.  Thanks!

  3. davidebb says:

    Chad: I fixed that character issue and posted the update on CodePlex (now version 2.0.01)

  4. Chad K says:

    Thanks Dave!  Do you have any suggestions for what Scott H talked about in his blog, looking for the Views.Textbox(), Views.Label and Views.Validation stuff?  I am using the latest version of Subsonic 3.0 and love the idea of automatically generating the validation, as well as the label and control options for it but don’t want to reinvent the wheel if someone has even a good starting off point.

    Thanks again!

  5. davidebb says:

    Chad, take a look et Eric Hexter’s solution: http://www.lostechies.com/blogs/hex/archive/2009/06/09/opinionated-input-builders-for-asp-net-mvc-using-partials-part-i.aspx. It looks promising, and may be better than a T4 based solution for input builders.

  6. daniel says:

    Awesome – great work on this so far.  I haven’t had a chance to play with this latest version, but one thing to watch out for on the content & script links is that the application may not be deployed at the root.  For example, "/Scripts/Map.js" will break if you deploy your app to http://myserver/somefolder/myapp.  You may already handle this, but I thought I’d mention…

  7. davidebb says:

    Daniel, the template does correctly handles non-root sites. I added a paragraph in the post to mention this. Thanks!

  8. Dave,

    This is great stuff! May I suggest you also add the following method to your T4Extensions class in order to support object htmlAttributes for ActionLink the same way as the standard html helper does:

    public static string ActionLink(this HtmlHelper htmlHelper, string linkText, ActionResult result, object htmlAttributes) {

            return ActionLink(htmlHelper, linkText, result, new RouteValueDictionary(htmlAttributes));

       }

  9. One more thing: Strongly typed links usually work great, but in this instance they didn’t and I was unable to find out what’s going wrong. I tried to replace the following:

    <link rel="STYLESHEET" type="text/css" href="~/Content/CustDatabase.css" />

    with this:

    <link rel="STYLESHEET" type="text/css" href="<%= Links.Content.CustDatabase_css%>" />

    There’s no error when compiling the view, but the generated link is strangely messed up:

    <link href="Views/Shared/%3C%25=%20Links.Content.CustDatabase_css%25%3E" type="text/css" rel="STYLESHEET"/>

    The code generated for the stylesheet in T4MVC.cs is

    public static string CustDatabase_css { get { return VirtualPathUtility.ToAbsolute("~/Content/CustDatabase.css"); } }

    I am completely clueless why comes up as Views/Shared/%3C%25=%20Links.Content.CustDatabase_css%25%3E in the generated html. Perhaps you have an idea?

  10. One last question: Is it possible to strong typing in Html.BeginForm?

  11. davidebb says:

    Adrian:

    – ActionLink: I added the suggested ActionLink overload (now version 2.0.02 on CodePlex)

    – CSS link: I think this happens because code is simply not allowed in this context, so you just can’t use a <%= %> here at all. I can’t think of a great way around this.

    – BeginForm: in most cases, you don’t want this to look like a method call to the Action, because the param values come from the form. But note that you can get some strong typing for the action and controller names by using:

     Html.BeginForm(MVC.Dinners.Actions.Delete, MVC.Dinners.Name)

  12. Marco says:

    I get the message "The Views folder has a sub-folder named ‘{0}’, but there is no matching controller". My web project contains the mvc t4 template, but my controllers are stored in a different project. It that not supported?

  13. William says:

    @Adrian Grigore: I removed the runat="server" from the head and it then ran perfectly.

    <head >

       <link href="<%= Links.Shared.CSS.Website_Default_css %>" rel="stylesheet" type="text/css" />

    </head>

  14. Richard says:

    This is an excellent template! One question, though – is there a reason the s_actions, s_views, and the public fields of the _Actions and _Views classes are not read-only?

    For the CSS link, you’ll need to use something like Dave Reed’s CodeExpressionBuilder:

    http://weblogs.asp.net/infinitiesloop/archive/2006/08/09/The-CodeExpressionBuilder.aspx

    <link rel="stylesheet" type="text/css" href='<%$ Code: Links.Content.CustDatabase_css %>’ />

  15. davidebb says:

    New build 2.1.00 is now on CodePlex.

    – New BeginForm helpers (thanks Michael Hart and Adrian)

    – Various strings changed to readonly, as suggested by Richard

    – Misc fixes (see history in .tt file for details)

  16. davidebb says:

    Marco: indeed, that’s not currently supported. I think it could work by:

    – Putting the .tt in your controllers project, not the web project

    – Changing the .tt logic to find the views in the web project

    If someone gets to try this and has success, please send me your updates.  Thanks! 🙂

  17. Graham says:

    Hey Dave, great template!

    How do you see this fitting in with MVC Futures? I sense planets will collide very soon… I personally enjoy the approach you have given us, perhaps over some of the "Future" constructs… As Hanselman said, would be nice for this to be put through QA and baked in =)

  18. davidebb says:

    Graham: I think this and the Futures can live together, though they do intersect in some aspects. One thing the Futures can’t do is the View name and static file support, because that’s based on physical file existence and not code constructs. On the other hand, the Futures have some View render helpers (e.g. TextBoxFor) which I’m not sure we can easily  match with the T4 approach.

  19. Anthony says:

    Just thought that I would let you know I have started using this and with great success. Not sure if you mentioned it or not but another place you can reference the generated code is when registering routs, i.e.

    this._Routes.MapRoute("Default", "{controller}/{action}", new { controller = MVC.Home.Name, action = MVC.Home.Actions.Index });

  20. Marco says:

    >>-Putting the .tt in your controllers project, not the web project

    No, does not work…

    >>- Changing the .tt logic to find the views in the web project

    I think the controllers should be loaded from all the projects in the current solution.Anyone tried to change the logic?

  21. Hi,

    If your Content folder is empty, you get a build error.

    ProcessStaticFiles writes out this line without wrapping in a class.

    public static readonly string Content = Url("Content");

  22. Christian says:

    The current implementation doesn’t work well with Dependency Injection!

    You automatically generate a default constructor and one with your dummy parameter.

    StructureMap for example now calls the wrong constructor.

    Any ideas on how to work around this without explicitly decorating the real constructor with an DI-specific attribute?

    despite this issue, i really like this approach.

    best regards, christian

  23. Jim says:

    I have controllers that inherit from a base controller, and the code generated in t4mvc.cs  produces ‘warnings’ at build time.  i.e.

    "…Controllers.MyController.RedirectToAction() hides inherited member ‘…Controllers.MyBaseController.RedirectToAction().  Use the new keyword if hiding was intended."

    I’m not really sure if this is a problem, other than making me wade through warnings to find any I’m really interested in, but I thought I’d pass it along.  Mostly likely I’m just doing something wrong.

    Thanks … jim

  24. davidebb says:

    New version 2.2.00 is up on CodePlex

    Richard Kimber: fixed the issue with empty Content folder. Good catch!

    Christian: made a change which *should* fix your issue with Dependency Injection. If it doesn’t please email me and we’ll take it offline.

    Jim: Fixed issue with Controller base class. For the fix to work, please make sure you make your base Controller abstract. Thanks!

  25. davidebb says:

    Marco: I didn’t mean that just putting the .tt in teh Controllers project was enough. It’s only a piece of a solution which also involves changing the .tt logic. Hopefully, I can look at that at some point, though if someone else gets to it first, all the better! 🙂

  26. davidebb says:

    Anthony: indeed, this is a great use of it in the routes, I hadn’t thought of it.  I actually just added some better support for this in 2.2. Now you can write:

       routes.MapRoute(

           "Default",

           "{controller}/{action}

           MVC.Home.Index()

       );

  27. Harry M says:

    Am I right in thinking this could be used to generate static reflection helper classes with things like property names and attributes? I’m going to have a play with the template ASAP!

  28. Alex M says:

    I’ve encountered several bugs in 2.2.00:

    BUG 1:

    If any controller is already declared partial then there will be an entry in the Controllers list for each constituent file. This triggers am exception on template execution on line 478, because the call to ‘SingleOrDefault’ will return multiple results.

    FIX:

    Change the type of the ‘Controllers’ list to ‘HashSet<ControllerInfo>’ for an implicit ‘distinct’, slap an IEquatable<ControllerInfo>’ interface in there and modify the ProcessControllerTypesInNamespace method slightly:

    line 287:

    static List<ControllerInfo> Controllers;

    =>

    static HashSet<ControllerInfo> Controllers;

    line 295:

    Controllers = new List<ControllerInfo>();

    =>

    Controllers = new HashSet<ControllerInfo>();

    Expanded ControllerInfo definition:

    class ControllerInfo : IEquatable<ControllerInfo> {

       …

       public bool Equals(ControllerInfo obj) {

           return obj != null && FullClassName == obj.FullClassName;

       }

       public override int GetHashCode() {

           return FullClassName.GetHashCode();

       }      

    }

    Modification to ‘ProcessControllerTypesInNamespace’ method:

    —–

    Controllers.Add(controllerInfo);

    controllerInfo.HasExplicitConstructor = HasExplicitConstructor(type);

    // Process all the action methods in the controller

    ProcessControllerActionMethods(controllerInfo, type);

    —–

    =>

    —–

    // either process new controllerinfo or integrate results into existing object for partially defined controllers

    var target = Controllers.Add(controllerInfo) ? controllerInfo : Controllers.First(c => c.Equals(controllerInfo));

    target.HasExplicitConstructor |= HasExplicitConstructor(type);

    // Process all the action methods in the controller

    ProcessControllerActionMethods(target, type);

    —–

    BUG 2:

    The static file access generation (and possibly other) code yields incorrect paths on (T4 or designer) generated files:

    Imagine an ‘Output.tt’ file in Project/Content that in turn generates some files called ‘foo.css’ & ‘bar.css’.

    The solution explorer will show these files as

    Project

    +- Content

       +- Output.tt

       +- foo.css

    +- bar.css

    The physical path for ‘foo.css’ is ‘Project/Content/foo.css’, but the T4MVC template generates a path of ‘~/Project/Content/Output.tt/foo.css’ which is clearly wrong 🙁

    FIX:

    Don’t rely on the IsFolder method. In it’s current implementation the results have more of a ‘HasChildren’ meaning.

    Instead use ProjectItem.Kind (http://msdn.microsoft.com/en-us/library/z4bcch80%28VS.80%29.aspx) to check if the item with children is actually a physical folder and only then construct an inner class with modified path.

    Since IsFolder is used in several places, I didn’t touch it but instead refactored the ProcessStaticFilesRecursive method:

    —-

    void ProcessStaticFilesRecursive(ProjectItem projectItem, string path) {

    bool isPhysicalFolder = projectItem.Kind == "{6BB5F8EF-4483-11D3-8BCF-00C04F8EC28C}"; // see ProjectItem.Kind documentation on msdn

    if (isPhysicalFolder) {

      WriteLine("[CompilerGenerated]");

      WriteLine(String.Format("public static class {0} {{", SanitizeFileName(projectItem.Name)));

      PushIndent("    ");

      WriteLine(String.Format("public static string Url() {{ return VirtualPathUtility.ToAbsolute("{0}"); }}", path + "/" + projectItem.Name));

      WriteLine(String.Format("public static string Url(string fileName) {{ return VirtualPathUtility.ToAbsolute("{0}/" + fileName); }}", path + "/" + projectItem.Name));

    // Recurse into all the items in the folder

    foreach (ProjectItem item in projectItem.ProjectItems) {

    ProcessStaticFilesRecursive(item, path + "/" + projectItem.Name);

    }

      PopIndent();

      WriteLine("}");

      WriteLine("");    

    } else {

      WriteLine(String.Format("public static readonly string {0} = Url("{1}");",

          SanitizeFileName(projectItem.Name),

          projectItem.Name));

    // non folder items may also have children (virtual folders, Class.cs -> Class.Designer.cs, template output) – just register them on the same path as their parent item

    foreach (ProjectItem item in projectItem.ProjectItems) {

    ProcessStaticFilesRecursive(item, path );

    }

    }

    }

    —-

    I’m pretty sure there is an equivalent bug for views and controllers (which could theoretically also be auto-generated and thus appear as child nodes to non folders), but I’ll leave that fix to somebody else 😉

    BUG 3:

    If a controller is derived from a base class that already implements some action methods, those methods are never discovered:

    abstract class FooBaseController : Controller {

     ActionResult SkippedMethod(…);

    }

    class FooController : FooBaseController {

     ActionResult FoundMethod(…);

    }

    There is no code generated for ‘SkippedMethod’.

    FIX:

    Haven’t looked into this one yet

  29. Pat says:

    Nice! I updated and it broke everything.

    Why the new requirement to only support ActionResult return types? My actions return strongly types like ViewResult, etc.

    Why can’t it keep track of what the action method return type is and carry that over into the helpers?

  30. Hien Khieu says:

    David,

    I am trying to use your latest T4MVC template with my mvc project and I get this error. Please help.

    Error 1 Running transformation: System.EntryPointNotFoundException: Entry point was not found.

      at VisualSVN.VS.Interop.NativeExtenderCallback.OnQueryEditFiles(UInt32 rgfQueryEdit, String[] rgpszMkDocuments, UInt32* pfEditVerdict, UInt32* prgfMoreInfo, Boolean* skipOriginal)

      at VisualSVN.VS.Interop.NativeExtender.OnQueryEditFiles(NativeExtender* , UInt32 rgfQueryEdit, Int32 cFiles, UInt16** rgpszMkDocuments, UInt32* rgrgf, __MIDL___MIDL_itf_ivsqueryeditquerysave2_0000_0001* rgFileInfo, UInt32* pfEditVerdict, UInt32* prgfMoreInfo, Boolean* skipOriginal)

      at EnvDTE80.CodeClass2.set_ClassKind(vsCMClassKind Kind)

      at Microsoft.VisualStudio.TextTemplating984425EEA278851503849A871981DA21.GeneratedTextTransformation.ProcessControllerTypesInNamespace(CodeNamespace ns) in e:DevelopmentCSharpMVCMVCExperimentMVCExperimentMvc-CodeGen.tt:line 401

      at Microsoft.VisualStudio.TextTemplating984425EEA278851503849A871981DA21.GeneratedTextTransformation.ProcessControllersRecursive(ProjectItem projectItem) in e:DevelopmentCSharpMVCMVCExperimentMVCExperimentMvc-CodeGen.tt:line 381

      at Microsoft.VisualStudio.TextTemplating984425EEA278851503849A871981DA21.GeneratedTextTransformation.ProcessControllersRecursive(ProjectItem projectItem) in e:DevelopmentCSharpMVCMVCExperimentMVCExperimentMvc-CodeGen.tt:line 374

      at Microsoft.VisualStudio.TextTemplating984425EEA278851503849A871981DA21.GeneratedTextTransformation.ProcessControllersFolder(Project project) in e:DevelopmentCSharpMVCMVCExperimentMVCExperimentMvc-CodeGen.tt:line 366

      at Microsoft.VisualStudio.TextTemplating984425EEA278851503849A871981DA21.GeneratedTextTransformation.PrepareDataToRender(TextTransformation tt) in e:DevelopmentCSharpMVCMVCExperimentMVCExperimentMvc-CodeGen.tt:line 306

      at Microsoft.VisualStudio.TextTemplating984425EEA278851503849A871981DA21.GeneratedTextTransformation.TransformText() in e:DevelopmentCSharpMVCMVCExperimentMVCExperimentMvc-CodeGen.tt:line 96 E:DevelopmentCSharpMVCMVCExperimentMVCExperimentMvc-CodeGen.tt 1 1

  31. Pat says:

    So I was able to improve it to add support for other Action return types. Not easy. Sometimes these "helpers" take on a life of their own. Basically in the get action methods routine I parsed out the return type name and stored it in the ActionMethodInfo class. Then I turned ControllerActionCallInfo into an interface and added new classes like ControllerActionResultCallInfo, ControllerViewResultCallInfo, etc that implemented that interface and fixed up various other parts to reference the interface and return the correct type instead of the hardcoded ActionResult and everything is working again!

    Great stuff….

  32. davidebb says:

    Alex M: thanks a lot for working through those issues!  New build 2.2.01 on CodePlex has the fixes for issues 1 and 2. I mostly used your code, with minor changes. I actually fixed the issue you bring up for controllers as well.

    BTW, they have constants for all the GUIDs, so you can write Constants.vsProjectItemKindPhysicalFolder. I know, not very discoverable, but once you know where they are, they’re all there!

    Bug #3: definitely a bug, but I haven’t had a chance to look into it.  Anyone? 🙂

  33. davidebb says:

    Hien Khieu: strange, I don’t know what could cause that. Is this with VS2008 SP1? I’ll ask the Visual Studio team if they can make sense of the stack. It’s dying when the code tries to change the class to be partial.

    Maybe you can work around by making it partial yourself.

  34. davidebb says:

    Pat: I went ahead and fixed this in 2.2.01 (now on CodePlex). My fix is similar to what you described. One difference is that I made it generic, by auto-generating derived classes for all the Result types it encounters, instead of hard coding a few (or maybe that’s what you did too?).

    Anyway, please make sure it works for you.  BTW, feel free to email me your changes next time, to give me a starting point for the fix 🙂

  35. davidebb says:

    Hien Khieu: it would appear that your issue is related to running VisualSVN. Please see this thread where a similar thing was reported:

    http://l2st4.codeplex.com/Thread/View.aspx?ThreadId=44898

  36. Hien Khieu says:

    David,

    Thank you for looking into my issue. VisualSVN is what I am thinking when I read the stack trace. One think I don’t understand that I was able to use your old MVC T4 template (the one that I download sometimes last week) with no problem. Thank you anyway.

  37. parminder says:

    Hi,

    thanks for this great work. I wana bring

    one issue in your attention. I have a controller method

    public  FileContentResult GetSmallImage(long photoID)

           {

               Return somefile;

           }

    It makes the method as virtual(no prblem).

    It doesnt compile and says Error ‘System.Web.Mvc.FileContentResult’ does not contain a constructor that takes ‘0’ arguments

    This is line where the error lies.

    public T4MVC_FileContentResult(string controller, string action) {

           this.InitMVCT4Result(controller, action);

       }

    It seems it didnt created controller method name properly.

  38. Alex M. says:

    Well David, I took another look at my Bug #3 and came up with a partial solution – which of course in turned lead to another problem.

    Since I don’t know the EnvDTE classes well enough to tell if my new problem is simple or easy to workaround, I’ll just throw in what I got this far:

    Modify ‘ProcessControllerActionMethods’ to:

    void ProcessControllerActionMethods(ControllerInfo controllerInfo, CodeClass2 current) {

    // walk up the controller inheritance chain until we arrive at the mvc default controller

    for (CodeClass2 type = current; current != null && current.FullName != "System.Web.Mvc.Controller"; current = current.Bases.Item(1) as CodeClass2) {

    foreach (CodeFunction2 method in GetMethods(type)) {

    // keep existing unmodified code here (skipped for brevity)

    // Make sure the method is virtual

    if (!method.CanOverride) {

    method.CanOverride = true; // *** THIS NEEDS SOME MORE CHECKS, SEE REMARKS

    Warning(String.Format("{0} changed the action method {1}.{2} to be virtual", T4FileName, type.Name, method.Name));

    }

    // more code that needs no change

    }

    }

    }

    Explanation:

    The modified code simply walks up the inheritance chain and looks at all the intermediate types’ methods until it stops at the stock mvc Controller base class. Choosing Bases.Item(1) shouldn’t cause any problem since no .NET language actually supports multiple inheritance.

    Now there are some potential problems with the ‘method.CanOverride = true’ call:

    A) The current controller code class ‘type’ is part of the same project as the template

    => everything should be okay, but might still fail for external reasons (source code control lock on the code file). The current implementation fails on those errors anyway – with a try/catch block you could just skip those elements, emit a warning and ‘continue’ with the next method.

    B) The current controller code class ‘type’ is defined in an external source, either referenced project or assembly:

    1) The code class ‘type’ is defined in another project which is part of the same solution as the template

      => switching the method to virtual should succeed (same caveats as in #A apply), but on my machine this always failed (the stacktrace contained some code from jetbrain’s resharper, so that component might be to blame for it).

    2) The code class ‘type’ is defined in a referenced assembly.

      => making the method virtual will always fail

    So as I see it, there needs to be a check (and try/catch block) around the ‘make virtual’ functionality for cases A & B.1 to work correctly.

    For B.1 it might also be necessary to walk the solution and find the actual code class from the base classes’ definition to change properties, since code classes with an ‘external’ storage are generated from metadata and that might have been why the ‘CanOverrid=true’ failed – There’s already very similar code in the template to find the actual ProjectItem for the template file.

    I cannot see any way to fix B.2, the best way here would probably just be to skip those methods with a warning, or at least process them with reduced output (and functionality) that doesn’t need virtual methods (you could still expose the action names).

  39. Alex M. says:

    Just noticed that I made a mistake in that demo code (wrote if from memory, since I removed those template changes). That new outer for loop should be:

    for (CodeClass2 type = current; type != null && type.FullName != "System.Web.Mvc.Controller"; type = type.Bases.Item(1) as CodeClass2) {

  40. Bob Baker says:

    I have the same problem as Hien with VisualSVN. The thread http://l2st4.codeplex.com/Thread/View.aspx?ThreadId=44898 has a reply where there seems to be a workaround. Can you implement that in your t4 template? Thanks

  41. Bob Baker says:

    Follow up with VisualSVN…

    Guidance from the VisualSVN team:

    "It turns out that problem is caused by ActiveWriter using DTE from temporary AppDomain. To fix this all calls to DTE should be marshaled to default AppDomain. As a simple workaround you can do code generation on separate working thread."

  42. davidebb says:

    parminder: just fixed this issue in build 2.2.02 on CodePlex

  43. davidebb says:

    Alex M: thanks for looking into this. I haven’t had a chance to look into your code yet, but I plan to early next week!

  44. davidebb says:

    Bob (and Hien): VisualSVN issue: I’m not very sure how I could do this from a T4 file. The T4 file is executed by VS in a different AppDomain, and I don’t think this can be changed. If someone understands the issue better and has a fix, please let me know.

  45. davidebb says:

    Alex M: I have integrated your fix to deal with base class action methods. I also added exception handling on the code that tries to make methods virtual (and make controllers partial). When that happens, it skips the method and gives a warning suggesting that the user makes that change themselves if possible. Obviously, when dealing with a true binary you don’t control, it won’t be possible, but I think that’s an edge case. Thanks again!

  46. davidebb says:

    Alex M: forgot to mention that the new build is 2.2.03 on CodePlex.

    Bob (and Hien): VisualSVN issue: 2.2.03 deals with those errors more gracefully. The workaround for you is to make the methods partial yourself (see previous comment).

  47. Ramandeep Singh says:

    I am using T4MVC.tt in my project. I am facing the problem that I have sub folders in the controllers folder.

    e.g. (ControllersMemberAccountActivationController.cs)

    There is no compile time error. But when I run the project it creates the object of those Controllers which are on the root of "Controllers folder" e.g "Home" = T4MVC.T4MVC_HomeController

    but Activation(ActivationControllers) is null

    And also all other controllers are null because they all are in the sub folders.

  48. Ramandeep Singh says:

    Please tell me the way out as I have more then 200 controllers and I want to keep them in sub folders.

    Thanks in advance

  49. davidebb says:

    Ramandeep: T4MVC is supposed to support controllers that are in sub folders of the Controllers folder. Please see the code in ProcessControllersRecursive. Not sure why it wouldn’t work for you. Please look through the tt file and the generated file to try to figure out what’s going on. Make sure you use the latest version (2.2.03).

    Or if you can put together a small repro, you can email it to me and I’ll take a look.

  50. Alex M. says:

    Nice changes there david – I’ve got some more bugs & fixes 😉

    1)

    The controller inheritance chain analysis works great now. But it still fails the ‘CanOverride=true’ call on base types from external project.

    If you insert the following code in ‘ProcessControllerActionMethods’ before the ‘foreach (CodeFunction2 method …’ loop you’ll get a better refactoring experience:

    —-

    // if the type is defined in another project, try getting a direct reference which might give more access for modifications

    if (type.InfoLocation != vsCMInfoLocation.vsCMInfoLocationProject) {

    var dte = type.DTE;

    foreach (Project prj in dte.Solution.Projects) {

    if (prj != Project && prj.CodeModel != null) {

    var prjCodeType = prj.CodeModel.CodeTypeFromFullName(type.FullName);

    if (prjCodeType != null && prjCodeType.InfoLocation == vsCMInfoLocation.vsCMInfoLocationProject) {

    type = (CodeClass2) prjCodeType;

    break;

    }

    }

    }        

    }

    —-

    With this modification I could successfully refactor any base class method from *non-generic* controllers. Generic base controllers still don’t work, but since the exception is as specific as ‘unexpected error (hresult 0x8004005)’ I don’t have much hope for those.

    2)

    I completed the ‘// TODO: Make the base type check more reliable’ task inside ‘ProcessControllerActionMethods’:

    —-

    // We only support action methods that return an ActionResult derived type

    if (!method.Type.CodeType.get_IsDerivedFrom("System.Web.Mvc.ActionResult"))

    continue;

    —-

    3)

    Thanks to the finally working base class methods I found a bug in one of our usage scenarios. For the following definition:

    public class FooBarController {

    [ActionName("Bar")]

    public ActionResult Foo() {

    }

    }

    the template will output code like

    public partial class FooBarController {

    public class _Actions {

    public readonly string Foo = "Foo";

    which is wrong, since the action (and the url where it can be invoked) is actually ‘FooBar/Bar’, not ‘FooBar/Foo’.

    You’ll probably have to check each controller action method for attributes deriving from System.Web.Mvc.ActionNameSelectorAttribute, but I’d argue it should be enough to only check for the derived ‘ActionName’ attribute.

    Though anybody could easily define their own ActionNameSelectorAttribute derived implementation (imagine an attribute that accepts every action which contains the letter ‘a’ at least three times), there might be no clearly defined reverse lookup from the attribute instance to the action name, and even if there is one, the template could never know it.

    ‘ActionName’ does both ship with the MVC Framework and has a clearly defined value-to-action relationship, so this would always work.

    If you intend to implement this functionality, be sure to also add the code from #1, since the ‘Attributes’ collection is always empty for code with an InfoLocation other than vsCMInfoLocationProject, so you need the direct type references to the defining project here.

  51. davidebb says:

    Alex M: would you mind contacting me be email (david.ebbo [@ microsoft.com]). It’ll be easier to continue discussing this.  Thanks!

  52. davidebb says:

    Alex M: please check out v2.3 on CodePlex.

  53. Pat says:

    Thanks! I tried the latest version in place of my custom fixed version and everything still works! I had hardcoded classes for the return types so your fix is definitely better. Thanks again! Nice to know I’m back in sync with the latest online version.