views:

228

answers:

1

I'm implementing a site which has a search that can be executed via Controller methods that are accessed from two different routes. One hangs off the default route (using either Post data or query string values for the search parameters) and one is an SEO optimisation url which takes a location and phrase via two route values. The second route looks like this:

routes.MapRoute("SEOSearch", "Search/{seoLocation}/{seoSearchString}",
  new { controller = "Search", 
        action = "SEOResults", 
        seoLocation = (string)null, 
        seoSearchString = (string)null });

You might be wondering why I have two different routes - it's because the search offers many other parameters than just location and phrase - but I want the SEO'd urls to include those two in the path, rather than using the query string.

As I say, the first route is the default /controller/action/id route, and the correct controller/action for that is "Search" and "Index".

In the end, both actions execute the same search operation in the controller, and both will render their results using the Index view, since their result models are identical.

On the index view I use a partial view for the search terms, another partial for the results and another for the paging.

The problem I'm having is getting the paging control to render the correct link to launch the current search for the next page using the same URL format as the current request.

What I want

So, assuming you've navigated to /Search?Location=[location]&Phrase=[phrase], I want Page 2's link generated by the pager to be /Search?Location=[location]&Phrase=[phrase]&Page=2.

However, if you've launched the search with /Search/[location]/[phrase], I want Page 2's link to be /Search/[location]/[phrase]?Page=2.

What I've got

The closest I've got is this:

<%=  Html.RouteLink("Previous Page", 
       RouteHelpers.Combine(ViewContext.RouteData.Values, 
       new RouteValueDictionary() { { "Page", Model.Results.PageNo + 1}})) %>

Where RouteHelpers.Combine is an extension that I've written that takes two objects and merges them into one RouteValueDictionary. By taking the RouteValues for the current request, I'm able to persist the current Controller and Action name (without having to know what they are) - however this misses some important information from ModelState - i.e. any extra search parameters that were provided - i.e. it works if the current Url is /Search/London/Widgets, but if it's /Search/London/Widgets?PageSize=50 then the PageSize parameter doesn't get persisted into the outgoing link.

Even worse, if it's a non-SEO'd url - i.e. /Search?Location=London&Phrase=Widgets, the outgoing url simply becomes /Search?Page=x.

My search parameters are read from the request into a model type, that is then fed to both the host page, and to the pager itself, so in theory I could simply always generate them from that - but I end up with all the parameters in the url, even when they are default values (therefore they do not need to be supplied) - so the url looks ugly.

How do I achieve what I want!? I'm feeling like I know nothing about MVC all of sudden!

+2  A: 

You don't have to use your RouteHelper to combine route values. Those values are combined with your anonymous object automatically when you use ActionLink instead. All existing values will be overwritten and new ones added. This call does that for you:

RouteValueDictionary values = RouteValuesHelpers.MergeRouteValues(
    actionName,
    controllerName,
    requestContext.RouteData.Values,
    routeValues,
    includeImplicitMvcValues); // true for ActionLink; false for RouteLink

Where routeValues are your values from anonymous object. They get merged to requestContext.RouteData.Values.

So you can still use:

Html.ActionLink(
    "whatever",
    this.ViewContext.RouteData.Values["action"],
    this.ViewContext.RouteData.Values["controller"],
    new { Page = /* whatever needs to be */ },
    null)

Null at the end is mandatory, so it doesn't confuse anonymous object with HTML attributes.

But you do have routing problems unless you also put a route constraint on your default route to only use certain actions, otherwise your /Search/SeoLocation/SeoSearchString will still be handled by your default route, by SeoLocation becoming your action and SeoSearchString the id.

Anyway. If your code would be correct all those values should be inside your route values dictionary and propagated to your links as expected.

Robert Koritnik
I spotted the constraints problem from another web post (possibly by Scott Gu) which solved the problem of the two routes getting confused.My current solution is not ideal - I derived a new Route Type that added the Route Name (that was used to add the route to the RouteTable-can't believe you can't get a route's original name back out!) - and then I made a link generation extension method for 'the current page' which cracks that back out of the current route and feeds it into the RouteLink overload.I will investigate this solution when the next deadline passes; thanks!
Andras Zoltan