web-dev-qa-db-fra.com

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

J'ai deux méthodes d'action qui sont en conflit. 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 pour 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 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 le paramètre page peut être nul, mais je ne peux pas trouver le meilleur moyen de la résoudre. Est-ce que ma conception est mauvaise pour commencer? J'ai envisagé d'étendre la signature de Method #1 Pour inclure les paramètres de recherche et de déplacer la logique de Method #2 Vers une méthode privée qu'ils appelleraient tous les deux, mais je ne crois pas que ce sera réellement le cas. résoudre l'ambiguïté.

Toute aide serait grandement appréciée.


Solution actuelle (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) { ... }
132
Jonathan Freeland

MVC ne prend pas en charge la surcharge de méthodes basée uniquement sur la signature. Cela échouera:

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

Cependant, il ne supporte 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 indique simplement "cette méthode correspond si la clé xxx était présente dans la demande". Vous pouvez également filtrer par informations contenues dans la route (controllerContext.RequestContext) si cela vous convient mieux.

179
Levi

Une autre approche consiste à renommer l'une des méthodes afin d'éviter tout 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

7
RickAndMSFT

Les paramètres de vos routes {roleId}, {applicationName} et {roleName} ne correspond 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 associé via regex? Si tel est le cas, vous pouvez ajouter une restriction à votre itinéraire afin que seules les URL correspondant au modèle soient identifiées comme contenant un itemId.

Si votre itemId ne contenait que des chiffres, alors ceci fonctionnerait:

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

Edit: Vous pouvez également ajouter une contrainte à la route AssignRemovePretty afin que les deux {parentName} et {itemName} sont requises.

Edit 2: De plus, puisque votre première action vient de rediriger vers votre 2ème action, vous pouvez supprimer une 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) { ... }

Ensuite, spécifiez les noms d’action dans vos itinéraires 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+" }
                );
7
CoderDennis

Récemment, j’ai saisi la chance d’améliorer la réponse de @ Levi pour prendre en charge un plus grand nombre de scénarios, tels que: prise en charge de plusieurs paramètres, correspond à n’importe lequel (au lieu de tous) et même à 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 cet article de blog que j’ai écrit sur ce sujet.

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

envisagez d'utiliser la bibliothèque d'itinéraires de test de MVC Contribs pour tester vos itinéraires

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