Thursday, January 13, 2011

Designing ASP.NET MVC Routes for a Tag-based application

I've written a couple of posts this week to describe problems that I've tackled in developing a new web application that I'm building for personal use:

As you can probably tell from these titles, the application that I'm developing is centered around the principle of tag-based metadata.  One of the important features in this application is that I want users to be able to use multiple tags to narrow results - similar to how Delicious does it.  For example, the following URL will return MVC articles written by 'The Gu':
http://www.delicious.com/tag/aspnetmvc+scottgu
I wanted to have a similar experience and so I factored this in when I sat down to design my URL routing scheme.  In the initial cut, I identified the following routes:

Path
Details
/groupADisplays all data for "Group A"
/groupA/cricketDisplays all data tagged as 'cricket' for "Group A"
/groupA/cricket+battingDisplays all data tagged as 'cricket' and 'batting' for "Group A"
/tags/cricketDisplays all data tagged as 'cricket' from any Group

Based on these requirements, I decided to implement the following two actions:
ContentController.Group(string groupName, IEnumerable tagValues) ;
ContentController.Tags(IEnumerable tagValues) ;
And I wired those up by using the following Routing configuration:
routes.MapRoute("1", "tags/{tagValues}", new { controller = "Content", action = "Tags" });
routes.MapRoute("2", "{groupName}/{tagValues}", new { controller = "Content", action = "Group", tagValues = ""});
So now, when users enter the URL's that I have in my example table above, they will successfully be routed to the correct Controller Action's, but there's still one step to go before it will work as desired.  As things currently stand, if the user passes through multiple tag values to one of these actions, they will be received only as a single value and we will have to parse them into their separate tag values.  For convenience, it would better if that parsing had already occurred and we could work direct with the individual tags by the time they reached our controller Actions.

The trick is of course to combine what I showed in the previous article and to implement a custom ActionFilter on the controller.  In this case, the custom ActionFilter will look like this:
public class TagValuesFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.RouteData.Values["tagValues"] == null)
            return;

        var list = new List<string>();
        var tagValues = filterContext.RouteData.Values["tagValues"].ToString()
            .Split("+".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

        foreach (var tagValue in tagValues)
        {
            list.Add(tagValue);
        }
        filterContext.ActionParameters["tagValues"] = list;
    }
}
And now all that is left is to simply apply that attribute to the ContentController like so:
[TagValuesFilter]
public class ActivitiesController : Controller
Now every request will be correctly routed through to the right controller Action's and the tag values will be already nicely split out for us, making the handling code nice and simple and free of any parsing code.

No comments:

Post a Comment