Неоднозначні методи дій ASP.NET MVC


135

У мене є два способи дії, які суперечать один одному. В основному, я хочу мати змогу дістатися до одного і того ж виду, використовуючи два різні маршрути, або за ідентифікатором елемента, або за іменем елемента, і його батьком (елементи можуть мати однакове ім’я для різних батьків). Пошуковий термін може використовуватися для фільтрації списку.

Наприклад...

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до приватного методу, який вони б обом викликали, але я не вірю, що насправді вирішить двозначність.

Будь-яка допомога буде дуже вдячна.


Фактичне рішення (на основі відповіді Леві)

Я додав наступний клас ...

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
Паулу Сантос

4
Дивовижний! Пропозиція про незначну зміну: (imo дійсно корисно) 1) params string [] valueNames, щоб зробити атрибут атрибута більш коротким і (перевагу) 2) замінити тіло методу IsValidForRequest наreturn ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v));
Бенджамін Подсун

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).
патрон

4
Якщо ви використовуєте MVC4 далі, вам слід використовувати controllerContext.HttpContext.Request[value] != nullзамість controllerContext.RequestContext.RouteData.Values.ContainsKey(value); але приємна робота.
Кевін Фарругія

Відповіді:


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), якщо це краще відповідає вашим цілям.


Це в кінцевому підсумку було саме тим, що мені було потрібно. Як ви запропонували, мені потрібно було використовувати ControContext.RequestContext.
Джонатан Фріленд,

4
Приємно! Я ще не бачив атрибут RequireRequestValue. Це добре знати.
CoderDennis

1
ми можемо використовувати valueprovider для отримання значень з кількох джерел, таких як: ControContext.Controller.ValueProvider.GetValue (значення);
Jone Polvora

Я пішов ...RouteData.Valuesнатомість, але це "працює". Незалежно від того, чи є це гарним зразком, відкрито для обговорення. :)
бамбуки

1
Я відхилив попереднє редагування, тому я просто прокоментую: [AttributeUsage (AttributeTargets.All, AllowMultiple = true)]
Mzn,

7

Параметри в маршрутах {roleId}, {applicationName}і {roleName}не збігаються з іменами параметрів в своїх методах дій. Я не знаю, чи це має значення, але це робить складніше зрозуміти, який ваш задум.

Чи відповідає ваш itemId шаблону, який можна зіставити за допомогою регулярного вираження? Якщо так, то ви можете додати обмеження до маршруту, щоб лише URL-адреси, що відповідають шаблону, були ідентифіковані як такі, що містять itemId.

Якщо ваш елементId містив лише цифри, то це діятиме:

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

Edit: Ви можете також додати обмеження на AssignRemovePrettyмаршрут , так що обидва {parentName}і {itemName}необхідні.

Редагування 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
Вибачте Деннісе, параметри насправді відповідають. Я вирішив питання. Я спробую спробувати обмеження для регулярного виведення та повернусь до вас. Дякую!
Джонатан Фріланд

Ваша друга редакція допомогла мені, але, зрештою, саме пропозиція Леві уклала угоду. Знову дякую!
Джонатан Фріланд


3

Нещодавно я скористався можливістю вдосконалити відповідь @ Леві на підтримку більш широкого спектру сценаріїв, з якими мені довелося зіткнутися, таких як: підтримка декількох параметрів, відповідність будь-якому з них (замість них усіх) і навіть відповідність жодному з них.

Ось атрибут, який я зараз використовую:

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

Для отримання додаткової інформації та способів реалізації зразків ознайомтеся з публікацією цього блогу, яку я написав на цю тему.


Дякую, велике поліпшення! Але параметрNames не встановлений в
ctor

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));
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.