Méthodes d'action ambiguë ASP.NET MVC

135

J'ai deux méthodes d'action qui sont contradictoires. Fondamentalement, je veux pouvoir accéder à la même vue en utilisant deux itinéraires différents, soit par l'ID d'un élément, soit par le nom de l'élément et celui de son parent (les éléments peuvent avoir le même nom dans différents parents). Un terme de recherche peut être utilisé pour filtrer la liste.

Par exemple...

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

Voici mes méthodes d'action (il y a aussi Removedes méthodes d'action) ...

// 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) { ... }

Et voici les itinéraires ...

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

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

Je comprends pourquoi l'erreur se produit, car le pageparamètre peut être nul, mais je ne peux pas trouver le meilleur moyen de le résoudre. Ma conception est-elle médiocre pour commencer? J'ai pensé à étendre Method #1la signature de pour inclure les paramètres de recherche et à déplacer la logique Method #2vers une méthode privée qu'ils appelleraient tous les deux, mais je ne pense pas que cela résoudra réellement l'ambiguïté.

Toute aide serait grandement appréciée.


Solution réelle (basée sur la réponse de Levi)

J'ai ajouté la classe suivante ...

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; }
}

Et puis décoré les méthodes d'action ...

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

[RequireRouteValues(new[] { "itemId" })]
public ActionResult Assign(string itemId) { ... }
Jonathan Freeland
la source
3
Merci d'avoir publié la mise en œuvre réelle. Cela aide certainement les personnes ayant des problèmes similaires. Comme je l'ai fait aujourd'hui. :-P
Paulo Santos
4
Incroyable! Suggestion de modification mineure: (imo vraiment utile) 1) params string [] valueNames pour rendre la déclaration d'attribut plus concise et (préférence) 2) remplacer le corps de la méthode IsValidForRequest parreturn ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v));
Benjamin Podszun
2
J'ai eu le même problème de paramètre de chaîne de requête. Si vous avez besoin que ces paramètres soient pris en compte pour l'exigence, remplacez la contains = ...section par quelque chose comme ceci:contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value) || controllerContext.RequestContext.HttpContext.Request.Params.AllKeys.Contains(value);
patridge
3
Remarque d'avertissement à ce sujet: les paramètres requis doivent être envoyés exactement comme ils ont été nommés. Si votre paramètre de méthode d'action est un type complexe rempli en transmettant ses propriétés par nom (et en laissant MVC les masser dans le type complexe), ce système échoue car le nom ne figure pas dans les clés de la chaîne de requête. Par exemple, cela ne fonctionnera pas:, ActionResult DoSomething(Person p)where Persona diverses propriétés simples comme Name, et les requêtes qui lui sont adressées sont faites directement avec les noms de propriétés (par exemple, /dosomething/?name=joe+someone&other=properties).
patridge
4
Si vous utilisez MVC4 à partir de, vous devez utiliser à la controllerContext.HttpContext.Request[value] != nullplace de controllerContext.RequestContext.RouteData.Values.ContainsKey(value); mais un beau travail néanmoins.
Kevin Farrugia

Réponses:

180

MVC ne prend pas en charge la surcharge de méthode basée uniquement sur la signature, donc cela échouera:

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

Cependant, il prend en charge la surcharge de méthode basée sur l'attribut:

[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; }
}

Dans l'exemple ci-dessus, l'attribut dit simplement "cette méthode correspond si la clé xxx était présente dans la demande". Vous pouvez également filtrer en fonction des informations contenues dans l'itinéraire (controllerContext.RequestContext) si cela vous convient le mieux.

Levi
la source
Cela a fini par être exactement ce dont j'avais besoin. Comme vous l'avez suggéré, j'avais besoin d'utiliser controllerContext.RequestContext.
Jonathan Freeland
4
Agréable! Je n'avais pas encore vu l'attribut RequireRequestValue. C'est bon à savoir.
CoderDennis
1
nous pouvons utiliser valueprovider pour obtenir des valeurs de plusieurs sources telles que: controllerContext.Controller.ValueProvider.GetValue (value);
Jone Polvora
Je suis allé après la ...RouteData.Valuesplace, mais cela "fonctionne". Que ce soit un bon modèle ou non est sujet à débat. :)
bambams
1
Ma précédente modification a été rejetée, je vais donc simplement commenter: [AttributeUsage (AttributeTargets.All, AllowMultiple = true)]
Mzn
7

Les paramètres de vos itinéraires {roleId}, {applicationName}et {roleName}ne correspondent pas aux noms de paramètre dans vos méthodes d'action. Je ne sais pas si cela compte, mais il est plus difficile de déterminer quelle est votre intention.

Votre itemId est-il conforme à un modèle qui pourrait être mis en correspondance via regex? Si tel est le cas, vous pouvez ajouter une restriction à votre itinéraire afin que seules les URL qui correspondent au modèle soient identifiées comme contenant un itemId.

Si votre itemId ne contenait que des chiffres, cela fonctionnerait:

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

Modifier: vous pouvez également ajouter une contrainte à l' AssignRemovePrettyitinéraire afin que les deux {parentName}et {itemName}soient obligatoires.

Edit 2: De plus, puisque votre première action est simplement une redirection vers votre 2ème action, vous pouvez supprimer une certaine ambiguïté en renommant la première.

// 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) { ... }

Spécifiez ensuite les noms d'action dans vos itinéraires pour forcer la méthode appropriée à être appelée:

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+" }
                );
CodeurDennis
la source
1
Désolé Dennis, les paramètres correspondent en fait. J'ai résolu la question. Je vais essayer la restriction de regex et vous recontacterai. Merci!
Jonathan Freeland
Votre deuxième montage m'a aidé, mais finalement c'est la suggestion de Levi qui a scellé l'accord. Merci encore!
Jonathan Freeland
3

Récemment, j'ai profité de l'occasion pour améliorer la réponse de @ Levi pour prendre en charge un plus large éventail de scénarios que je devais gérer, tels que: prise en charge de plusieurs paramètres, faire correspondre l'un d'entre eux (au lieu de tous) et même ne correspondre à aucun d'entre eux.

Voici l'attribut que j'utilise maintenant:

/// <summary>
/// Flags an Action Method valid for any incoming request only if all, any or none of the given HTTP parameter(s) are set,
/// enabling the use of multiple Action Methods with the same name (and different signatures) within the same MVC Controller.
/// </summary>
public class RequireParameterAttribute : ActionMethodSelectorAttribute
{
    public RequireParameterAttribute(string parameterName) : this(new[] { parameterName })
    {
    }

    public RequireParameterAttribute(params string[] parameterNames)
    {
        IncludeGET = true;
        IncludePOST = true;
        IncludeCookies = false;
        Mode = MatchMode.All;
    }

    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
    {
        switch (Mode)
        {
            case MatchMode.All:
            default:
                return (
                    (IncludeGET && ParameterNames.All(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p)))
                    || (IncludePOST && ParameterNames.All(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p)))
                    || (IncludeCookies && ParameterNames.All(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p)))
                    );
            case MatchMode.Any:
                return (
                    (IncludeGET && ParameterNames.Any(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p)))
                    || (IncludePOST && ParameterNames.Any(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p)))
                    || (IncludeCookies && ParameterNames.Any(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p)))
                    );
            case MatchMode.None:
                return (
                    (!IncludeGET || !ParameterNames.Any(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p)))
                    && (!IncludePOST || !ParameterNames.Any(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p)))
                    && (!IncludeCookies || !ParameterNames.Any(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p)))
                    );
        }
    }

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

    /// <summary>
    /// Set it to TRUE to include GET (QueryStirng) parameters, FALSE to exclude them:
    /// default is TRUE.
    /// </summary>
    public bool IncludeGET { get; set; }

    /// <summary>
    /// Set it to TRUE to include POST (Form) parameters, FALSE to exclude them:
    /// default is TRUE.
    /// </summary>
    public bool IncludePOST { get; set; }

    /// <summary>
    /// Set it to TRUE to include parameters from Cookies, FALSE to exclude them:
    /// default is FALSE.
    /// </summary>
    public bool IncludeCookies { get; set; }

    /// <summary>
    /// Use MatchMode.All to invalidate the method unless all the given parameters are set (default).
    /// Use MatchMode.Any to invalidate the method unless any of the given parameters is set.
    /// Use MatchMode.None to invalidate the method unless none of the given parameters is set.
    /// </summary>
    public MatchMode Mode { get; set; }

    public enum MatchMode : int
    {
        All,
        Any,
        None
    }
}

Pour plus d'informations et des exemples d'implémentation, consultez ce billet de blog que j'ai écrit sur ce sujet.

Darkseal
la source
Merci, grande amélioration! Mais ParameterNames n'est pas défini dans ctor
nvirth
0
routes.MapRoute("AssignRemove",
                "Items/{parentName}/{itemName}",
                new { controller = "Items", action = "Assign" }
                );

envisagez d'utiliser la bibliothèque de routes de test MVC Contribs pour tester vos routes

"Items/parentName/itemName".Route().ShouldMapTo<Items>(x => x.Assign("parentName", itemName));
Rony
la source