Metode tindakan ambigu ASP.NET MVC

135

Saya memiliki dua metode tindakan yang saling bertentangan. Pada dasarnya, saya ingin bisa mendapatkan tampilan yang sama menggunakan dua rute berbeda, baik dengan ID item atau dengan nama item dan induknya (item dapat memiliki nama yang sama di antara orang tua yang berbeda). Istilah pencarian dapat digunakan untuk memfilter daftar.

Sebagai contoh...

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

Berikut adalah metode tindakan saya (ada juga Removemetode tindakan) ...

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

Dan inilah rutenya ...

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

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

Saya mengerti mengapa kesalahan terjadi, karena pageparameternya bisa nol, tetapi saya tidak tahu cara terbaik untuk mengatasinya. Apakah desain saya awalnya buruk? Saya telah berpikir tentang memperluas Method #1tanda tangan untuk menyertakan parameter pencarian dan memindahkan logika Method #2ke metode pribadi yang akan mereka panggil, tetapi saya tidak percaya itu benar-benar akan menyelesaikan ambiguitas.

Bantuan apa pun akan sangat dihargai.


Solusi Aktual (berdasarkan jawaban Levi's)

Saya menambahkan kelas berikut ...

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

Dan kemudian mendekorasi metode tindakan ...

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

[RequireRouteValues(new[] { "itemId" })]
public ActionResult Assign(string itemId) { ... }
Jonathan Freeland
sumber
3
Terima kasih telah memposting implementasi yang sebenarnya. Ini pasti membantu orang dengan masalah serupa. Seperti yang saya alami hari ini. :-P
Paulo Santos
4
Luar biasa! Saran perubahan kecil: (sangat berguna) 1) params string [] valueNames untuk membuat deklarasi atribut lebih ringkas dan (preferensi) 2) mengganti tubuh metode IsValidForRequest denganreturn ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v));
Benjamin Podszun
2
Saya memiliki masalah parameter querystring yang sama. Jika Anda memerlukan parameter tersebut dipertimbangkan untuk persyaratan, contains = ...contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value) || controllerContext.RequestContext.HttpContext.Request.Params.AllKeys.Contains(value);
ganti
3
Perhatikan peringatan itu: parameter yang diperlukan harus dikirim persis seperti yang disebutkan. Jika parameter metode tindakan Anda adalah tipe kompleks yang diisi dengan meneruskan propertinya berdasarkan nama (dan membiarkan MVC memijatnya ke dalam tipe kompleks), sistem ini akan gagal karena namanya tidak ada dalam kunci querystring. Misalnya, ini tidak akan berhasil:, di ActionResult DoSomething(Person p)mana Personmemiliki berbagai properti sederhana seperti Name, dan permintaan untuk itu dibuat dengan nama properti secara langsung (misalnya, /dosomething/?name=joe+someone&other=properties).
patridge
4
Jika Anda menggunakan MVC4 seterusnya, Anda harus menggunakan controllerContext.HttpContext.Request[value] != nullbukan controllerContext.RequestContext.RouteData.Values.ContainsKey(value); tapi tetap saja pekerjaan yang bagus.
Kevin Farrugia

Jawaban:

180

MVC tidak mendukung metode overloading hanya berdasarkan tanda tangan, jadi ini akan gagal:

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

Namun, tidak metode dukungan overloading berdasarkan atribut:

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

Dalam contoh di atas, atribut hanya mengatakan "metode ini cocok jika kunci xxx ada dalam permintaan." Anda juga dapat memfilter berdasarkan informasi yang terdapat dalam rute (controllerContext.RequestContext) jika itu lebih sesuai dengan tujuan Anda.

Levi
sumber
Ini akhirnya menjadi apa yang saya butuhkan. Seperti yang Anda sarankan, saya perlu menggunakan controllerContext.RequestContext.
Jonathan Freeland
4
Bagus! Saya belum melihat atribut RequireRequestValue. Itu bagus untuk diketahui.
CoderDennis
1
kita bisa menggunakan valueprovider untuk mendapatkan nilai dari beberapa sumber seperti: controllerContext.Controller.ValueProvider.GetValue (value);
Jone Polvora
Aku pergi setelah itu ...RouteData.Values, tapi ini "berhasil". Apakah itu pola yang baik atau tidak terbuka untuk diperdebatkan. :)
bambam
1
Saya mendapat suntingan saya sebelumnya ditolak jadi saya hanya akan berkomentar: [AttributeUsage (AttributeT Target.All, AllowMultiple = true)]
Mzn
7

Parameter di rute Anda {roleId}, {applicationName}dan {roleName}tidak cocok dengan nama parameter dalam metode tindakan Anda. Saya tidak tahu apakah itu penting, tetapi itu membuatnya lebih sulit untuk mengetahui apa niat Anda.

Apakah itemId Anda sesuai dengan pola yang dapat dicocokkan melalui regex? Jika demikian, Anda dapat menambahkan pembatas ke rute Anda sehingga hanya url yang cocok dengan pola yang diidentifikasi sebagai berisi itemId.

Jika itemId Anda hanya berisi angka, ini akan berfungsi:

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

Sunting: Anda juga bisa menambahkan batasan ke AssignRemovePrettyrute sehingga keduanya {parentName}dan {itemName}dibutuhkan.

Sunting 2: Juga, karena tindakan pertama Anda hanya mengarahkan ke tindakan kedua, Anda dapat menghilangkan beberapa ambiguitas dengan mengganti nama yang pertama.

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

Kemudian tentukan nama Tindakan di rute Anda untuk memaksa metode yang tepat dipanggil:

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
sumber
1
Maaf Dennis, parameternya benar-benar cocok. Saya telah memperbaiki pertanyaan itu. Saya akan mencoba pengekangan regex dan menghubungi Anda kembali. Terima kasih!
Jonathan Freeland
Hasil edit kedua Anda membantu saya, tetapi pada akhirnya itu adalah saran Levi yang menutup kesepakatan. Terima kasih lagi!
Jonathan Freeland
7

Pendekatan lain adalah dengan mengganti nama salah satu metode agar tidak terjadi konflik. Sebagai contoh

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

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

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

RickAndMSFT
sumber
3

Baru-baru ini saya mengambil kesempatan untuk meningkatkan jawaban @ Levi's untuk mendukung berbagai skenario yang harus saya tangani, seperti: dukungan beberapa parameter, cocokkan salah satu dari mereka (bukan semuanya) dan bahkan tidak mencocokkan tidak satu pun dari mereka.

Inilah atribut yang saya gunakan sekarang:

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

Untuk info lebih lanjut dan contoh cara penerapan, lihat posting blog yang saya tulis tentang topik ini.

Darkseal
sumber
Terima kasih, peningkatan yang luar biasa! Tapi ParameterNames tidak disetel di ctor
nvirth
0
routes.MapRoute("AssignRemove",
                "Items/{parentName}/{itemName}",
                new { controller = "Items", action = "Assign" }
                );

pertimbangkan untuk menggunakan pustaka rute pengujian MVC Contribs untuk menguji rute Anda

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