Where are these extra url parameters coming from in my ActionLink?
Posted on March 3rd, 2011
So you've created your web app in MVC and <3 the hell out of it. You have your menu system and your tools working like a champ. Then, out of the blue, users start complaining about one of your tools. Not a big complaint, but enough of a UX annoyance that they raise a stink. They say that when they are looking at reports and switch from one report to another the date range used for the report is passed along and not reset to the default date range for the new report they are navigating to. What could this be?
Lets investigate. To start with, we will set up some example code to simulate what is occurring. We will create an MVC3 project and use the Razor engine, although this issue that we are investigating occurs in MVC2 also (maybe in MVC1 as well but I have not verified) and is not related to the view engine (can occur with the ASPX view engine as well). Our project will have a HomeController for our home page and a ReportsController for two sample reports called Sales and Traffic. We will use the _Layout.cshtml for our site shell and main navigation. We will also create a model called BaseReport and use strongly typed views for our report pages that implement that model.
Rather than make you wait, I am going to out the culprit right now. Our main navigation is using the Html.ActionLink helper method, and the logic it uses to auto-generate the html is causing our issue. We will see how this occurs as we put all of the pieces into play. First up is our _Layout.cshtml content:
@Html.ActionLink("Go Home", "Index", "Home")
@Html.ActionLink("Sales", "Sales", "Reports")
@Html.ActionLink("Traffic", "Traffic", "Reports")
We have our calls to Html.ActionLink to handle our main navigation to our reports as well as back to our home page. Our HomeController just contains one action method for handling our home page content.
public class HomeController : Controller
public ActionResult Index()
The default _ViewStart.cshtml file that was created by Visual Studio in our project is already wired up to handle our default layout for all views in our main Views directory:
Layout = "~/Views/Shared/_Layout.cshtml";
The Index.cshtml view file in the Views/Home directory sets the page title and displays some heading text:
ViewBag.Title = "Home";
Our reporting application
The ReportsController will have two action methods, one for Sales and one for Traffic. Both will have optional DateTime parameters for startDate and endDate. A user can navigate to one of these reports without a date range and get a default date range (the past month) or they can navigate to the report and request a specific date range in the GET parameters. The controller code contains our two action methods as well as some helper methods for setting the default DateTime values when none are set:
public class ReportsController : Controller
public ActionResult Sales(DateTime? startDate, DateTime? endDate)
var model = this.initializeModel(startDate, endDate);
public ActionResult Traffic(DateTime? startDate, DateTime? endDate)
var model = this.initializeModel(startDate, endDate);
private BaseReport initializeModel(DateTime? startDate, DateTime? endDate)
var validStartDate = this.initializeDate(startDate, DateTime.Now.AddMonths(-1));
var validEndDate = this.initializeDate(endDate, DateTime.Now.AddDays(-1));
return new BaseReport(validStartDate, validEndDate);
private DateTime initializeDate(DateTime? date, DateTime defaultDate)
if(date == null || date == DateTime.MinValue)
From @Model.StartDate.ToString("MMM d, yyyy") to @Model.EndDate.ToString("MMM d, yyyy")
@Html.ActionLink("Back Another Month", "Sales",
startDate = Model.StartDate.AddMonths(-1).ToString("yyyy-MM-dd"),
endDate = Model.EndDate.AddMonths(-1).ToString("yyyy-MM-dd")
We are defining it as a strongly typed view with the line:
Then we set the title value and add our html for displaying the report name and current date range. Below the date range we would have our report data (we will leave that out of our example as it doesn't pertain to the issue we are investigating), and below that we will have an Html.ActionLink for changing the date range. This is just a fixed date range link for the sake of testing our example, however you could imagine this could be a set of date picker controls or maybe some other pre-defined date range links. It really doesn't matter, as long as the end result of changing the date leads to a GET request for the current report that includes the start and end dates in the url structure.
Our Traffic.cshtml content is the same as the Sales.cshtml, only the title and heading text as well as the action value in the Html.ActionLink are set to Traffic:
MVC will handle mapping those GET parameters into the action method parameters. However, since MVC has the power of routing we would like to leverage that and keep our urls looking slick. So we add the routes to do so, which makes us happy...but now we have our problem!
Apparently the Html.ActionLink code is interested in your view data. Since our BaseReport model that we are passing to our view contains property fields for StartDate and EndDate and our routes define parameters with the same spelling (regardless of case), the Html.ActionLink will extract the data from our view model and use the values in the link html that it renders. What? Why? Well, this is somewhat of a double-edged sword. There may be cases where you want this action to occur...I guess. Actually, I can't think of one off the top of my head right now. Lets use our application and see where this issue becomes a problem.
If we F5 our application we get our home page and our Sales and Traffic links are nice and clean (no date range appended).
If we navigate to the Sales page and then click on our Back Another Month link we will end up at the url that contains our start and end date values. Our date range display will reflect the new range.
Now, if we hover over our main navigation link for Traffic we will notice our current date range is in the url structure. Oh noes!
We have replicated the issue our users have been complaining about. If they go from one report page to a different report the date range they were using for their previous report carries over to the new report. I guess we could call this a "feature enhancement"...but our users don't think so. So how do we solve this? We can add two new routes to our route table that define a route to our reports without any start or end date in the url structure (the url parameter in the MapRoute constructor). The Html.ActionLink code is digging into the route table and looking for matches when it attempts to render the link html. As we know (or if you don't know yet then now you do), routes in the route table are parsed for matches in the order that they are added to the table. So we need to add our desired default routes for our reports ahead of our routes with the date range values. That way we will ensure that the Html.ActionLink method will find a match on those and use them, thus avoiding the auto-set of those parameters in the url. The routes to add are as follows:
Note that we still need to send in our startDate and endDate parameters with our object default values in our MapRoute constructor. If we left those off then our Html.ActionLink call would render out the url with our old school GET parameters attached to the link with a question mark.
If we do another F5 (be aware that you may need to kill off any previous running instances of the VS debug web server before doing so to ensure your route changes have been applied) and we test the same navigation process we can see that our urls are so fresh and so clean:
I am sure that there is a viable reason for having the Html.ActionLink method do this sort of logic (if you know, add a comment). If I come across the reason I will add a comment as well. But, like we learned in the 80's, "Knowing is half the battle". If you come across this issue you are fully prepared to resolve it. Thank you interwebs. Now get back out there and continue the MVC offensive soldier!