Mehrdeutige ASP.NET MVC-Aktionsmethoden

135

Ich habe zwei widersprüchliche Aktionsmethoden. Grundsätzlich möchte ich in der Lage sein, über zwei verschiedene Routen zur gleichen Ansicht zu gelangen, entweder anhand der ID eines Elements oder anhand des Namens des Elements und seiner übergeordneten Elemente (Elemente können über verschiedene übergeordnete Elemente hinweg denselben Namen haben). Ein Suchbegriff kann verwendet werden, um die Liste zu filtern.

Beispielsweise...

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

Hier sind meine Aktionsmethoden (es gibt auch RemoveAktionsmethoden) ...

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

Und hier sind die Routen ...

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

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

Ich verstehe, warum der Fehler auftritt, da der pageParameter null sein kann, aber ich kann nicht herausfinden, wie ich ihn am besten beheben kann. Ist mein Design anfangs schlecht? Ich habe darüber nachgedacht, Method #1die Signatur um die Suchparameter zu erweitern und die Logik Method #2auf eine private Methode zu verschieben, die beide aufrufen würden, aber ich glaube nicht, dass dies die Mehrdeutigkeit tatsächlich auflösen wird.

Jede Hilfe wäre sehr dankbar.


Tatsächliche Lösung (basierend auf Levis Antwort)

Ich habe die folgende Klasse hinzugefügt ...

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

Und dann die Aktionsmethoden dekoriert ...

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

[RequireRouteValues(new[] { "itemId" })]
public ActionResult Assign(string itemId) { ... }
Jonathan Freeland
quelle
3
Vielen Dank, dass Sie die eigentliche Implementierung veröffentlicht haben. Es hilft sicher Menschen mit ähnlichen Problemen. Wie ich es heute getan habe. :-P
Paulo Santos
4
Tolle! Kleiner Änderungsvorschlag: (imo wirklich nützlich) 1) params string [] valueNames, um die Attributdeklaration präziser zu gestalten, und (Präferenz) 2) Ersetzen Sie den IsValidForRequest-Methodenkörper durchreturn ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v));
Benjamin Podszun
2
Ich hatte das gleiche Problem mit den Querystring-Parametern. Wenn Sie diese Parameter für die Anforderung berücksichtigen müssen, tauschen Sie den contains = ...Abschnitt gegen contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value) || controllerContext.RequestContext.HttpContext.Request.Params.AllKeys.Contains(value);
Folgendes aus
3
Hinweis zur Warnung: Die erforderlichen Parameter müssen genau wie angegeben gesendet werden. Wenn Ihr Aktionsmethodenparameter ein komplexer Typ ist, der durch Übergabe seiner Eigenschaften nach Namen (und durch MVC-Massage in den komplexen Typ) gefüllt wird, schlägt dieses System fehl, da der Name nicht in den Querystring-Schlüsseln enthalten ist. Zum Beispiel wird dies nicht funktionieren:, ActionResult DoSomething(Person p)wo Personverschiedene einfache Eigenschaften wie Nameund Anfragen dazu mit Eigenschaftsnamen direkt (zB /dosomething/?name=joe+someone&other=properties) gestellt werden.
Patridge
4
Wenn Sie MVC4 verwenden, sollten Sie controllerContext.HttpContext.Request[value] != nullanstelle von verwenden controllerContext.RequestContext.RouteData.Values.ContainsKey(value). aber trotzdem ein schönes Stück Arbeit.
Kevin Farrugia

Antworten:

180

MVC unterstützt keine Methodenüberladung, die ausschließlich auf der Signatur basiert. Dies schlägt daher fehl:

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

Aber es tut Unterstützungsverfahren Überlastung basierend auf 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; }
}

Im obigen Beispiel sagt das Attribut einfach "Diese Methode stimmt überein, wenn der Schlüssel xxx in der Anforderung vorhanden war." Sie können auch nach Informationen filtern, die in der Route enthalten sind (controllerContext.RequestContext), wenn dies Ihren Zwecken besser entspricht.

Levi
quelle
Dies war genau das, was ich brauchte. Wie Sie vorgeschlagen haben, musste ich controllerContext.RequestContext verwenden.
Jonathan Freeland
4
Nett! Ich hatte das RequireRequestValue-Attribut noch nicht gesehen. Das ist gut zu wissen.
CoderDennis
1
Wir können valueprovider verwenden, um Werte aus verschiedenen Quellen abzurufen, z. B.: controllerContext.Controller.ValueProvider.GetValue (value);
Jone Polvora
Ich ging ...RouteData.Valuesstattdessen nach, aber das "funktioniert". Ob es sich um ein gutes Muster handelt oder nicht, steht zur Debatte. :)
Bambams
1
Ich habe meine vorherige Bearbeitung abgelehnt bekommen, also werde ich nur kommentieren: [AttributeUsage (AttributeTargets.All, AllowMultiple = true)]
Mzn
7

Die Parameter in Ihren Routen {roleId}, {applicationName}und {roleName}die Parameternamen nicht in den Aktionsmethoden entsprechen. Ich weiß nicht, ob das wichtig ist, aber es macht es schwieriger herauszufinden, was Ihre Absicht ist.

Entspricht Ihre itemId einem Muster, das über Regex abgeglichen werden kann? Wenn ja, können Sie Ihrer Route eine Einschränkung hinzufügen, sodass nur URLs, die dem Muster entsprechen, eine itemId enthalten.

Wenn Ihre itemId nur Ziffern enthalten würde, würde dies funktionieren:

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

Bearbeiten: Sie können der AssignRemovePrettyRoute auch eine Einschränkung hinzufügen, sodass beide {parentName}und {itemName}erforderlich sind.

Bearbeiten 2: Da Ihre erste Aktion nur zu Ihrer zweiten Aktion umleitet, können Sie Unklarheiten beseitigen, indem Sie die erste umbenennen.

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

Geben Sie dann die Aktionsnamen in Ihren Routen an, um den Aufruf der richtigen Methode zu erzwingen:

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
quelle
1
Sorry Dennis, die Parameter stimmen tatsächlich überein. Ich habe die Frage behoben. Ich werde die Regex-Zurückhaltung ausprobieren und mich bei Ihnen melden. Vielen Dank!
Jonathan Freeland
Ihre zweite Bearbeitung hat mir geholfen, aber letztendlich war es Levis Vorschlag, der den Deal besiegelte. Danke noch einmal!
Jonathan Freeland
7

Ein anderer Ansatz besteht darin, eine der Methoden umzubenennen, damit kein Konflikt entsteht. Beispielsweise

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

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

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

RickAndMSFT
quelle
3

Vor kurzem habe ich die Gelegenheit genutzt, die Antwort von @ Levi zu verbessern, um eine größere Bandbreite von Szenarien zu unterstützen, mit denen ich mich befassen musste, z. B.: Unterstützung mehrerer Parameter, Übereinstimmung mit einem von ihnen (anstelle von allen) und sogar mit keinem von ihnen.

Hier ist das Attribut, das ich jetzt verwende:

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

Weitere Informationen und Beispiele für die Implementierung finden Sie in diesem Blog-Beitrag , den ich zu diesem Thema geschrieben habe.

Darkseal
quelle
Danke, tolle Verbesserung! Aber ParameterNames ist nicht in ctor
nvirth
0
routes.MapRoute("AssignRemove",
                "Items/{parentName}/{itemName}",
                new { controller = "Items", action = "Assign" }
                );

Erwägen Sie die Verwendung der MVC Contribs-Testroutenbibliothek, um Ihre Routen zu testen

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