Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is this route parameter tacked onto the querystring?

I have an ASP.NET MVC 3 application that records a user's pedometer entries. A user can view all most recent pedometer entries by visiting /Pedometer and can filter by year, year/month, or year/month/date by visiting URLs like /Pedometer/2011, /Pedometer/2011/08 and /Pedometer/2011/08/15, respectively.

I've created two mapped routes in Global.asax. The first route, shown below, is what allows the various URL patterns for filtering by date. The second route (not shown) is the default ASP.NET MVC route.

routes.MapRoute(
    "PedometerEntries", // Route name
    "Pedometer/{year}/{month}/{day}", // URL with parameters
    new
    {
        controller = "Pedometer",
        action = "Index",
        year = UrlParameter.Optional,
        month = UrlParameter.Optional,
        day = UrlParameter.Optional
    }, // Parameter defaults
    new
    {
        year = @"\d{4}",
        month = @"([012]?\d{1})?",
        day = @"(([1-9])|([12][0-9])|(3[0-1]))?"
    } // Parameter constraints
);

Here's my question. I have a view where I want to create a link of the form: currentUrl?format=csv, which will let the user download the pedometer entries for the requested URL in a CSV format. So if a user is visiting /Pedometer, the download link would be to /Pedometer?format=csv. If the user is visiting /Pedometer/2011/08 the download link would be to /Pedometer/2011/08?format=csv.

To create such a link I added a custom Html Helper named DownloadToExcel with the following code:

public static MvcHtmlString DownloadToExcel(this HtmlHelper helper, string linkText)
{
    RouteValueDictionary routeValues = helper.ViewContext.RouteData.Values;

    // Add the format parameter to the route data collection, if needed
    if (!routeValues.ContainsKey("format"))
        routeValues.Add("format", "csv");

    return helper.ActionLink(linkText,                          // Link text
                                routeValues["action"].ToString(),  // Action
                                routeValues);                      // Route values
}

When I add the @Html.DownloadToExcel() markup in my view, it generates a link, but here's the rub - when the user visits the recent entries or the entries filtered by year/month or year/month/date, it works as expected, but not when the user visits the year filter URL.

The following list shows the URL the user visits and the corresponding URL generated by the custom Html Helper:

  • Visiting: /Pedometer - Download link: /Pedometer?format=csv
  • Visiting: /Pedometer/2011 - Download link: /Pedometer?year=2011&format=csv
  • Visiting: /Pedometer/2011/08 - Download link: /Pedometer/2011/08?format=csv
  • Visiting: /Pedometer/2011/08/15 - Download link: /Pedometer/2011/08/15?format=csv

Why is it when visiting /Pedometer/2011 the download link is /Pedometer?year=2011&format=csv and not /Pedometer/2011?format=csv? And why does it not work for that one case but works as expected for the year/month and year/month/date cases?

Thanks

like image 804
Scott Mitchell Avatar asked Aug 16 '11 22:08

Scott Mitchell


2 Answers

This problem is most likely caused by this bug described by Phil Haack on his blog. There's a regression bug introduced in ASP.NET MVC 3 when you have two consecutive optional URL parameters.

like image 65
George Stocker Avatar answered Nov 16 '22 16:11

George Stocker


I created a small MVC 3 application with the code you provided and got exactly the same behavior as described.

If I go to http://localhost:51181/pedometer/2011 the generated link would be http://localhost:51181/Pedometer?year=2011&format=csv.

But if I explicitly entered the name of the action (Index) it would render correctly.

Visiting http://localhost:51181/pedometer/index/2011 will generate the following link:

http://localhost:51181/pedometer/index/2011?format=csv

It seems the correct route is not always used by the HtmlHelper extension method.

If I add the following route just below your custom route, but before the default MVC route it works OK.

routes.MapRoute(
    "PedometerDefault",
    "Pedometer/{year}",
    new { controller = "Pedometer", action = "Index", 
          year = UrlParameter.Optional }
);
like image 4
Christophe Geers Avatar answered Nov 16 '22 15:11

Christophe Geers