Использование строго типизированного 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.

Другие вопросы по тегам