วิธีการดำเนินการที่ไม่ชัดเจนของ ASP.NET MVC


135

ฉันมีสองวิธีการดำเนินการที่ขัดแย้งกัน โดยพื้นฐานแล้วฉันต้องการไปยังมุมมองเดียวกันโดยใช้เส้นทางที่แตกต่างกันสองเส้นทางไม่ว่าจะโดย ID ของรายการหรือตามชื่อของรายการและของผู้ปกครอง (รายการสามารถมีชื่อเดียวกันในกลุ่มผู้ปกครองที่แตกต่างกันได้) สามารถใช้คำค้นหาเพื่อกรองรายการ

ตัวอย่างเช่น...

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

นี่คือวิธีการดำเนินการของฉัน (มีRemoveวิธีดำเนินการด้วย) ...

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

และนี่คือเส้นทาง ...

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

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

ฉันเข้าใจว่าเหตุใดจึงเกิดข้อผิดพลาดเนื่องจากpageพารามิเตอร์อาจเป็นโมฆะ แต่ฉันไม่สามารถหาวิธีที่ดีที่สุดในการแก้ไขได้ การออกแบบของฉันเริ่มต้นด้วยไม่ดีหรือไม่? ฉันคิดเกี่ยวกับการขยายMethod #1ลายเซ็นเพื่อรวมพารามิเตอร์การค้นหาและย้ายตรรกะเข้าMethod #2ออกเป็นวิธีการส่วนตัวที่ทั้งคู่จะเรียก แต่ฉันไม่เชื่อว่าจะช่วยแก้ความคลุมเครือได้จริง

ความช่วยเหลือใด ๆ ที่จะได้รับการชื่นชมอย่างมาก.


โซลูชันที่แท้จริง (ตามคำตอบของ Levi)

ฉันเพิ่มชั้นเรียนต่อไปนี้ ...

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

แล้วก็ตกแต่งวิธีดำเนินการ ...

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

[RequireRouteValues(new[] { "itemId" })]
public ActionResult Assign(string itemId) { ... }

3
ขอขอบคุณที่โพสต์การใช้งานจริง ช่วยให้ผู้ที่มีปัญหาคล้ายกันนี้ได้แน่นอน อย่างที่ฉันมีในวันนี้ :-P
Paulo Santos

4
สุดทึ่ง! ข้อเสนอแนะการเปลี่ยนแปลงเล็กน้อย: (imo มีประโยชน์จริงๆ) 1) สตริง params [] valueNames เพื่อทำให้การประกาศแอตทริบิวต์กระชับมากขึ้นและ (การตั้งค่า) 2) แทนที่เนื้อความของเมธอด IsValidForRequest ด้วยreturn ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v));
Benjamin Podszun

2
ฉันมีปัญหาพารามิเตอร์สตริงการสืบค้นเดียวกัน หากคุณต้องการพารามิเตอร์เหล่านั้นที่พิจารณาความต้องการให้สลับcontains = ...ส่วนนี้ออกมา:contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value) || controllerContext.RequestContext.HttpContext.Request.Params.AllKeys.Contains(value);
patridge

3
หมายเหตุคำเตือน: ต้องส่งพารามิเตอร์ที่จำเป็นตรงตามที่ตั้งชื่อ หากพารามิเตอร์วิธีการดำเนินการของคุณเป็นประเภทที่ซับซ้อนที่เติมโดยการส่งผ่านคุณสมบัติตามชื่อ (และปล่อยให้ MVC นวดให้เป็นประเภทที่ซับซ้อน) ระบบนี้จะล้มเหลวเนื่องจากชื่อไม่อยู่ในคีย์สตริงการสืบค้น ตัวอย่างเช่นสิ่งนี้จะใช้ไม่ได้: ActionResult DoSomething(Person p)โดยที่Personมีคุณสมบัติง่ายๆต่างๆเช่นNameและการร้องขอจะสร้างด้วยชื่อคุณสมบัติโดยตรง (เช่น/dosomething/?name=joe+someone&other=properties)
patridge

4
หากคุณใช้ MVC4 เป็นต้นไปคุณควรใช้controllerContext.HttpContext.Request[value] != nullแทนcontrollerContext.RequestContext.RouteData.Values.ContainsKey(value); แต่เป็นผลงานที่ดี
Kevin Farrugia

คำตอบ:


180

MVC ไม่สนับสนุนวิธีการโอเวอร์โหลดตามลายเซ็นเพียงอย่างเดียวดังนั้นสิ่งนี้จะล้มเหลว:

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

แต่ก็ไม่วิธีการสนับสนุนการบรรทุกเกินพิกัดตามแอตทริบิวต์:

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

ในตัวอย่างข้างต้นแอตทริบิวต์เพียงระบุว่า "วิธีนี้จะตรงกับกรณีที่มีคีย์xxxอยู่ในคำขอ" คุณยังสามารถกรองตามข้อมูลที่มีอยู่ภายในเส้นทาง (controllerContext.RequestContext) ว่าเหมาะสมกับวัตถุประสงค์ของคุณมากกว่าหรือไม่


สิ่งนี้กลายเป็นเพียงสิ่งที่ฉันต้องการ ตามที่คุณแนะนำฉันต้องใช้ controllerContext.RequestContext
Jonathan Freeland

4
ดี! ฉันยังไม่เห็นแอตทริบิวต์ RequireRequestValue นั่นเป็นสิ่งที่ดีที่จะรู้
CoderDennis

1
เราสามารถใช้ valueprovider เพื่อรับค่าจากแหล่งต่างๆเช่น: controllerContext.Controller.ValueProvider.GetValue (value);
Jone Polvora

ฉันเดินตาม...RouteData.Valuesแทน แต่ "ได้ผล" ไม่ว่ารูปแบบที่ดีจะเปิดให้มีการถกเถียงกันหรือไม่ :)
bambams

1
ฉันได้รับการแก้ไขครั้งก่อนของฉันถูกปฏิเสธดังนั้นฉันจะแสดงความคิดเห็น: [AttributeUsage (AttributeTargets all, AllowMultiple = true)]
Mzn

7

พารามิเตอร์ในเส้นทางของคุณ{roleId}, {applicationName}และ{roleName}ไม่ตรงกับชื่อพารามิเตอร์ในวิธีการกระทำของคุณ ฉันไม่รู้ว่ามันสำคัญหรือเปล่า แต่มันทำให้ยากที่จะเข้าใจว่าความตั้งใจของคุณคืออะไร

itemId ของคุณสอดคล้องกับรูปแบบที่สามารถจับคู่ผ่าน regex ได้หรือไม่ ในกรณีนี้คุณสามารถเพิ่มข้อ จำกัด ในเส้นทางของคุณเพื่อให้ระบุเฉพาะ url ที่ตรงกับรูปแบบที่มี itemId

หาก itemId ของคุณมีเพียงตัวเลขเท่านั้นสิ่งนี้จะใช้ได้:

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

แก้ไข: คุณสามารถเพิ่มข้อ จำกัด ให้กับAssignRemovePrettyเส้นทางเพื่อให้ทั้งสองอย่าง{parentName}และ{itemName}จำเป็น

แก้ไข 2: นอกจากนี้เนื่องจากการกระทำแรกของคุณเพิ่งเปลี่ยนเส้นทางไปยังการกระทำที่ 2 ของคุณคุณสามารถลบความคลุมเครือได้โดยการเปลี่ยนชื่อรายการแรก

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

จากนั้นระบุชื่อการดำเนินการในเส้นทางของคุณเพื่อบังคับให้เรียกวิธีการที่เหมาะสม:

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+" }
                );

1
ขออภัยเดนนิสพารามิเตอร์ตรงกันจริง ฉันได้แก้ไขคำถามแล้ว ฉันจะลองใช้ความยับยั้งชั่งใจ regex และติดต่อกลับ ขอบคุณ!
Jonathan Freeland

การแก้ไขครั้งที่สองของคุณช่วยฉันได้ แต่ในที่สุดก็เป็นคำแนะนำของ Levi ที่ปิดผนึกข้อตกลง ขอบคุณอีกครั้ง!
Jonathan Freeland

7

อีกวิธีหนึ่งคือการเปลี่ยนชื่อวิธีการใดวิธีหนึ่งเพื่อให้ไม่มีข้อขัดแย้ง ตัวอย่างเช่น

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

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

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


3

เมื่อเร็ว ๆ นี้ฉันได้ใช้โอกาสในการปรับปรุงคำตอบของ @ Levi เพื่อรองรับสถานการณ์ที่กว้างขึ้นที่ฉันต้องจัดการเช่น: การรองรับพารามิเตอร์หลายตัวจับคู่ใด ๆ (แทนที่จะเป็นทั้งหมด) และยังไม่ตรงกับสถานการณ์ใดเลย

นี่คือคุณสมบัติที่ฉันใช้ตอนนี้:

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

สำหรับข้อมูลเพิ่มเติมและตัวอย่างวิธีการใช้งานโปรดดูบล็อกโพสต์ที่ฉันเขียนในหัวข้อนี้


ขอบคุณการปรับปรุงที่ดี! แต่ ParameterNames ไม่ได้ตั้งค่าใน ctor
61

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

พิจารณาใช้ไลบรารีเส้นทางทดสอบ MVC Contribs เพื่อทดสอบเส้นทางของคุณ

"Items/parentName/itemName".Route().ShouldMapTo<Items>(x => x.Assign("parentName", itemName));
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.