views:

1581

answers:

3

I have two action methods that are conflicting. Basically, I want to be able to get to the same view using two different routes, either by an item's ID or by the item's name and its parent's (items can have the same name across different parents). A search term can be used to filter the list.

For example...

Items/{action}/ParentName/ItemName
Items/{action}/1234-4321-1234-4321

Here are my action methods (there are also Remove action methods)...

// Method #1
public ActionResult Assign(string parentName, string itemName) { 
    // Logic to retrieve item's ID here...
    string itemId = ...;
    return RedirectToAction("Assign", "Items", new { itemId });
}

// Method #2
public ActionResult Assign(string itemId, string searchTerm, int? page) { ... }

And here are the routes...

routes.MapRoute("AssignRemove",
                "Items/{action}/{itemId}",
                new { controller = "Items" }
                );

routes.MapRoute("AssignRemovePretty",
                "Items/{action}/{parentName}/{itemName}",
                new { controller = "Items" }
                );

I understand why the error is occurring, since the page parameter can be null, but I can't figure out the best way to resolve it. Is my design poor to begin with? I've thought about extending Method #1's signature to include the search parameters and moving the logic in Method #2 out to a private method they would both call, but I don't believe that will actually resolve the ambiguity.

Any help would be greatly appreciated.


Actual Solution (based on Levi's answer)

I added the following class...

public class RequireRouteValuesAttribute : ActionMethodSelectorAttribute {
    public RequireRouteValuesAttribute(string[] valueNames) {
        ValueNames = valueNames;
    }

    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
        bool contains = false;
        foreach (var value in ValueNames) {
            contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value);
            if (!contains) break;
        }
        return contains;
    }

    public string[] ValueNames { get; private set; }
}

And then decorated the action methods...

[RequireRouteValues(new[] { "parentName", "itemName" })]
public ActionResult Assign(string parentName, string itemName) { ... }

[RequireRouteValues(new[] { "itemId" })]
public ActionResult Assign(string itemId) { ... }
+4  A: 

The parameters in your routes {roleId}, {applicationName} and {roleName} don't match the parameter names in your action methods. I don't know if that matters, but it makes it tougher to figure out what your intention is.

Do your itemId's conform to a pattern that could be matched via regex? If so, then you can add a restraint to your route so that only url's that match the pattern are identified as containing an itemId.

If your itemId only contained digits, then this would work:

routes.MapRoute("AssignRemove",
                "Items/{action}/{itemId}",
                new { controller = "Items" },
                new { itemId = "\d+" }
                );

Edit: You could also add a constraint to the AssignRemovePretty route so that both {parentName} and {itemName} are required.

Edit 2: Also, since your first action is just redirecting to your 2nd action, you could remove some ambiguity by renaming the first one.

// Method #1
public ActionResult AssignRemovePretty(string parentName, string itemName) { 
    // Logic to retrieve item's ID here...
    string itemId = ...;
    return RedirectToAction("Assign", itemId);
}

// Method #2
public ActionResult Assign(string itemId, string searchTerm, int? page) { ... }

Then specify the Action names in your routes to force the proper method to be called:

routes.MapRoute("AssignRemove",
                "Items/Assign/{itemId}",
                new { controller = "Items", action = "Assign" },
                new { itemId = "\d+" }
                );

routes.MapRoute("AssignRemovePretty",
                "Items/Assign/{parentName}/{itemName}",
                new { controller = "Items", action = "AssignRemovePretty" },
                new { parentName = "\w+", itemName = "\w+" }
                );
Dennis Palmer
Sorry Dennis, the parameters do actually match. I've fixed the question. I will try out the regex restraint and get back to you. Thanks!
Jon Freeland
Your second edit helped me out, but ultimately it was Levi's suggestion that sealed the deal. Thanks again!
Jon Freeland
A: 
routes.MapRoute("AssignRemove",
                "Items/{parentName}/{itemName}",
                new { controller = "Items", action = "Assign" }
                );

consider using MVC Contribs test routes library to test your routes

"Items/parentName/itemName".Route().ShouldMapTo<Items>(x => x.Assign("parentName", itemName));
Rony
+22  A: 

MVC doesn't support method overloading based solely on signature, so this will fail:

public ActionResult MyMethod(int someInt) { /* ... */ }
public ActionResult MyMethod(string someString) { /* ... */ }

However, it does support method overloading based on attribute:

[RequireRequestValue("someInt")]
public ActionResult MyMethod(int someInt) { /* ... */ }

[RequireRequestValue("someString")]
public ActionResult MyMethod(string someString) { /* ... */ }

public class RequireRequestValueAttribute : ActionMethodSelectorAttribute {
    public RequireRequestValueAttribute(string valueName) {
        ValueName = valueName;
    }
    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
        return (controllerContext.HttpContext.Request[ValueName] != null);
    }
    public string ValueName { get; private set; }
}

In the above example, the attribute simply says "this method matches if the key xxx was present in the request." You can also filter by information contained within the route (controllerContext.RequestContext) if that better suits your purposes.

Levi
This ended up being just what I needed. As you suggested, I needed to use controllerContext.RequestContext.
Jon Freeland
Nice! I hadn't seen the RequireRequestValue attribute yet. That's a good one to know.
Dennis Palmer