Using strongly typed ActionLink when the action method does not accept a primitive type

Does anyone know how I can do something like:

Html.ActionLink (c => c.SomeAction (new MessageObject {Id = 1}))

This should output a link with the url "/ Controller / SomeAction / 1" pointing to the ActionMethod along the lines:

public Controller : Controller
{
  public ActionResult SomeMethod(MessageObject message)
  {
      // do something with the message
      return View();
  }
}

      

I wrote something similar for generating forms, but you don't need to include the Id value at the end of the Url. Basically, I want to do some kind of reverse lookup on my routes, but I can't seem to find any document on how I can do this. I have a ModelBinder setup that is capable of creating a MessageObject from the GET and POST parameters, but I'm not sure how I can change the process.

Thanks, Matt

+1


source to share


4 answers


I ended up wrapping the following code in an extension method HtmlHelper. This will allow me to use something like Html.ActionLink (c => c.SomeAction (new MessageObject {Id = 1}))

and have all MessageObject properties created as 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 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);
                    }
                }
            });
        }

      

+1


source


I'm not sure what exactly you are trying to do as your example url does not match the one required to sign your method. Typically, if you use a method that requires a complex object, you pass the values ​​to construct that object in the query string or as form parameters, and ModelBinder constructs the object from the data provided in the parameters. If you only want to pass an ID then the method usually takes no parameters, you retrieve the ID from RouteData and view the object in persistent storage (or cache). If you want to do the latter, your method should look like this:



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

      

0


source


I'm not sure what you are trying to do from your example url does not match the required signature for your method. Usually, if you use a method that requires a complex object, you pass values ​​to construct that object in a string request or as form parameters and ModelBinder builds the object from the data given in the parameters.

LOL that's exactly what I'm trying to do :) This url works fine and matches this method, the model binder can turn this url into a route that matches this action and works fine. (This route maps "1" to a RouteValue named Id, which then associates the model with the Id field of the message object).

What I'm trying to do is go the other way, accept a method call and turn it into a route.

0


source


IF you don't mind adding a method next to every action in your controller that you want to generate urls for, you can proceed as follows. This has some disadvantages compared to your lambda expression approach, but some of them too.

Implementation: -

Add this to your controller for every action method that requires strongly typed url generation ...

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

      

You can use them in your cartographic route code ...

Routes.MapRoute ("SomeMethod", 
                  HomeController.SomeMethodUrl,
                  HomeController.SomeMethodRouteValues(null));

      

And you can use them ANYWHERE, you need to create a link to this action: - eg.

<%=Url.RouteUrl(HomeController.SomeMethodValues(new MessageObject())) %>

      

If you do it like this ...

1) You only have one place in your code where parameters for any action are defined.

2) There is only one way to convert these parameters to routes, since Html.RouteLink and Url.RouteUrl can take HomeController.SomeMethodRouteValues ​​(...) as a parameter.

3) Easily set default values ​​for any additional route values.

4) Easily refactor your code without breaking any urls. Suppose you need to add the SomeMethod parameter. All you do is change both SomeMethodUrl and SomeMethodRouteValues ​​() to match the new parameter list, and then you fix any broken links in your code or views. Try this with the new {action = "SomeMethod", ...} scattered throughout your code.

5) You get Intellisense support so you can see what parameters are needed to build a link or URL for any action. As far as "strongly typed" is concerned, this approach seems better than using lambda expressions, where there is no compile-time or design-time check that your link generation options are valid.

The downside is that you still have to keep these methods in sync with the actual activity (but they can be next to each other in your code, making it easier to see). Purists no doubt object to this approach, but in practice they say they find and fix bugs that would otherwise require testing in order to find and help replace the strongly typed page methods that we used in our WebForms projects.

0


source







All Articles