136 votes

ASP.NET MVC : méthodes d'action ambiguës

J'ai deux méthodes d'action qui sont en conflit. En fait, je veux pouvoir accéder à la même vue en utilisant deux routes différentes, 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 avec des parents différents). 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 Remove 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, puisque la page peut être nul, mais je n'arrive pas à trouver la meilleure façon de résoudre ce problème. Ma conception est-elle mauvaise pour commencer ? J'ai pensé à étendre Method #1 afin d'inclure les paramètres de recherche et de déplacer la logique dans la signature de l'utilisateur. Method #2 vers 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é 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 ensuite 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) { ... }

4 votes

Incroyable ! Suggestion de changement mineur : (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 par return ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKe‌​y(v));

0 votes

Salut Jon, je pense que je n'ai pas compris quelque chose, car où sont les paramètres de requête dans RouteData ?

2 votes

J'ai eu le même problème avec le paramètre querystring. Si vous avez besoin que ces paramètres soient pris en compte pour l'exigence, remplacez le paramètre contains = ... pour quelque chose comme ça : contains = controllerContext.RequestContext.RouteData.Values.ContainsKe‌​y(value) || controllerContext.RequestContext.HttpContext.Request.Params.‌​AllKeys.Contains(val‌​ue);

182voto

Levi Points 22222

MVC ne prend pas en charge la surcharge des méthodes basée uniquement sur la signature, ce qui entraînera un échec :

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

Cependant, il fait prise en charge de la surcharge des méthodes en fonction des attributs :

[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ésent dans la demande". Vous pouvez également filtrer par les informations contenues dans la route (controllerContext.RequestContext) si cela convient mieux à vos besoins.

4 votes

Joli ! Je n'avais pas encore vu l'attribut RequireRequestValue. C'est un bon élément à connaître.

1 votes

Nous pouvons utiliser valueprovider pour obtenir des valeurs de plusieurs sources comme : controllerContext.Controller.ValueProvider.GetValue(value) ;

0 votes

Je suis allé après le ...RouteData.Values à la place, mais cela "fonctionne". La question de savoir s'il s'agit ou non d'un bon modèle est ouverte au débat :)

8voto

CoderDennis Points 7170

Les paramètres de vos itinéraires {roleId} , {applicationName} y {roleName} ne correspondent pas aux noms des paramètres dans vos méthodes d'action. Je ne sais pas si c'est important, mais cela rend plus difficile la compréhension de votre intention.

Vos identifiants d'articles correspondent-ils à un modèle qui pourrait être mis en correspondance avec une expression rationnelle ? Si c'est le cas, vous pouvez ajouter une restriction à votre route 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, alors cela fonctionnerait :

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

Edit : Vous pourriez également ajouter une contrainte à l'option AssignRemovePretty de sorte que les deux {parentName} y {itemName} sont nécessaires.

Edit 2 : Aussi, puisque votre première action ne fait que rediriger vers votre 2ème action, vous pourriez lever une certaine ambiguïté en renommant la première action.

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

Ensuite, spécifiez les noms des actions dans vos routes pour forcer l'appel de la méthode approprié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+" }
                );

7voto

RickAnd - MSFT Points 3741

Une autre approche consiste à renommer l'une des méthodes afin qu'il n'y ait pas de conflit. Par exemple

// GET: /Movies/Delete/5
public ActionResult Delete(int id = 0)

// POST: /Movies/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id = 0)

Voir http://www.asp.net/mvc/tutorials/getting-started-with-mvc3-part9-cs

3voto

Darkseal Points 18

Récemment, j'ai saisi l'occasion d'améliorer la réponse de @Levi afin de prendre en charge un plus grand nombre de scénarios auxquels j'ai été confronté, tels que : la prise en charge de plusieurs paramètres, la correspondance avec n'importe lequel d'entre eux (au lieu de tous) et même la correspondance avec 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 de mise en œuvre, consultez le site suivant cet article de blog que j'ai écrit sur ce sujet.

0voto

Rony Points 6340
routes.MapRoute("AssignRemove",
                "Items/{parentName}/{itemName}",
                new { controller = "Items", action = "Assign" }
                );

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

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X