Extending the RouteCollection in ASP.NET Routing
I have been developing a pure MVC CMS for fun and have run into an annoying bug/feature of ASP.NET routing.
Each dynamic managed page in my CMS is associated with a particular route that is pulled from the database. These are loaded at app start. When a user adds a new page or edits the Url of an existing page I need to be able to edit the RouteTable to insert/edit route accordingly.
The problem is that the new route does not need to be simply added to the end of the RouteCollection, instead it may need to be inserted into a particular position. Seems logical enough except that the RouteCollection contains only a standard Insert(int idx, RouteBase route)
method inherited from Collection<T>
which doesn't contain the route name. The name of the route is important as I use it throughout to开发者_JAVA技巧 generate action links.
Looking at reflector I can't see an easy way to extend this collection as the _namedMap dictionary is marked as private. I tried chopping the collection at the point of insert and re-adding each item again, however because there is no method to reverse lookup a route's name from the RouteCollection I cannot re-add them with the name they may have had before. So frustrating!!!
Why is the name of the route not a property of the route object? Why if MS is serious about us extending MVC and Routing do they make crucial classes hard to extend?
Any suggestions as to the best solution here?
Edit :
Ok maybe I should have been much much clearer here. I am not looking for a critique of my CMS design. I appreciate the comments but this is not what I am asking.
Simplified question. How can I insert a named route into the route collection at runtime? The current insert method on the class is insufficient as it does not include the name.
Cheers,
Ian
If you have a look at the ClearItems()
method, this should provide a way to empty out the routes. If you first move your routes collection into a temporary collection (and insert your new route in there as well), run ClearItems() and then re-populate using Add()
.
It should be mentioned that you should also use the GetReadLock()
and GetWriteLock()
in order to avoid potential conflicts in your application.
Look at the IRouteConstraint
interface. Basically you add a catch all route add the end of the collection at application startup, which takes a constraint object as parameter. In here you would lookup in your CMS if the incoming url matches a valid page and then return true or false, which will instruct the routing framework whether the route should be considered for the incoming request or not
public interface IRouteConstraint
{
bool Match(HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection);
}
http://msdn.microsoft.com/en-us/library/system.web.routing.irouteconstraint.aspx
Defines the contract that a class must implement in order to check whether a URL parameter value is valid for a constraint.
Short answer: you don't insert routes dynamically. When it's time to change them, they need to be rebuilt from scratch. There's several reasons for this, most tied to ensuring the routing system is not the bottleneck for your application. Essentially the set of routes is intended to be a small static set of resources that map a large number of URLs. Everything about the routing system was designed with that in mind.
This is a departure for many developers, particularly coming from file-based frameworks (like routeless WebForms projects). It does force you to think about the URLs a bit differently.
URL Routing was recently made popular by Ruby on Rails, which in turn got the idea from a paper on Representational State Transfer (REST): http://en.wikipedia.org/wiki/Representational_State_Transfer . The concept existed prior to Rails, but it is a concept that was carried over to ASP.NET MVC.
The principle behind RESTful routes is that they have a common structure, with only certain portions changing. In your application, it would be the name of the managed page. By default, ASP.NET matches your route like this:
/{controller}/{action}/{id}
This means that the portion of the URL that matches where the "{controller}" word is will be stored in the "controller" parameter. Same with {action} and {id} as well. What this means is that you can have common logic for managed pages like this:
/Page/Details/I eat spinach
That gets mapped as follows:
- controller = "Page" (maps to the PageController class in your Controllers directory)
- action = "Details" (maps to the Details method on the PageController class)
- id = "I eat spinach" (passed as a parameter on the Details action.
Your controller code would have a method like this:
public ActionResult Details(string id)
{
return View(db.FindPage(id));
}
With those basics out of the way, we are not limited to this structure. As long as we have a way of providing a mapping to the right controller, the right action, and can look up the managed page by id, we can make the URL what we want. Let's say we wanted the page name to come first, and the action to come second, and we didn't want to worry about the controller at all. We would create a route that looks like this:
routes.MapRoute(
"ManagedPages", // Route Name
"{id}/{action}", // URL structure
// Default route parameters
new { controller = "ManagedPage", action = "Details", id = "Home" }
);
The parameters provide default values if they are not overidden in the URL. That means a blank URL will always match to ManagedPageController.Details("Home")
. If you wanted to edit the page, the URL might look like "I eat spinach/Edit".
There are some caveats to the "id" parameter, which relate to forbidden characters the MVC tries to save you from. If you force the page names to never include these forbidden characters you will have a lot fewer problems.
精彩评论