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
Anonymous
June 30, 2009
The protected constructor is a very interesting approach. Now I have to pull it down and do some testing around my container of choice.. StructureMapAnonymous
June 30, 2009
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, christianAnonymous
June 30, 2009
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".Anonymous
June 30, 2009
This is a really useful template, it has become an integral part of my current project. Thanks for sharing.Anonymous
July 01, 2009
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.Anonymous
July 01, 2009
The comment has been removedAnonymous
July 01, 2009
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.Anonymous
July 01, 2009
The comment has been removedAnonymous
July 01, 2009
Christian, new build 2.2.01 on CodePlex throws NotSupportedException as you suggest. Thanks!Anonymous
July 01, 2009
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.Anonymous
July 01, 2009
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:
- I wanted 'Go To Definition' on MVC.Home.Index() to go to the user's Index method, and not the generated derived method.
- Having it on the base lets your shorten MVC.Dinner.Views.InvalidOwner to simply Views.InvalidOwner when writing code in the Dinners controller.
Anonymous
July 01, 2009
Hien Khieu: see my reply in the other post.Anonymous
July 02, 2009
The comment has been removedAnonymous
July 02, 2009
The comment has been removedAnonymous
July 09, 2009
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".Anonymous
July 10, 2009
David, This doesn't work if the controllers are in a different assemblyAnonymous
July 10, 2009
Mark Leistner: that's an interesting idea. I'll put that on the list of future scenarios to look at.Anonymous
January 18, 2011
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