The next Canton Software Craftsmanship meeting is this Monday. We’ll be going over TDD basics by having everyone follow us along on simple exercises.
Register here if you’e interested.
Hope to see you there!
The next Canton Software Craftsmanship meeting is this Monday. We’ll be going over TDD basics by having everyone follow us along on simple exercises.
Register here if you’e interested.
Hope to see you there!
Thanks to everyone that attended the first Canton Software Craftsmanship meeting this past Monday. It was a great turn-out (around 33 people?) and everyone seemed genuinely excited to see where this goes, myself included. Good food and good discussions all around. Sign up for the next meeting on April 4th if you haven’t already, and be sure to spread the word to anyone that might be interested. Remember, we’re about the practices, not the language, so everyone’s welcome.
I’d also like to thank Brandon Joyce and John Hoover for doing a great job with the logistics needed to get this group off the ground. Great job guys!
The latest version of Taskie (which is available on NuGet) now comes bundled with a console runner. Simply implement ITaskieServiceLocator with your IoC container of choice, and put your compiled assembly with that implementation in the same directory as Taskie.exe. Taskie will pick up on the implementation and use it like normal.
This should reduce the usage overhead if you don’t feel like creating a virtually empty console project in your application, and lower the getting started barrier even more. If you’d still like to host your own console application for Taskie, that option will continue to be supported and its usage hasn’t changed.
We try to strongly type everything in our MVC project, especially URLs. It’s pretty easy to do using all the build in functionality of ASP.NET MVC along with some lovin’ from MvcContrib, but the one situation we’ve always had problems with was client-side javascript. If it’s a basic action call with no arguments, we’re golden (using <%= Html.BuildUrlFromExpression(x => x.MyAction()) %>). It gets tricky when we have a slightly more complex action though:
1 2 3 4 5 | [AcceptAjax] public string DoSomethingComplex(int id, string accountNumber, int amount) { return string.Format("id = {0}, accountNumber = {1}, amount = {2}", id, accountNumber, amount); } |
If we wanted to do an AJAX call to this bad boy, we’d unfortunately have to resort to string concatenation to build up the URL:
1 2 3 4 5 6 | // get these values from form fields or something... var id = 3; var accountNumber = "123456"; var amount = 325; var ugly_url = "/Home/DoSomethingComplex/" + id + "?accountNumber=" + accountNumber + "&amount=" + amount; |
Booo creepy magic strings. Renaming the action name or any of the parameter names left us relying on either ReSharper’s ability to catch the change, manual search and replace, or hoping we had a UI test hitting the page to catch it. Basically, nothing too terribly reliable to keep our app in working order. The more you worry about small changes breaking your application, the less likely you are to refactor it. The less you refactor, the faster your application degrades into nastiness (code not matching up with current business conventions, etc), and the slower you are to respond to change. Not cool.
Before I go further, I should probably throw up this disclaimer: We use the default routes for everything. The application is behind a login page, and we have no need for fancy SEO friendly URLs, so the solution I’m about to show caters to that scenario. If your application leverages custom routes, you’ll either have to tweak this solution to your needs, or figure out something else. Sorry.
You’ll end up being able to build the URL above like this:
1 2 | var beautiful_url = "<%= Html.UrlTemplate<HomeController>(x => x.DoSomethingComplex(Any<int>.Arg, Any<string>.Arg, Any<int>.Arg)) %>" .substitute(id, accountNumber, amount); |
This’ll produce a URL template like "/Home/DoSomethingComplex/{0}?accountNumber={1}&amount={2}" on the client. You then plug in the template’s holes with your client-side values. Pretty simple, really.
There’s a few server and client-side pieces to this puzzle.
The heart of this solution (and the biggest chuck of code) is the actual building of the URL template.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | private static bool onlyTakesInSingleViewModel(string[] routeValues) { return (routeValues.Length == 3 && routeValues[2].ToLower().EndsWith("viewmodel")); } public static string UrlTemplateFor<CONTROLLER>(Expression<Action<CONTROLLER>> action) where CONTROLLER : Controller { var routeValues = Microsoft.Web.Mvc.Internal.ExpressionHelper.GetRouteValuesFromExpression(action); var actionPath = string.Format("/{0}/{1}", routeValues["Controller"], routeValues["Action"]); if (routeValues.Count > 2) { var routeValuesKeysArray = routeValues.Keys.ToArray(); if (onlyTakesInSingleViewModel(routeValuesKeysArray)) { return actionPath; } if (routeValuesKeysArray[2] == "id") { actionPath += "/{0}"; } else { actionPath += "?" + routeValuesKeysArray[2] + "={0}&"; } var placeHolderCounter = 1; if (routeValues.Count > 3) { if (actionPath.Contains("?") == false) { actionPath += "?"; } for (var i = 3; i < routeValues.Count; i++) { actionPath += routeValuesKeysArray[i] + "={" + placeHolderCounter++ + "}&"; } } actionPath = actionPath.TrimEnd('&'); } return actionPath; } |
This method (which has unit tests in the sample project provided at the end of the post) basically builds up the URL template by leaning on a method inside the MVC Futures assembly to get the controller, action, and parameter names. This is the portion you’d have to tweak if you use different routing rules.
Then it’s simply a matter of wrapping the UrlBuilder call with an HTML Helper extension method:
1 2 3 4 5 6 7 | public static class HtmlHelperExtensions { public static string UrlTemplate<CONTROLLER>(this HtmlHelper htmlHelper, Expression<Action<CONTROLLER>> action) where CONTROLLER : Controller { return UrlBuilder.UrlTemplateFor(action); } } |
Looking at the example of using this method above, you can see all the parameters in the UrlTemplate call replaced with calls to an Any class. Technically speaking, whatever values you put in the expression passed to UrlTemplate will be ignored. You can put in nulls for references & nullable types, 0’s for value types, etc. I decided to drive home the point to anyone looking at the code that we don’t care what value they provide by making a very slim class that provides the default value for whatever type is needed:
1 2 3 4 5 6 7 | public class Any<T> { public static T Arg { get { return default(T); } } } |
It drives home that whole not caring point pretty well, but it’s also a bit wordy, especially if there’s 3 or 4 parameters that need specified. You can omit using the Any class and just give dummy values if you want. Your choice.
There’s not a whole lot to the client-side portion. Basically a very simple version of the .NET Framework’s string.Format method (which you’ll probably want to put in an external js file and reference as needed). It’s written as an extension on the string type to make reading the final product a bit more natural:
1 2 3 4 5 6 7 8 9 10 11 12 13 | String.prototype.replaceAll = function (patternToFind, replacementString) { return this.replace(new RegExp(patternToFind, "gi"), replacementString); } String.prototype.substitute = function () { var formatted = this; for(var i = 0; i < arguments.length; i++) { formatted = formatted.replaceAll("\\{" + i + "\\}", arguments[i]); } return formatted; } |
That’s it. Using all these pieces together gives us the final product:
1 2 | var beautiful_url = "<%= Html.UrlTemplate<HomeController>(x => x.DoSomethingComplex(Any<int>.Arg, Any<string>.Arg, Any<int>.Arg)) %>" .substitute(id, accountNumber, amount); |
You can provide the values for the URL template from client-side code, or from your ViewModel by just outputting the value in the proper spot (i.e. use <%= Model.Id %> rather than the client-side id property). This setup has proven very helpful and quite versatile for us so far.
Having shown all this, there are a few potential pitfalls you need to be aware of:
id and amount parameters around would still compile and technically run, but the Javascript would continue passing the id in for the amount parameter and vise-versa. You don’t usually switch around parameters for no reason, but it’s worth noting, as you’d have to do a find usages and make sure all the calls are updated properly. At least you’d be able to find all the usages with certainty, though.Next time you find yourself needing to concatenate strings on the client-side to call URLs, think about using this technique (or something similar) to keep it all strongly typed. If you’re going to work in a static language like C#, you might as well leverage it as much as possible. Strong typing lets you refactor with full confidence that no references to the rename will get left behind by mistake.
You can grab a copy of the source code for the project right here (built with MVC2). Give it a spin and let me know if you have any problems, or if you know a better way to do this without building the URLs by hand.