Использование строго типизированного ActionLink, когда метод действия не принимает примитивный тип
Кто-нибудь знает, как я мог бы сделать что-то вроде:
Html.ActionLink (c => c.SomeAction (new MessageObject {Id = 1}))
Это должно вывести ссылку с URL-адресом "/Controller/SomeAction/1", указывающим на ActionMethod вдоль строк:
public Controller : Controller
{
public ActionResult SomeMethod(MessageObject message)
{
// do something with the message
return View();
}
}
Я написал нечто подобное для генерации форм, но нет необходимости включать значение Id в конце URL-адреса. По сути, я хочу сделать какой-то обратный поиск в моих маршрутах, но я не могу найти никаких документов о том, как я могу это сделать. У меня есть установка ModelBinder, которая может построить MessageObject из параметров GET и POST, но я не уверен, как можно повернуть процесс вспять.
Спасибо Мэтт
4 ответа
В итоге я обернул следующий код в метод расширения HtmlHelper. Это позволило бы мне использовать что-то вроде Html.ActionLink(c => c.SomeAction(new MessageObject { Id = 1 }))
и иметь все свойства объекта MessageObject, созданного как RouteValues.
public static RouteValueDictionary GetRouteValuesFromExpression<TController>(Expression<Action<TController>> action)
where TController : Controller
{
Guard.Against<ArgumentNullException>(action == null, @"Action passed to GetRouteValuesFromExpression cannot be null.");
MethodCallExpression methodCall = action.Body as MethodCallExpression;
Guard.Against<InvalidOperationException>(methodCall == null, @"Action passed to GetRouteValuesFromExpression must be method call");
string controllerName = typeof(TController).Name;
Guard.Against<InvalidOperationException>(!controllerName.EndsWith("Controller"), @"Controller passed to GetRouteValuesFromExpression is incorrect");
RouteValueDictionary rvd = new RouteValueDictionary();
rvd.Add("Controller", controllerName.Substring(0, controllerName.Length - "Controller".Length));
rvd.Add("Action", methodCall.Method.Name);
AddParameterValuesFromExpressionToDictionary(rvd, methodCall);
return rvd;
}
/// <summary>
/// Adds a route value for each parameter in the passed in expression. If the parameter is primitive it just uses its name and value
/// if not, it creates a route value for each property on the object with the property's name and value.
/// </summary>
/// <param name="routeValues"></param>
/// <param name="methodCall"></param>
private static void AddParameterValuesFromExpressionToDictionary(RouteValueDictionary routeValues, MethodCallExpression methodCall)
{
ParameterInfo[] parameters = methodCall.Method.GetParameters();
methodCall.Arguments.Each(argument =>
{
int index = methodCall.Arguments.IndexOf(argument);
ConstantExpression constExpression = argument as ConstantExpression;
if (constExpression != null)
{
object value = constExpression.Value;
routeValues.Add(parameters[index].Name, value);
}
else
{
object actualArgument = argument;
MemberInitExpression expression = argument as MemberInitExpression;
if (expression != null)
{
actualArgument = Expression.Lambda(argument).Compile().DynamicInvoke();
}
// create a route value for each property on the object
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(actualArgument))
{
object obj2 = descriptor.GetValue(actualArgument);
routeValues.Add(descriptor.Name, obj2);
}
}
});
}
Если вы не возражаете добавить метод рядом с каждым действием в вашем контроллере, для которого вы хотите создать URL, вы можете действовать следующим образом. Это имеет некоторые недостатки по сравнению с вашим подходом к лямбда-выражению, но есть и недостатки.
Реализация:-
Добавьте это в метод действия Controller for EACH, для которого вы хотите создать строго типизированный URL-адрес...
// This const is only needed if the route isn't already mapped
// by some more general purpose route (e.g. {controller}/{action}/{message}
public const string SomeMethodUrl = "/Home/SomeMethod/{message}";
// This method generates route values that match the SomeMethod method signature
// You can add default values here too
public static object SomeMethodRouteValues(MessageObject messageObject)
{
return new { controller = "Home", action = "SomeMethod",
message = messageObject };
}
Вы можете использовать их в своем коде отображения маршрута...
Routes.MapRoute ("SomeMethod",
HomeController.SomeMethodUrl,
HomeController.SomeMethodRouteValues(null));
И вы можете использовать их ВЕЗДЕ, где вам нужно создать ссылку на это действие: - например,
<%=Url.RouteUrl(HomeController.SomeMethodValues(new MessageObject())) %>
Если вы делаете это таким образом...
1) У вас есть только одно место в вашем коде, где определены параметры для любого действия
2) Есть только один способ, которым эти параметры преобразуются в маршруты, потому что Html.RouteLink и Url.RouteUrl могут оба принять HomeController.SomeMethodRouteValues (...) в качестве параметра.
3) Легко установить значения по умолчанию для любых дополнительных значений маршрута.
4) Рефакторинг вашего кода легко, не нарушая URL. Предположим, вам нужно добавить параметр в SomeMethod. Все, что вы делаете, это изменяете SomeMethodUrl и SomeMethodRouteValues (), чтобы они соответствовали новому списку параметров, а затем вы исправляете все неработающие ссылки в коде или в представлениях. Попробуйте сделать это с новым {action="SomeMethod", ...}, разбросанным по всему коду.
5) Вы получаете поддержку Intellisense, чтобы вы могли ВИДЕТЬ, какие параметры необходимы для создания ссылки или URL-адреса для какого-либо действия. С точки зрения "строгой типизации", этот подход кажется лучше, чем использование лямбда-выражений, где нет проверки времени компиляции или ошибки разработки, чтобы убедиться, что ваши параметры генерации ссылки действительны.
Недостатком является то, что вам по-прежнему необходимо синхронизировать эти методы с фактическим методом действия (но они могут быть рядом друг с другом в коде, что облегчает их просмотр). Пуристы, без сомнения, будут возражать против этого подхода, но практически говорят о том, что он находит и исправляет ошибки, которые в противном случае потребовали бы тестирования, чтобы найти, и это помогает заменить строго типизированные методы Page, которые мы использовали в наших проектах WebForms.
Я не уверен, что именно вы пытаетесь сделать, так как ваш пример URL не совпадает с тем, что требуется для подписи вашего метода. Как правило, если вы используете метод, который требует сложного объекта, вы передаете значения для создания этого объекта в строке запроса или в качестве параметров формы, а ModelBinder создает объект из данных, предоставленных в параметрах. Если вы хотите передать только идентификатор, то метод обычно не принимает никаких параметров, вы извлекаете идентификатор из RouteData и ищите объект в постоянном хранилище (или в кэше). Если вы хотите сделать последнее, ваш метод должен выглядеть так:
public ActionResult SomeMethod()
{
int messageObjectID;
if (RouteData.Values.TryGetValue("id",out messageObjectID))
{
... get the object with the correct id and process it...
}
else
{
... error processing because the id was not available...
}
return View();
}
Я не уверен, что именно вы пытаетесь сделать, так как ваш пример URL не совпадает с тем, что требуется для подписи вашего метода. Как правило, если вы используете метод, который требует сложного объекта, вы передаете значения для создания этого объекта в строке запроса или в качестве параметров формы, а ModelBinder создает объект из данных, предоставленных в параметрах.
LOL that's exactly what I'm trying to do:) That url works fine and maps to that method, the model binder is able to turn that URL into a route that maps to that action and works fine. (That route maps the "1" to a RouteValue named Id, which the model binder then assigns to the Id field of the message object).
What I'm trying to do is go the other way, take a method call and turn it into a route.