Posted by Will on Thursday, August 14, 2008 at 12:57 PM

One of the big new features in the upcoming release of HappyFish will be the ability of users to upload their data to their account and then download it elsewhere.  This is perfect for users who have HappyFish installed both at home and work.  Each time HF starts it will pull down the latest changes and then push them back up when it exits.  The value added feature in uploading will be that user's subscriptions will be anonymously aggregated to form a feed directory.  The result (I hope) is a directory of feeds that people actually subscribe to rather than a huge spam list.  Optionally, users will also be able to share, in a public feed of their own, their feed subscriptions along with favorite feed items and related comments.  I don't know about you, but that sounds like a perfectly good excuse for testing out the ASP.Net MVC framework.

For this article I am assuming you've worked through some introductory material like the tutorial videos found on the ASP.net MVC page.  At the time of this writing even though I've got Preview 4 installed on my machine the Preview 3 introductory videos are still relevant.  The videos do a good job of covering the basics of getting started.  My goal here is to expand on some of the details associated with setting up an MVC site that were not apparent to me when I first began.

Controllers and Actions

One of the differences in working with an MVC application that stands out even before delving into the code structure is the way the URLs in an application change.  No more http://mysite.com/default.aspx.  Under MVC the default page address probably looks like http://mysite.com/home.  The parts of URL specify controllers, actions, and arguments.  This is similar to class, method, arguments of the typical programming model.  The URL pattern maps to a route in the Global.asax file.  The default route specified is

routes.MapRoute(
    "Default",                                             // Route name
    "{controller}/{action}/{id}",                          // URL with parameters
    new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);

Based on that I began organizing my application around intended controller actions and arguments:

Controller Action Argument Description
Feeds Search keywords Search by keywords in the feed's title or description
  Browse letter of alphabet Alphabetical listing by feed title
  Recent number of new feeds to show The newest x number of feeds added to the directory
  Rating number of stars Lists feeds by user rating
  User username Not sure about this one

Everything is pretty straightforward until I come to the User action.  For the user's feed subscriptions I want two things, a web page view of the user's subscriptions and the subscriptions output in OPML format.  Likewise, with the user's favorite feed items I want to provide a web page view as well as output in RSS format.  Sounds like I need to promote the User action concept in my application to a controller. 

Controller Action Argument Description
User Feeds username, format The users feeds as either a web page or OPML.
  Favorites username, format The users favorites as either a web page or RSS.

This seems reasonable.  I have a lot of options for the action argument.  I could accept the username with an optional dotted suffix a la

  • Directory/User/Feeds/thirstycrow - outputs the feeds in a web page
  • Directory/User/Feeds/thirstycrow.opml - outputs the feeds in opml format

or simply always output the feeds in OMPL format and style them for browser consumption with xslt.  <groaning and gnashing of teeth />  I think I'll go with the former - a single argument.  It cleanly expresses the intent and will look familiar to users to see the .opml or .rss suffixes.  Still, seeing username, format made me wonder how to route multiple arguments to an action like

public ActionResult Feeds(string username, string format) { ... }

I fooled around with the Default route in the Global.asax page, extending the path with more slashes and parameters, but could not even get a single parameter Action to work. 

"id" Matters

When I set up my first test controller action it was

public ActionResult Search(string keyword) { ... }

However, when I tested ../Feeds/Search/test the argument 'test' wasn't making into the method.  Experimenting I found that ../Feeds/Search/?keyword=test worked, which was not the attractive URL that MVC had promised.  Is this some sort of bait and switch?  It turns out that by changing my method to

public ActionResult Search(string id) { ... }

the 'test' argument made it through.  In other words {id} in the default route "{controller}/{action}/{id}" should be the name of the argument in the method as well.  Despite going through several tutorials I never picked that up.  Applying this to the two argument question I now see the answer is an action on my User controller that looks like this

public ActionResult Feeds(string id, string format) { ... }

with a corresponding URL of ../User/Feeds/thirstycrow?format=opml.  Not as aesthetically pleasing as ../User/Feeds/thirstycrow.opml, but it's the answer none the less.  The thing to keep in mind is that what I've said above applies only if I want to funnel everything through the default route.  Ultimately what I realized I needed to do was establish other routes in my Global.asax file.  And this is where the routing concept finally clicked.  Back to Global.asax.

Expanding Routes

The purpose of routes in the MVC is to map a given URL pattern to a controller's action and pass in the associated arguments, if any.  Understanding that and based on what I had learned from experimentation I saw that if I want ../User/Feeds/thirstycrow.opml to map to

public ActionResult Feeds(string username) { ... }

on the User controller, instead of modifying the default route, I needed to add a route.

routes.MapRoute(
    "UserFeeds",               //Route name is arbitrary
    "User/Feeds/{username}",
    new { controller = "User", action = "Feeds", username = "" });

And by extension, a multi-argument action

public ActionResult FormattedFeeds(string username, string format){ ... }

requires a different route.

routes.MapRoute(
    "FormattedUserFeeds",               //Route name is arbitrary
    "User/Feeds/{username}/{format}",
    new { controller = "User", action = "FormattedFeeds", username = "",
    format = "" });

The FormattedUserFeeds route above would handle ../User/Feeds/thirstycrow/opml.  Not bad.  I still prefer the .opml variant, but you can see the point.  Don't funnel everything through the default route.  It's important to note in the above snippets I did not overload the Feeds method as you might expect.

public ActionResult Feeds(string username) { ... }               //will not work
public ActionResult Feeds(string username, string format){ ... } //will not work

Attempting an overload, even with differentiated routes like those shown above will throw an error along the lines of "More than one action named 'Feeds' was found on controller..."

Next Step

Having grasped the relationship between URLs, routes, controller actions, and arguments the next step is to write the bodies of the controller actions and create the associated views.  In the case of the HappyFish feed directory I'm using the new SQL Server Data Services beta for the back end.  In the next post I'll cover some of what I've learned while experimenting with the private beta.

Comments [1]     Categories: asp.net | MVC