views:

1195

answers:

2

Hi,

I have a question that has pretty much been asked here:

http://stackoverflow.com/questions/780643/asp-net-mvc-html-actionlink-keeping-route-value-i-dont-want

However, the final solution is a kludge, pure and simple and I really would like to understand why this happens, if someone can please explain it to me?

For completeness, it is possible to recreate the scenario very easily:

  • Create a new MVC web app.
  • Run it up.
  • Visit the About tab Modify the URL to read /Home/About/Flib - This obviously takes you to the action with an id of 'Flib' which we don't care about.

Notice that the top menu link to About now actually links to /Home/About/Flib - this is wrong as far as I can see, as I now have absolutely no way of using site links to get back to /Home/About

I really don't understand why I should be forced to modify all of my Html.ActionLinks to include new { id = string.Empty } for the routevalues and null for the htmlAttribs. This seems especially out of whack because I already specify id = 0 as part of the route itself.

Hopefully I'm missing a trick here.

+8  A: 

When you look into the source code for the action link you find that

<%= Html.ActionLink("LinkText", "Action", "Controller"); %>

will match

public static MvcHtmlString ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName) {
        return ActionLink(htmlHelper, linkText, actionName, controllerName, new RouteValueDictionary(), new RouteValueDictionary());
    }

Now so far this looks good since it is creating a new route value dictionary so it is not passing along the values in your current context to be added to the new link, which is what will happen.

However, further down in the code where the url is being generated:

public static string GenerateUrl(string routeName, string actionName, string controllerName, RouteValueDictionary routeValues, RouteCollection routeCollection, RequestContext requestContext, bool includeImplicitMvcValues) {
        if (routeCollection == null) {
            throw new ArgumentNullException("routeCollection");
        }

        if (requestContext == null) {
            throw new ArgumentNullException("requestContext");
        }

        RouteValueDictionary mergedRouteValues = RouteValuesHelpers.MergeRouteValues(actionName, controllerName, requestContext.RouteData.Values, routeValues, includeImplicitMvcValues);

        VirtualPathData vpd = routeCollection.GetVirtualPathForArea(requestContext, routeName, mergedRouteValues);
        if (vpd == null) {
            return null;
        }

        string modifiedUrl = PathHelpers.GenerateClientUrl(requestContext.HttpContext, vpd.VirtualPath);
        return modifiedUrl;
    }

you can see the requestContext being referenced which has access to the routeData and routeCollections, which will contain the id data. When creating the VirtualPathForArea, the following line is where the id value appears in your url:

internal static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, string name, RouteValueDictionary values, out bool usingAreas) {
        if (routes == null) {
            throw new ArgumentNullException("routes");
        }

        if (!String.IsNullOrEmpty(name)) {
            // the route name is a stronger qualifier than the area name, so just pipe it through
            usingAreas = false;
            return routes.GetVirtualPath(requestContext, name, values);
        }

        string targetArea = null;
        if (values != null) {
            object targetAreaRawValue;
            if (values.TryGetValue("area", out targetAreaRawValue)) {
                targetArea = targetAreaRawValue as string;
            }
            else {
                // set target area to current area
                if (requestContext != null) {
                    targetArea = AreaHelpers.GetAreaName(requestContext.RouteData);
                }
            }
        }

        // need to apply a correction to the RVD if areas are in use
        RouteValueDictionary correctedValues = values;
        RouteCollection filteredRoutes = FilterRouteCollectionByArea(routes, targetArea, out usingAreas);
        if (usingAreas) {
            correctedValues = new RouteValueDictionary(values);
            correctedValues.Remove("area");
        }

        VirtualPathData vpd = filteredRoutes.GetVirtualPath(requestContext, correctedValues);
        return vpd;
    }

The line:

VirtualPathData vpd = filteredRoutes.GetVirtualPath(requestContext, correctedValues);

takes the virtual path (which is just the route) and returns it. So the virtual path would be /Home/About/Flib

Then when that virtual path is returned the following line uses it to set the client url for the action link:

 string modifiedUrl = PathHelpers.GenerateClientUrl(requestContext.HttpContext, vpd.VirtualPath);

So pretty much it looks like the actionlink is set using the virtual path for the area, which is just the route that was matched, which happens to be the default route in this case.

amurra
+1  A: 

If I understand correctly, it sounds like you need to register a second "Action Only" route and use Html.RouteLink. First register a route like this in you application start up:

routes.MapRoute("ActionOnly", "{controller}/{action}", new { controller = "Home", action = "Index" } );

Then instead of ActionLink to create those links use:

Html.RouteLink("About","ActionOnly")
Brian