views:

241

answers:

4

Hello Everyone,

I would like to achieve something very similar to this question, with some enhancements.

There is an ASP.NET MVC web application.

I have a tree of entities.
For example, a Page class which has a property called Children, which is of type IList<Page>. (An instance of the Page class corresponds to a row in a database.)

Note that the owners of the site can add a new page anytime, or delete existing ones, and the URLs should reflect those changes as well.

I would like to assign a unique URL to every Page in the database.
I handle Page objects with a Controller called PageController.

Example URLs:

http://mysite.com/Page1/
http://mysite.com/Page1/SubPage/
http://mysite.com/Page/ChildPage/GrandChildPage/

You get the picture.
So, I'd like every single Page object to have its own URL that is equal to its parent's URL plus its own name.
In addition to that, I also would like the ability to map a single Page to the / (root) URL.

I would like to apply these rules:

  1. If a URL can be handled with any other route, or a file exists in the filesystem in the specified URL, let the default URL mapping happen
  2. If a URL can be handled by the virtual path provider, let that handle it
  3. If there is no other, map the other URLs to the PageController class

I also found this question, and also this one and this one, but they weren't of much help, since they don't provide an explanation about my first two points.

I see the following possible soutions:

  • Map a route for each page invidually.
    This requires me to go over the entire tree when the application starts, and adding an exact match route to the end of the route table.
  • I could add a route with {*path} and write a custom IRouteHandler that handles it, but I can't see how could I deal with the first two rules then, since this handler would get to handle everything.

So far, the first solution seems to be the right one, because it is also the simplest. But still, even in that case I'm not sure how could I make the PageController to handle the requests.

I would really appreciate your thoughts on this.

Thank you in advance!

EDIT: I now had the time to examine every aspects of every answer I received. I accepted Neal's answer, since he is the one providing the best explanation about how things work. I also upvoted all other answers, since they provide good ideas.

+1  A: 

One different idea is to use T4 (Text Template Transformation Toolkit) to read your Children once and generate the content of your Global.asax file.

EDIT: Bascially with T4 you can automate text file generation. For instance, instead of manually copying out items of some huge collection and pasting them with some specific context into a text file (like INSERT INTO [MyTable] (Text) VALUES (@ItemText)) you could have a T4 engine read the collection and generate these insert statements for you. It is static and not meant for a runtime.

I find a very good introduction is available from the Pro Entity Framework 4.0 book.

But if you say you need to do it dynamically, this may not be the tool for you.

Developer Art
I'd be glad if you could provide some more detail, I've never heard of T4.BTW, the pages can change during runtime. Admins of the site may add/remove/change them as much as they want.
Venemo
Thanks for pointing out that it may not be the tool for me. Indeed, the collection changes in runtime. Anyway, thank you for taking the time to answer me. :)
Venemo
+2  A: 

Routes are processed in the order they are added to the collection. You could add your custom route after the existing routes to ensure it is the last one to get a chance at handling the request. This will allow you to add routes for existing files (virtual or otherwise) before it and therefore meet criteria 1 and 2.

By default, MVC routing will route to existing files before applying any routes stored in the route collection; see http://msdn.microsoft.com/en-us/library/system.web.routing.routecollection.routeexistingfiles.aspx. (hattip to Paul - see comments).

To route requests to your page controller, simply create a custom route that examines the virtual path and if it matches the pattern for a page in the database returns the RouteData. Set up your RouteData with the appropriate values extracted from the virtual path (e.g. set the Path key to /Parent/Child/Grandchild), set the controller key to your page controller name (e.g. Page) and the action to the name of the action that you want executed (e.g. Show). The RouteData should be created with the MvcRouteHandler (not sure if that is the correct class name).

To ensure that urls to your database driven pages are returned correctly, override the GetVirtualPath( RequestContext, RouteValueDictionary ) method of RouteBase and use the route values passed in to determine if this is a database driven page and if it is create the virtual path data required (or return null otherwise).

For help with overriding GetRouteData and GetVirtualPath, look at the reflected source code of System.Web.Routing.RouteBase and System.Web.Routing.Route; after that Google is your friend.

Routes are used in reverse to determine the url given the controller, action and any other route values. You should be able to utilise this to build the url of the page within the context that it is being requested.

Neal
Why and how would I add routes for existing files? Also, how would I build the route for the tree and direct them to the appropriate Controller?
Venemo
Paul
@Paul - Thanks. Does this apply to files provided by a virtual path provider as well?
Venemo
@Venemo> Should be both; the RouteCollection has a field called _vpp which is a VirtualPathProvider that it gets injected through its constructor (or takes the static instance out of the HostingEnvironment). It then calls this._vpp.FileExists and returns null if the file exists. Returning null tells the system that the Routing subsystem isn't handling that request. So unless it's not getting a copy of the VirtualPathProvider you're using for some reason, it should ignore those files.
Paul
@Paul - Thank you very much for that!
Venemo
@Neal - After all this time, I finally had the time to think this through and implement your idea. It works fine, thank you very much for it!
Venemo
@Venemo - Good to hear. Glad I could be of help =)
Neal
@Neal - one small issue though: I have to place my route before the default MVC route because otherwise it doesn't work.
Venemo
+1  A: 

You know your pages structure when you save page. So, you can generate URL for each page and save it in to the database record. Then you can use {*path} rule and find exact match in database. This rule should be last in your rules definition, so you can match other routes.

For example, your Page1 has no parent page, it's url is Page1. Your SubPage knows it's parent so it can ganarate url Page1/SubPage etc.

rarouš
rarouš - Thank you for your answer! This is a good ida but won't `{*path}` catch real (and virtual) filenames, too?
Venemo
+1  A: 

You could use a "Page/{*path}" pattern. You can then either decompose path by splitting the string on ‘/’ and walk that, or you can use Rarouš’ suggestion of storing the [generated] path in the DB and do a direct lookup.

If you use Rarouš’ method then you will have to update path entries in your table for all the children when the parent path changes. This can be done simply enough with a single update query.

I’m assuming that you are mapping the page you wish to use for the home page somewhere in a config file, or table entry. You can have your home page controller either do the lookup and return the content for the home page view to render (you can use a shared view, partial view, or call into the page controller so that you don’t duplicate behaviour), or you can have it redirect to that page.

Using this technique you can have a single page controller and view that handles all these pages in the same way. Your other requirements seem to be handled automatically by the MVC framework.

Your path would look like this:

http://mysite.com/Page/Page1/ 
http://mysite.com/Page/Page1/SubPage/ 
http://mysite.com/Page/Page/ChildPage/GrandChildPage/ 

You can of course use a prefix other than "Page".

Andre Artus
@Andre - Thank you for your answer! This is the best idea so far. :)
Venemo
You can order your route entries from most specific to least specific and drop the prefix, use RouteTester (http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx) to check your routes.
Andre Artus
http://stephenwalther.com/blog/archive/2008/08/03/asp-net-mvc-tip-29-build-a-controller-to-debug-your-custom-routes.aspx
Andre Artus
@Andre - Nice articles, thanks!
Venemo
@Venemo - Pleasure, have you found your answer yet?
Andre Artus
@Andre - Every answer is very nice, but I haven't got the time to implement a solution yet. (This project is something I do in my free time, which I haven't had much in the past weeks.)
Venemo