Udostępnij za pośrednictwem


T4MVC 2.4 updates: settings file, sub view folders, ActionName support and more

To get the latest build of T4MVC:

Go to T4MVC page on CodePlex

This post is a continuation of various previous posts on the T4MVC template for ASP.NET MVC:

I last blogged about version 2.2, and there have been a number of changes since that (you can get the full history at the top of the T4MVC.tt file).  This post describes some of those changes.

T4MVC now uses a separate settings file

Previously, if you wanted to customize T4MVC, you’d have to change T4MVC.tt directly.  This is fine until you want to grab the next build, and have to hand merge the changes.

Instead, it is now using a separate settings file called T4MVC.settings.t4.  The idea is that you can tweak some behavior by changing this file, without changing the main file.  Make sure that you copy both files when you grab 2.4 or later!

Note: it uses a .t4 extension instead of .tt, because we don’t want VS to process it directly.  Instead, it’s included by the main .tt file.  What’s nice is that the .t4 extension is also recognized by Clarius’ Visual T4.

Currently, it doesn’t support all that many settings, but it’s only a first step.  The idea is that as more customization scenarios come up, new things will appear there.  Of course, you may need some small hand merging of the settings files when you update to newer versions, but that’s a lot less painful than merging the main .tt file.

Here is what it contains in 2.4.00:

 // The prefix used for things like MVC.Dinners.Name and MVC.Dinners.Delete(Model.DinnerID)
const string HelpersPrefix = "MVC";

// The folder under the project that contains the controllers
const string ControllersFolder = "Controllers";

// The folder under the project that contains the views
const string ViewsRootFolder = "Views";

// Folders containing static files for which links are generated (e.g. Links.Scripts.Map_js)
readonly string[] StaticFilesFolders = new string[] {
    "Scripts",
    "Content",
};

Support for views in sub folders

I have had a few users mention that they sometimes don’t put their views directly under Views\CtrlName, but instead put them in a subfolder.  e.g. you might have Views\Dinners\Sub\Details.aspx.

Previously, T4MVC was ignoring those.  Now, it finds them and makes them available using a matching hierarchy.

So when you have Views\Dinners\Sub\Details.aspx, you can refer to it as

MVC.Dinners.Views.Sub.Details

or if you’re within the Dinners controller, you can just write:

Views.Sub.Details

And this will evaluate to "Sub/Details".

Support for [ActionName] attribute

Suppose you have an action method that looks like:

 public virtual ActionResult Details(int id) {

By default, the action name is just the method name: “Details”.  That is, this is what normally shows up in your URLs, e.g. /Dinners/Details/2.

But sometimes, you want your action name to be different from the method name, and that’s when you would use the MVC ActionName attribute.  e.g.

 [ActionName("NewActionName")]
public virtual ActionResult Details(int id) {

And now your URLs start looking like /Dinners/NewActionName/2.

Except that previously, T4MVC was ignoring this attribute, causing it to still generate the old URL!  Now, it correctly locates and honors the attribute.

The beauty is that T4MVC completely shields your code from those changes.  Without it, you’d have written:

 <%= Html.ActionLink(dinner.Title, "Details", new { id=dinner.DinnerID }) %>

And you would have had to change “Details'” to “NewActionName”.  But with T4MVC, you just write:

 <%= Html.ActionLink(dinner.Title, MVC.Dinners.Details(dinner.DinnerID)) %>

And it keeps on doing the right thing no matter what the ActionName is set to!

New parameter-less overload for all actions

Previously, T4MVC was only offering pseudo-action calls that had the exact same signature as the real action method.  This usually works well, but in some POST scenarios, the parameters comes from the form and not the URL.

To solve this, T4MVC now always generates a parameter-less overloads.  e.g. suppose your action looks like:

 [AcceptVerbs(HttpVerbs.Post), Authorize]
public virtual ActionResult Edit(int id, FormCollection collection) {

Where both the id and the FormCollection are things that you don’t want to pass explicitly, you can write:

 <% using (Html.BeginForm(MVC.Dinners.Edit())) { %>

So you still get the benefit of not hard coding the controller and action names (what T4MVC is all about), even though you’re not using the signature that matches your real action.

Support for placing T4MVC.tt below the root of the app

Previously, T4MVC.tt had to be at the root of the MVC web application.  Some users mentioned that they preferred to have it in some other folder, like ‘Templates’.  So I made a change so it can be anywhere under the project root.

Bug fixes

There are also a number of bug fixes that are not interesting enough to discuss individually.  Please see the history at the top of T4MVC.tt for details.

Comments

  • Anonymous
    July 28, 2009
    Hi I'm using your excellent template but are having som difficulties. I use S#arp architecture where the controllers sits in separate assembly away from the main webbsite. I copied and modified your script so that it works for me but it will be a pain to hand merge those changes to new versions. Please make i configurable which project the controllers are in instead of assuming that it is the same as the views. Thanks for a great tool!

  • Anonymous
    July 28, 2009
    will this excellent project be included in ASP.NET MVC v2? thanks

  • Anonymous
    July 29, 2009
    This is real cool it just gets better and better. These things can start to grow and grow and soon have a life of their own. Steve :D

  • Anonymous
    July 29, 2009
    Jonathan: could you contact me by email so we can discuss exactly how things work in this architecture?  e.g. the controllers are in a different assembly, but the views stay in the standard structure? thanks, David

  • Anonymous
    July 29, 2009
    cowgaR: that's a good question, which hasn't really been discussed. I need to chat with the MVC team guys about it. One concern is that including it would then make it harder to rev. Though I suppose it could include some stable version of it, with the option to go replace it by a newer one from CodePlex.

  • Anonymous
    July 29, 2009
    I am also using S#arp architecture and have attempted to adapt the script for Controllers living in a separate assembly, but with no success. Can you please include support for multiple projects? Jonatan Berggren, any patch to suggest?

  • Anonymous
    July 29, 2009
    S#arp architecture is here: http://sharparchitecture.net/ And yes, it puts the Controllers in their own project.

  • Anonymous
    July 29, 2009
    The comment has been removed

  • Anonymous
    July 30, 2009
    Daniel: Good point! I just dropped a new 2.4.01 build which adds this support through a new AlwaysKeepTemplateDirty flag in T4MVC.settings.t4

  • Anonymous
    July 30, 2009
    The comment has been removed

  • Anonymous
    July 30, 2009
    The comment has been removed

  • Anonymous
    July 31, 2009
    @David I did try just removing the runat=server, and that would have worked, but in my case I was setting Page.Title on the viewpage, which apparently requires the runat=server.  I got an error to this effect when I removed runat=server. May be out of the scope of what you want to do with T4MVC, though.  Anyway, keep up the great work!

  • Anonymous
    July 31, 2009
    The comment has been removed

  • Anonymous
    August 03, 2009
    It's great the the T4 templates support the ActionName attribute now.  However, I'd like to use the generated template code IN the ActionName attribute.  So, the example you gave was this: [ActionName("NewActionName")] public virtual ActionResult Details(int id) { but I want to be able to do this: [ActionName(MVC.MyController.Actions.NewActionName)] public virtual ActionResult Details(int id) { But I can't do this because the action variables were defined as readonly instead of constants.  Any reason for this?

  • Anonymous
    August 03, 2009
    @Steve: I'm a bit confused by what you're trying to do. How can you use the NewActionName constant in the ActionName attribute since this attribute is what drive the constant?  Isn't that a chicken and egg problem? The reason they are not C# constant is that MVC.MyController is an object instance, and so is MVC.MyController.Actions.  Constants are not accessible through instances, only through types.

  • Anonymous
    August 03, 2009
    @David - Sorry, best to illustrate with a more real-world example rather than names like "NewActionName".  Let's say I already have a controller method calls Create() which simply displays my form: public ActionResult Create() I have a second C# controller method called Save() but I want to apply the ActionName attribute to it so it looks like it gets posted to create like this: [ActionName("Create")] public ActionResult Save(int id) that's the typical use for the ActionName attribute.  So I'm wondering if there is a way to avoid the hard coded "Create" string in the ActionName attribute.  by doing something like this: [ActionName(MVC.MyController.Actions.Index)] public ActionResult Save(int id) but you may have just given the answer.  I didn't realize the generated code was attached to instances - so it looks like what I'm suggesting is not possible with the current implementation.  It would have to instead generate a bunch of static classes with the constants strings and that may not be worth it or desirable.

  • Anonymous
    August 03, 2009
    @Steve: ok I understand your scneraio now and it makes sense. Indeed, I can't think of a simple workaround with the current implementation. Note that there are good reasons for those to be instances, in term of being able to write MVC.MyController.Create(). But when it comes to the simple 'Actions' and 'Views' constant, I can see how working with instances is not a great as it could be. Maybe there is some smart way to keep the best of both world.

  • Anonymous
    August 04, 2009
    @David - Hmm, I'm trying to think through the implications if only the "Name" member was constant.  That would allow: [ActionName(MyController.Name)] but then you would not longer be able to do: this.Name Right now it looks like the primary consumer of the Name member, is code like this: return new T4MVC_ActionResult(Name, Actions.Save); that would have to be changed to this: return new T4MVC_ActionResult(MyController.Name, Actions.Save); Agreed that the controllers themselves definitely have to remain instances as you mentioned. Not sure if this would have other adverse affects elsewhere on the API.

  • Anonymous
    August 04, 2009
    @Steve I think the Name could indeed be changed to be a const, but would that really help your scenario? That would let you name your Action after your Controller, but that seems like an unusual thing to do :)

  • Anonymous
    August 04, 2009
    @David - LOL - I'm an idiot - I didn't mean to put the controller's name in the attribute - obviously that does not make sense. :) What I meant to say, was to wonder if there was any way to make the Action's names constants. So that it could be written like this: [ActionName(MyController.Actions.Index)] similar to my original example but not prefaced with the MVC static class which holds instances.  But even so, there is a chicken and the egg problem with the code generation so this simply might not be practical.

  • Anonymous
    August 04, 2009
    @Steve Sorry, didn't mean to imply that at all; I figured I was misunderstanding you! :) But actually, what you describe can work.  e.g. in T4MVC.tt, try adding this block after the block that has 'public class _Actions':        public class TestActions { <#          foreach (var method in controller.ActionMethodsWithUniqueNames) { #>            public const string <#= method.ActionName #> = <#= method.ActionNameValueExpression #>; <#          } #>        } Now you can write what you want, e.g. [ActionName(MyController.TestActions.Index)], and I don't think you'll hit a chicken/egg problem. But the reason I'm hesitant to completely replace the existing mechanism with this is that MVC Views typically don't import the Controller's namespace, so you wouldn't be able to easily use them in there. But it's conceivable to have both if we can do it without causing confusion. Maybe we should continue this conversation in email, it's getting long! :)

  • Anonymous
    August 05, 2009
    I'm sorry if this has been covered in a separate discussion, but shouldn't the default HelpersPrefix be 'Mvc', not 'MVC' - the .NET convention is for acronyms longer than two characters to only have their first letter capitalised. (Yes, I'm probably being pedantic!) Thanks for the template. It's a great example for those of us getting started with T4.

  • Anonymous
    August 05, 2009
    @Dave R: you're probably right. I had some concerns that this would cause naming conflicts with the Mvc namespace (as in System.Mvc), which is why I went upper case.  Note that you can easily change it by setting HelpersPrefix in T4MVC.settings.t4.

  • Anonymous
    August 24, 2009
    Am I really the only one here to run StyleCop on my MVC project? The T4MVC.cs class generates all sorts of StyleCop warnings.  To get around them I first tried to add a project file Compile entry: <Compile Include="T4MVC.cs">      <AutoGen>True</AutoGen>      <DesignTime>True</DesignTime>      <DependentUpon>T4MVC.tt</DependentUpon>      <ExcludeFromStyleCop>true</ExcludeFromStyleCop> </Compile> Unfortunately that only works OUTSIDE Visual Studio. I then tried some of the solutions outlined at http://sergeyshishkin.spaces.live.com/blog/cns!9F19E53BA9C1D63F!253.entry The region works, but the easiest is to simply put a // <auto-generated /> comment at the top of the generated file. Would be nice to see this included in the next release.

  • Anonymous
    August 24, 2009
    The comment has been removed

  • Anonymous
    August 24, 2009
    Also due to the client side caching strategy we use it would be great if we could define a postfix. This would allow us to do something like "http://static.stuff.com/Assets/Scripts/test.js?v1.1"... that way we can force browser to re-get the content.

  • Anonymous
    August 24, 2009
    The link to sergey's blog is broken above - here is a shortcut: http://bit.ly/stylecopignore

  • Anonymous
    September 02, 2009
    @austegard: I just uploaded build 2.4.02 to CodePlex, and added the auto-generated comment as you suggested.

  • Anonymous
    September 02, 2009
    @Anthony: I just uploaded build 2.4.02 to CodePlex.  It has a new LinksNamespace setting that you can change in T4MVC.settings.t4. I think this is what you meant but may be wrong.  If needed, we can take this offline by email to discuss further.

  • Anonymous
    September 02, 2009
    @Anthony: reading your comment again, it's clear that this is not what you meant! :)  Let's take this offline and discuss.

  • Anonymous
    October 14, 2009
    I am using T4MVC 2.4.03 with the MVC 2.0 preview 2 and with one small change to the template its working great. The System.Web.Mvc.Html.LinkExtensions.RouteLink extension method now returns type MvcHtmlString rather than string, as the template expects.  So all calls of the form: return htmlHelper.RoutLink(...); need to be update to read: return htmlHelper.RoutLink(...).ToHtmlString(); and voila, it works!  Hope this helps.

  • Anonymous
    October 15, 2009
    @Ken: indeed, this was broken. I just updated a fixed build (2.4.04) and blogged it. Thanks!

  • Anonymous
    October 21, 2009
    Thanks for all your work! I tried dropping it into a simple app I'm working on (based on Steve Sanderson's book) and I got the following error on build: error CS0246: The type or namespace name 'T4MVC_ActionResult' could not be found (are you missing a using directive or an assembly reference?) It seems the template is not generating this class. But when I try dropping it a newly-created app, it works. Any ideas?

  • Anonymous
    October 21, 2009
    @Gabriel, not sure what would cause that. If you email me a zipped minimal repro, I'll be glad to take a look.

  • Anonymous
    January 10, 2011
    Hi, I converted my solution to run with VS2010 from VS2008. But I'm still running .Net 3.5 instead of 4. T4MVC has stopped working and is not able to generate any code. Would be great if you could please help with this. Thanks.

  • Anonymous
    January 10, 2011
    @Divi: could you please post a question on StackOverflow? It may take a few back and forth to figure it out, and blog comments quickly get messy!