ASP.NET Методы неоднозначных действий MVC
-
20-08-2019 - |
Вопрос
У меня есть два метода действия, которые противоречат друг другу.По сути, я хочу иметь возможность перейти к одному и тому же представлению, используя два разных маршрута, либо по идентификатору элемента, либо по имени элемента и его родительского элемента (элементы могут иметь одно и то же имя у разных родительских элементов).Для фильтрации списка можно использовать поисковый запрос.
Например...
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) { ... }
Решение
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; }
}
В приведенном выше примере атрибут просто говорит "этот метод соответствует, если ключ ХХХ присутствовал в запросе ". Вы также можете выполнить фильтрацию по информации, содержащейся в маршруте (ControllerContext.RequestContext), если это лучше соответствует вашим целям.
Другие советы
Параметры в ваших маршрутах {roleId}
, {applicationName}
и {roleName}
не совпадайте с именами параметров в ваших методах действия.Я не знаю, имеет ли это значение, но так сложнее понять, каковы ваши намерения.
Соответствуют ли ваши идентификаторы элементов шаблону, который может быть сопоставлен с помощью регулярного выражения?Если это так, то вы можете добавить ограничение к своему маршруту, чтобы только 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+" }
);
Другой подход заключается в переименовании одного из методов, чтобы не возникло конфликта.Например
// 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
Недавно я воспользовался возможностью улучшить ответ @Levi's, чтобы поддерживать более широкий спектр сценариев, с которыми мне приходилось иметь дело, таких как:поддержка нескольких параметров, сопоставление любому из них (вместо всех) и даже ни одному из них.
Вот атрибут, который я использую сейчас:
/// <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
}
}
Для получения дополнительной информации и практических примеров реализации ознакомьтесь это сообщение в блоге что я написал на эту тему.
routes.MapRoute("AssignRemove",
"Items/{parentName}/{itemName}",
new { controller = "Items", action = "Assign" }
);
рассмотрите возможность использования библиотеки тестовых маршрутов MVC Contributs для тестирования ваших маршрутов
"Items/parentName/itemName".Route().ShouldMapTo<Items>(x => x.Assign("parentName", itemName));