T4MVC 2.2 update: Routing, Forms, DI container, fixes


To get the latest build of T4MVC:


Go to T4MVC home page


This post is a continuation of various recent posts, most notably:



First, I’d like to thank all those who are using the MVC T4 template and sent me suggestions and bug reports.  Most issues have been addressed, and most suggestions have been integrated.  I’m up to the 8th CodePlex drop, and it’s only been a week! You can see the history of changes at the top of the .tt file.


Frankly, when I started playing with this, I just thought it’d be a fun thing to spend the afternoon on.  Instead, I have probably spent close to half my time working on it in the last week.  And I do have other things to work on! 🙂  But it’s been a lot of fun, so no regrets!


I’m not going to detail every change since the last blog post, since many are bug fixes that are not particularly exciting (though they help make the thing work!).  Instead, I’ll just focus on a few new key areas.


Strongly typed support for MapRoute


This is similar to the RedirectToAction/ActionLink support, but applied to route creation.  The original Nerd Dinner routes look like this:

routes.MapRoute(
“UpcomingDinners”,
“Dinners/Page/{page}”,
new { controller = “Dinners”, action = “Index” }
);

routes.MapRoute(
“Default”,
“{controller}/{action}/{id}”,
new { controller = “Home”, action = “Index”, id = “” }
);


The ‘defaults’ line of each route is what we’re trying to fix.  It makes heavy use of anonymous objects and literal strings, which should be avoided whenever possible.  If your controller or action name change, you won’t easily catch it.


Previous version of T4MVC let you change that line to:

new { controller=MVC.Dinners.Name, action=MVC.Dinners.Actions.Index }

This fixes the issue for the most part, but it’s a bit wordy and doesn’t support refactoring.  Now, the latest version of T4MVC lets you change those routes to:

routes.MapRoute(
“UpcomingDinners”,
“Dinners/Page/{page}”,
MVC.Dinners.Index(null)
);

routes.MapRoute(
“Default”,
“{controller}/{action}/{id}”,
MVC.Home.Index(),
new { id = “” }
);


Note how we are now pointing at the action by simply making a call to it (though it’s a pseudo-call, and your action method won’t actually be called).


Also note that in the second route, we’re still using an anonymous object for the ‘id’.  This is because even though the route expects an id, the default Action method (HomeController.Index()) doesn’t take one.  You could get around this by adding a ‘string id’ param to HomeController.Index() and ignoring it in the method.  You could then write MVC.Home.Index(null) in the route and avoid all anonymous objects.


BeginForm support


This came courtesy of Michael Hart.  Essentially, it’s the same support as ActionLink() but for the BeginForm() method.  But note that because form posts typically pass most of the data via the form and not the URL, BeginForm() is trickier to use correctly than the other methods.


Here is how you might use this:

using (Html.BeginForm(MVC.Account.LogOn(), FormMethod.Post)) { … }

Or if the action method takes parameters, you would pass them in the call.  However, when you do this you need to make sure that you aren’t also trying to pass those same parameters via the form (e.g. using a text box).


Generally, the rule of thumb is that this support works well when you’re dealing with forms where the Action method signature exactly matches the form URL.


Support for Dependency Injection containers


Though this is a bug fix rather than a new feature, I’ll mention it because it affected several people and had me puzzled for a while.


The root of the problem is that T4MVC generate new constructors for the controllers, and that DI containers use a somewhat arbitrary algorithm to decide what constructor to use.  And in those users’ cases, they ended up calling the new constructor generated by T4MVC, messing things up badly.


The fix was simply to change the generated constructor to be protected, to prevent DI containers from using it.  Since T4MVC only uses it when it instantiates the controller derived class, protected works fine.


Well, I should note that I haven’t actually verified that the fix addresses the issue, but I’m sure those users will tell me if it doesn’t! 🙂

Comments (18)

  1. eric hexter says:

    The protected constructor is a very interesting approach.  Now I have to pull it down and do some testing around my container of choice.. StructureMap

  2. Christian says:

    hi david,

    the protected constructor fixed the problem for me. (I’m using StructureMap) THANK YOU!

    but why do even create this constructor with your dummy-parameter? my workaround yesterday was to just remove this one and make sure that there’s always a default constructor. (I think this is done by your template anyway)

    StructureMap for example always calls the constructor with most parameters, so this already fixes the problem.

    thanks for working on this,

    christian

  3. Christian says:

    the ExecuteResult()-method of your ControllerActionCallInfo class should not throw a "NotImplementedException" because ReSharper for example shows this as an open entry in the TODO-list. I think it would be better if you throw a "NotSupportedException".

  4. This is a really useful template, it has become an integral part of my current project. Thanks for sharing.

  5. Anthony says:

    Love the new strongly typed support for MapRoute…

    My only concern is that when one uses "MVC.Home.Index()" what this does is not 100% clear to a ready who doesn’t know about the templates or how they work. As in when a user sees "MVC.SomthingOrOther", the MVC doesn’t mean much to me.

    What this template is all about and the fluid interface it generates is strongly typed names/references. So couldn’t we come up with something a little more suggestive than just "MVC" that helps the user out a little more like "RouteFor.SomthingOrOther"???

    Now in reality it doest have to be " RouteFor ", but if someone saw RouteFor.Home.Index() vs. MVC.Home.Index() it might be a little more clear about what the intending usage is in a given scenario.

    I just think there must be a better name than just MVC that helps describe to the user whats going on.

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

  7. Anthony says:

    Just another quick comment. The Members and Classes that you add to the partial class of each controller (with the exception of RedirectToAction), why wouldn’t you you put them into the sub type "T4MVC_xyz" that you generate for a given controller and change the static reference in the MVC class to "public static T4MVC_SquadProfileController = new T4MVC_SquadProfileController();"?

    Just a thought.

  8. davidebb says:

    Christian: we need the dummy ctor because if the Controller already has a default ctor, it may have logic we don’t want to execute. So I guess technically, we only need the Dummy when there is already an explicit default ctor, so we could optimize that a bit.

  9. davidebb says:

    Christian, new build 2.2.01 on CodePlex throws NotSupportedException as you suggest.  Thanks!

  10. davidebb says:

    Anthony: I agree that the syntax on routes doesn’t make it super clear what it’s doing. What’s tricky is that ‘MVC.Home.Index()’ is the standard syntax T4MVC to encapsulate an action call with params, so it looks the same here as in ActionLink, RedirectToAction, …

    Of course, we can change the MVC prefix if we find a better one, but it would be used in those other places as well.

    And finally, we could rename the MapRoute() extension method to something like MapRouteToController() if we think that’s better.

  11. davidebb says:

    Anthony: I actually started out this way, with new members added in the derived class.  However, I found two reasons to do it as I did:

    1. I wanted ‘Go To Definition’ on MVC.Home.Index() to go to the user’s Index method, and not the generated derived method.

    2. Having it on the base lets your shorten MVC.Dinner.Views.InvalidOwner to simply Views.InvalidOwner when writing code in the Dinners controller.

  12. davidebb says:

    Hien Khieu: see my reply in the other post.

  13. Hi,

    I made the following changes at line 282 to get the code to compile with FileResult.

    <# foreach (string resultType in ActionResultTypes) { #>

    [CompilerGenerated]

    public class T4MVC_<#= resultType #>: <#= resultType #>, IT4MVCActionResult {

       public T4MVC_<#= resultType #>(string controller, string action) <# if (resultType == "FileResult") { #>: base("")<# } #>  {

           this.InitMVCT4Result(controller, action);

       }

       public override void ExecuteResult(ControllerContext context) {

           throw new NotSupportedException();

       }

       public string Controller { get; set; }

       public string Action { get; set; }

       public RouteValueDictionary RouteValues { get; set; }

    <# if (resultType == "FileResult") { #>

    protected override void WriteFile(HttpResponseBase response) {

    throw new NotImplementedException();

    }

    <# } #>

    }

    <# } #>

  14. davidebb says:

    Richard: I just fixed this issue in build 2.2.02 on CodePlex. Thanks!

  15. Mark Leistner says:

    How difficult would it be to add support for ASP.Net WebForms pages as well, similar to how static content files are stored under Links.

    Many projects I work on are slowly migrating to MVC and will contain both WebForms and MVC for the foreseable future.  I’d love to be able to reference the files as Pages.Home_aspx instead of "~/Home.aspx".

  16. Kiran says:

    David,

    This doesn’t work if the controllers are in a different assembly

  17. davidebb says:

    Mark Leistner: that’s an interesting idea.  I’ll put that on the list of future scenarios to look at.

  18. Hassan Salama says:

    I can't get the point. How to use protected constructor to solve the problem?

    I'm using StructureMap with ASP.NET MVC2.

    It will be nice if someone provide us with an example as i'm new to MVC, StructureMap, and T4