Niejednoznaczne metody akcji ASP.NET MVC

135

Mam dwie sprzeczne metody działania. Zasadniczo chcę mieć możliwość uzyskania tego samego widoku przy użyciu dwóch różnych tras, albo według identyfikatora elementu, albo według nazwy elementu i jego elementu nadrzędnego (elementy mogą mieć tę samą nazwę u różnych elementów nadrzędnych). Do filtrowania listy można użyć wyszukiwanego hasła.

Na przykład...

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

Oto moje metody akcji (są też Removemetody akcji) ...

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

A oto trasy ...

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

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

Rozumiem, dlaczego występuje błąd, ponieważ pageparametr może być pusty, ale nie mogę znaleźć najlepszego sposobu rozwiązania tego problemu. Czy mój projekt jest kiepski? Myślałem o rozszerzeniu Method #1podpisu tak, aby obejmował parametry wyszukiwania i przeniesieniu logiki Method #2do prywatnej metody, którą obaj będą wywoływać, ale nie wierzę, że to faktycznie rozwiąże tę niejednoznaczność.

Każda pomoc byłaby bardzo mile widziana.


Rzeczywiste rozwiązanie (na podstawie odpowiedzi Levi's)

Dodałem następującą klasę ...

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

A potem udekorował metody działania ...

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

[RequireRouteValues(new[] { "itemId" })]
public ActionResult Assign(string itemId) { ... }
Jonathan Freeland
źródło
3
Dziękujemy za przesłanie aktualnej implementacji. Z pewnością pomaga ludziom z podobnymi problemami. Tak jak dzisiaj. :-P
Paulo Santos,
4
Niesamowity! Drobna sugestia zmiany: (imo naprawdę przydatne) 1) params string [] valueNames, aby deklaracja atrybutu była bardziej zwięzła i (preferencja) 2) zamień treść metody IsValidForRequest nareturn ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v));
Benjamin Podszun
2
Miałem ten sam problem z parametrem querystring. Jeśli potrzebujesz tych parametrów uwzględnionych w wymaganiu, zamień contains = ...sekcję na coś takiego:contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value) || controllerContext.RequestContext.HttpContext.Request.Params.AllKeys.Contains(value);
patridge
3
Uwaga z ostrzeżeniem: wymagane parametry muszą być przesłane dokładnie tak, jak podano. Jeśli parametr metody akcji jest typem złożonym wypełnionym przez przekazanie jego właściwości według nazwy (i zezwolenie MVC na masowanie ich do typu złożonego), ten system nie powiedzie się, ponieważ nazwy nie ma w kluczach zapytań. Na przykład to nie zadziała:, ActionResult DoSomething(Person p)gdzie Personma różne proste właściwości, takie jak Name, a żądania do niego są wysyłane bezpośrednio z nazwami właściwości (np /dosomething/?name=joe+someone&other=properties.).
patridge
4
Jeśli używasz MVC4 i nowszych, powinieneś używać controllerContext.HttpContext.Request[value] != nullzamiast controllerContext.RequestContext.RouteData.Values.ContainsKey(value); ale mimo wszystko niezła robota.
Kevin Farrugia,

Odpowiedzi:

180

MVC nie obsługuje przeciążania metod opartego wyłącznie na sygnaturze, więc to się nie powiedzie:

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

Jednak to nie metoda wsparcie przeciążenia na podstawie atrybutu:

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

W powyższym przykładzie atrybut mówi po prostu „ta metoda pasuje, jeśli klucz xxx był obecny w żądaniu”. Możesz również filtrować według informacji zawartych w trasie (controllerContext.RequestContext), jeśli to lepiej pasuje do twoich celów.

Levi
źródło
To było właśnie tym, czego potrzebowałem. Jak zasugerowałeś, potrzebowałem użyć controllerContext.RequestContext.
Jonathan Freeland
4
Miły! Nie widziałem jeszcze atrybutu RequireRequestValue. Dobrze wiedzieć.
CoderDennis
1
możemy użyć valueprovider, aby uzyskać wartości z kilku źródeł, takich jak: controllerContext.Controller.ValueProvider.GetValue (value);
Jone Polvora,
Poszedłem za tym ...RouteData.Valueszamiast, ale to „działa”. To, czy jest to dobry wzór, jest otwarte do dyskusji. :)
bambams
1
Moja poprzednia edycja została odrzucona, więc chcę tylko skomentować: [AttributeUsage (AttributeTargets.All, AllowMultiple = true)]
Mzn
7

Parametry w swoich tras {roleId}, {applicationName}a{roleName} nie pasuje do nazwy parametrów w metodach działania. Nie wiem, czy to ma znaczenie, ale utrudnia ustalenie, jakie masz zamiary.

Czy element itemId jest zgodny ze wzorcem, który można dopasować za pomocą wyrażenia regularnego? Jeśli tak, możesz dodać ograniczenie do swojej trasy, aby tylko adresy URL pasujące do wzorca były identyfikowane jako zawierające elementId.

Jeśli element itemId zawiera tylko cyfry, to zadziała:

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

Edycja: możesz również dodać ograniczenie do AssignRemovePrettytrasy, aby zarówno {parentName}i{itemName} były wymagane.

Edycja 2: Ponadto, ponieważ Twoja pierwsza akcja to po prostu przekierowanie do drugiej akcji, możesz usunąć pewną niejednoznaczność, zmieniając nazwę pierwszej.

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

Następnie określ nazwy akcji w swoich trasach, aby wymusić wywołanie właściwej metody:

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+" }
                );
CoderDennis
źródło
1
Przepraszam Dennis, parametry faktycznie pasują. Naprawiłem pytanie. Wypróbuję ograniczenie regex i wrócę do Ciebie. Dzięki!
Jonathan Freeland
Twoja druga edycja pomogła mi, ale ostatecznie to sugestia Leviego przypieczętowała umowę. Dzięki jeszcze raz!
Jonathan Freeland
3

Niedawno skorzystałem z okazji, aby ulepszyć odpowiedź @ Levi, aby wspierać szerszy zakres scenariuszy, z którymi miałem do czynienia, takich jak: obsługa wielu parametrów, dopasowanie dowolnego z nich (zamiast wszystkich), a nawet dopasowanie żadnego z nich.

Oto atrybut, którego teraz używam:

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

Więcej informacji i przykłady implementacji znajdziesz w tym wpisie na blogu, który napisałem na ten temat.

Darkseal
źródło
Dzięki, wielka poprawa! Ale ParameterNames nie jest ustawiony w ctor
nvirth
0
routes.MapRoute("AssignRemove",
                "Items/{parentName}/{itemName}",
                new { controller = "Items", action = "Assign" }
                );

rozważ użycie biblioteki tras testowych MVC Contribs do przetestowania tras

"Items/parentName/itemName".Route().ShouldMapTo<Items>(x => x.Assign("parentName", itemName));
Rony
źródło