So something that has been somewhat of an uphill climb as of late is routing in MVC. I think part of it is the way it works and part of it is that I’ve approached MVC like a 10 year old with a new game; F- the instructions, I’m all up in this.
What goes out must come in…
The biggest misconception I had with routes is that the route definitions meant something when a page is loading. This is bad. Now before the nerd rage starts to build, I will qualify that statement. Route definitions mean nothing beyond a simple mapping of values. What? Well suppose I have two routes:
routes.MapRoute
(
...
"{controller}/{action}/{roomId}/{name}",
...
);
and
routes.MapRoute
(
...
"{controller}/{action}/{userId}/{name}",
...
);
Now on the way out, the system can find the route easy if you supply the correct route data: (Lets say for the second route)
routeValues = new RouteDicationary()
routeValues.Add("controller", "Room");
routeValues.Add("action", "Index");
routeValues.Add("userId", 1);
routeValues.Add("name", "ProgramminTool");
neededHelper.RouteUrl("SecondRoute", routeValues);
Which will give me a wonderful address of:
/User/Index/1/ProgramminTool
Awesome, the routing system did what I wanted. Problem is, I assumed too much coming in, ie when the page is loading from the url. (Say from a clicked link) I kept getting an error like:
RoomId doesn’t exist. Make roomId nullable. Blah blah blah I hate you and you should quit programming and do something more equal to your ability level like spinning in circles.
Though I might have take some artist license on the error, the meaning was simple: it was trying to run that url against the Room controller and it couldn’t find the roomId parameter in the request. Well that doesn’t make sense, after all it’s obvious that it should be looking for the user controller and use the route with the userId paramter. Sadly, it doesn’t work that way and not sure it could. Why? Well look at the url.
/User/Index/1/ProgramminTool
Now if you were too look at that, what would you think? You have four values, values that you can’t really guess the type since you have no real context. After all there could easily be a method that takes in a string userId as opposed to an integer userId. On top of that, it has no idea what the 1 is. There’s no userId=1 to tell it what it is. So what does it do? It goes down the route table and finds the best fit. Now you tell me, which does this url fit better?
"{controller}/{action}/{roomId}/{name}"
Or
"{controller}/{action}/{userId}/{name}"
It’s kind of a trick question since the answer is neither. It will just fit it to the first one that it likes, and in this example it’s the roomId one. Problem is, now the user controller method that is looking for the userId fails because there is no userId. Uhg.
Computers are dumb, they can only do what we tell them.
Fact is, in this situation I have to make some rules for the routing system to take in and start digesting the url properly, and there’s a way to do this: IRouteConstraint.
Say that you have certain controllers or actions you don’t want to use the first route. For this example I’ll use user actions from my current project. On the room index view there is a button for adding the room as a favorite. Now the action/method it needs is on the user controller, not the room controller. Therefore I have to post some information to the user controller using a route that looks like this:
"{controller}/{action}/{userId}/{roomId}"
But it is preceded by one that looks like:
"{controller}/{action}/{id}/{name}"
See the issue? Now when MVC generates the url, everything is fine. After all it uses the route name and the parameters to pick the correct route. On the way in, not so good. It naturally tries to conform the url to the id/name route. BOOM HEADSHOT! Now the solution.
public class NotGivenAction : IRouteConstraint
{
//This is the action that we want to prevent from the route accepting
private String GivenAction { get; set; }
public NotGivenAction(String givenAction)
{
GivenAction = givenAction;
}
public Boolean Match(HttpContextBase httpContext, Route route, String parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
String parameterValue = values[parameterName].ToString();
//Make sure the parameter exists and it doesn't match the bad action
return values.ContainsKey(parameterName) && parameterValue != GivenAction;
}
}
The IRouteConstraint interface has a method Match that has to be given life. The short of it is take in the parameter name and find it’s value and then see if the value is the same as the action it’s looking for. For example, if I had an action of AddToFavorites, this would go through and make sure it isn’t that action. If it is, it knows to not use the current route for this action.
routes.MapRoute
(
GeneralConstants.RouteIdName,
"{controller}/{action}/{id}/{name}",
new { controller = "Room", action = "Index", id = "0", name = "None" },
new { action = new NotGivenAction(UserControllerConstants.AddToUserFavorite) }
);
routes.MapRoute
(
GeneralConstants.RouteUserIdRoomId,
"{controller}/{action}/{userId}/{roomId}",
new { controller = GeneralConstants.ControllerUser, action = UserControllerConstants.AddToFavorites, userId = "-1", roomId = "-1" }
);
So now it will skip the first and push to the second when the url looks like:
/User/routes.MapRoute/AddToFavorites/1/2/
Take that you f-ing routes. KING KONG AIN’T GOT S- ON ME!