5

I have following two routes registered in my global.asax file

routes.MapRoute(
    "strict",
    "{controller}.mvc/{docid}/{action}/{id}",
    new { action = "Index", id = "", docid = "" },
    new { docid = @"\d+"}
);
routes.MapRoute(
    "default",
    "{controller}.mvc/{action}/{id}",
    new { action = "Index", id = "" },
    new { docConstraint = new DocumentConstraint() }
);

and I have a static "dashboard" link in my tabstrip and some other links that are constructed from values in db here is the code

 <ul id="globalnav" class = "t-reset t-tabstrip-items">
     <li class="bar" id = "dashboard">
         <%=Html.ActionLink("dash.board", "Index", pck.Controller,  new{docid =string.Empty,id = pck.PkgID }, new { @class = "here" })%>
     </li>
     <%  
         foreach (var md in pck.sysModules)
         {
     %>
     <li class="<%=liClass%>">
         <%=Html.ActionLink(md.ModuleName, md.ActionName, pck.Controller, new { docid = md.DocumentID}, new { @class = cls })%>
     </li>
     <%
         }
     %>
 </ul>

Now my launching address is localhost/oa.mvc/index/11 clearly matching the 2nd route. But when I visit any page that has mapped to first route and then come back to dash.board link it shows me localhost/oa.mvc/7/index/11 where 7 is docid and picked from previous Url.

I understand that my action method is after docid and changing it would not clear the docid.

My question here is, can I remove docid in this scenario without changing the route?

6 Answers 6

4

I have the same "not clearing out" value problem...

I've stepped into source code and I don't understand the reason for being of segment commented as : // Add all current values that aren't in the URL at all

@ System\Web\Routing\ParsedRoute.cs, public BoundUrl Bind(RouteValueDictionary currentValues, RouteValueDictionary values, RouteValueDictionary defaultValues, RouteValueDictionary constraints) method from line 91 to line 100

While the clearing process is correctly handled in method preceding steps, this code "reinjects" the undesired parameter into acceptedValues dictionary!?

Sign up to request clarification or add additional context in comments.

1 Comment

thanks it was a great insight. how would u suggest clearing these values
4

My routing is defined this way:

routes.MapRoute(
    "Planning",
    "Plans/{plan}/{controller}/{action}/{identifier}",
    new { controller = "General", action = "Planning", identifier = UrlParameter.Optional },
    new { plan = @"^\d+$" }
);

// default application route
routes.MapRoute(
    "Default",
    "{controller}/{action}/{identifier}",
    new {
        controller = "General",
        action = "Summary",
        identifier = UrlParameter.Optional,
        plan = string.Empty // mind this default !!!
    }
);

This is very similar to what you're using. But mind my default route where I define defaults. Even though my default route doesn't define plan route value I still set it to string.Empty. So whenever I use Html.ActionLink() or Url.Action() and I want plan to be removed from the URL I call it the usual way:

Url.Action("Action", "Controller", new { plan = string.Empty });

And plan is not included in the URL query string any more. Try it out yourself it may work as well.

1 Comment

When I do this I just end up with the same value I "empty out" being set as a GET parameter instead :(
2

Muhammad, I suggest something like this : (written 5 mn ago, not tested in production)

public static class MyHtmlHelperExtensions {

    public static MvcHtmlString FixActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, object routeValues, object htmlAttributes) {
        var linkRvd = new RouteValueDictionary(routeValues);
        var contextRvd = htmlHelper.ViewContext.RouteData.Values;
        var contextRemovedRvd = new RouteValueDictionary();

        // remove clearing route values from current context
        foreach (var rv in linkRvd) {
            if (string.IsNullOrEmpty((string)rv.Value) && contextRvd.ContainsKey(rv.Key)) {
                contextRemovedRvd.Add(rv.Key, contextRvd[rv.Key]);
                contextRvd.Remove(rv.Key);
            }
        }

        // call ActionLink with modified context
        var htmlString = htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes);

        // restore context
        foreach (var rv in contextRemovedRvd) {
            contextRvd.Add(rv.Key, rv.Value);
        }

        return htmlString;
    }
}

Comments

2

This is such a frustrating problem and I would venture to say that it is even a bug in ASP.Net MVC. Luckily it's an easy fix using ActionFilters. If you are using MVC3 then I would just put this as a global attribute to clear out ambient values. I made this attribute discriminatory, but you can change it to clear all attributes.

The assumption here is that by the time the Result is executing (your view most likely), you have already explicitly specified all your ActionLinks and Form Actions. Thus this will execute before they (the links) are evaluated, giving you a new foundation to generate them.

public class ClearAmbientRouteValuesAttribute : ActionFilterAttribute 
{
    private readonly string[] _keys;

    public ClearAmbientRouteValuesAttribute(params string [] keys)
    {
        if (keys == null)
            _keys = new string[0];

        _keys = keys;
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        foreach (var key in _keys) {
            // Why are you sticking around!!!
            filterContext.RequestContext.RouteData.Values.Remove(key);        
        }
    }
}

// Inside your Global.asax
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());
    filters.Add(new ClearAmbientRouteValuesAttribute("format"));
}

Hope this helps someone, cause it sure helped me. Thanks for asking this question.

5 Comments

-1 This means that all the links you wish to use this ambient data for (the common case) have to be generated in the controller, bringing what is arguably view logic back into the controller. Not an approach I'd recommend.
actually that's not true, the action filter here let's you specify which route values you want to get rid of, it doesn't just blow all of them away.
I didn't mean to imply that it removed all of them: my point is that it's rare that an ambient value won't be required in a view at all. The most common case is that a small sub-set of links won't need the values. If some links still need the values that are removed, you need to generate the link in the controller, which isn't great. Of course, the values could be provided on the model, but then you're just transferring effort from one place to another, not a net benefit.
@JT. Even if this magic is useful in many cases, surely the case where you don't want magic should be trivial to handle, and any implementation where it is hard to avoid misdirected magic to be bad? I would think that the ActionLink method should have an option to disregard any ambient routevalues (that might have no relevance whatsoever), or that it's disabled and that you just pass on the values you need from the controller, as you would expect to do without having magic.
@Alex good point, and you can write extension methods to ActionLink that will do this for you. In my experience, however, these ambient values help you far more often than they hurt, so opting out is better.
0

In this particular scenario I have two recommendations:

  1. Use named routes. The first parameter to the MapRoute method is a name. To generate links use Html.RouteLink() (and other similar APIs). This way you'll always choose the exact route that you want and never have to wonder what gets chosen.

  2. If you still want to use Html.ActionLink() then explicitly set docid="" to clear out its value.

2 Comments

@Elion yes i want to use html.actionlink() and i m setting docid = "" but this is not clearing out its value. it rather appends docid into query string. i m using .NET 4 mvc2 RTM thanks
Same problem as @MuhammadAdeelZahid :( Anyone found a solution?
0

Here's how I solved my problem, it may take a little adapting to get it to work, but I felt like I could get what I needed and just use routing more or less normally:

Excerpted from Apress Pro ASP.Net.MVC 3 Framework:

  • A value must be available for every segment variable defined in the URL pattern. To find values for each segment variable, the routing system looks first at the values we have provided (using the properties of anonymous type), then the variable values for the current request, and finally at the default values defined in the route. (We return to the second source of these values later in this chapter.)

  • None of the values we provided for the segment variables may disagree with the default-only variables defined in the route. These are variables for which default values have been provided, but which do not occur in the URL pattern. For example, in this route definition, myVar is a default-only variable:

    routes.MapRoute("MyRoute", "{controller}/{action}", new { myVar = "true" });

    For this route to be a match, we must take care to not supply a value for myVar or to make sure that the value we do supply matches the default value.

  • The values for all of the segment variables must satisfy the route constraints. See the “Constraining Routes” section earlier in the chapter for examples of different kinds of constraints.

Basically I used the rule about a route not matching if it doesn't define a segment, but has a default variable used to give me a little more control over whether a route was chosen for outbound routing or not.

Here's my fixed routes, notice how I specify a value for category that would never be valid and don't specify a segment for category. This means that route will be skipped if I have a category, but will use it if I only have a page:

routes.MapRoute(null, "receptionists/faq/{page}", new { controller = "Receptionist", action = "Faq", page = 1, category = (Object)null }, new { page = @"^\d+$" });

routes.MapRoute(null, "receptionists/faq/{category}/{page}", new { controller = "Receptionist", action = "Faq", page = 1 }, new { category = @"^\D+$", page = @"^\d+$" });

For Category Links

@Html.ActionLink("All", "Faq", new { page = 1 })

@foreach (var category in Model.Categories)
{
@Html.ActionLink(category.DisplayName, "faq", new { category = category.DisplayName.ToLower(), page = 1 })
}

For Page Links

@for (var p = 1; p <= Model.TotalPages; p++)
{
@Html.ActionLink(p.ToString(), "Faq", new { page = p, category = Model.CurrentCategory})
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.