Udostępnij za pośrednictwem


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.

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

  • Anonymous
    June 26, 2009
    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));    } }

  • Anonymous
    June 26, 2009
    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!

  • Anonymous
    June 26, 2009
    Chad: I fixed that character issue and posted the update on CodePlex (now version 2.0.01)

  • Anonymous
    June 26, 2009
    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!

  • Anonymous
    June 26, 2009
    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.

  • Anonymous
    June 26, 2009
    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...

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

  • Anonymous
    June 26, 2009
    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));    }

  • Anonymous
    June 27, 2009
    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?

  • Anonymous
    June 27, 2009
    One last question: Is it possible to strong typing in Html.BeginForm?

  • Anonymous
    June 27, 2009
    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)
  • Anonymous
    June 28, 2009
    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?

  • Anonymous
    June 28, 2009
    The comment has been removed

  • Anonymous
    June 29, 2009
    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 %>' />

  • Anonymous
    June 29, 2009
    The comment has been removed

  • Anonymous
    June 29, 2009
    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! :)
  • Anonymous
    June 29, 2009
    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 =)

  • Anonymous
    June 29, 2009
    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.

  • Anonymous
    June 29, 2009
    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 });

  • Anonymous
    June 29, 2009
    >>-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?

  • Anonymous
    June 30, 2009
    The comment has been removed

  • Anonymous
    June 30, 2009
    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

  • Anonymous
    June 30, 2009
    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

  • Anonymous
    June 30, 2009
    The comment has been removed

  • Anonymous
    June 30, 2009
    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! :)

  • Anonymous
    June 30, 2009
    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()    );

  • Anonymous
    June 30, 2009
    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!

  • Anonymous
    June 30, 2009
    The comment has been removed

  • Anonymous
    July 01, 2009
    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?

  • Anonymous
    July 01, 2009
    The comment has been removed

  • Anonymous
    July 01, 2009
    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....

  • Anonymous
    July 01, 2009
    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? :)

  • Anonymous
    July 01, 2009
    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.

  • Anonymous
    July 01, 2009
    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 :)

  • Anonymous
    July 01, 2009
    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

  • Anonymous
    July 01, 2009
    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.

  • Anonymous
    July 01, 2009
    The comment has been removed

  • Anonymous
    July 02, 2009
    The comment has been removed

  • Anonymous
    July 02, 2009
    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) {

  • Anonymous
    July 02, 2009
    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

  • Anonymous
    July 02, 2009
    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."

  • Anonymous
    July 02, 2009
    parminder: just fixed this issue in build 2.2.02 on CodePlex

  • Anonymous
    July 02, 2009
    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!

  • Anonymous
    July 02, 2009
    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.

  • Anonymous
    July 06, 2009
    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!

  • Anonymous
    July 06, 2009
    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).

  • Anonymous
    July 06, 2009
    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.

  • Anonymous
    July 06, 2009
    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

  • Anonymous
    July 07, 2009
    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.

  • Anonymous
    July 07, 2009
    The comment has been removed

  • Anonymous
    July 07, 2009
    Alex M: would you mind contacting me be email (david.ebbo [@ microsoft.com]). It'll be easier to continue discussing this.  Thanks!

  • Anonymous
    July 07, 2009
    Alex M: please check out v2.3 on CodePlex.

  • Anonymous
    July 14, 2009
    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.