views:

974

answers:

4

Hi,

Does anyone know how I could go about doing something like :

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

This should output a link with the url of "/Controller/SomeAction/1", pointing at an ActionMethod along the lines of:

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

I've written something similar for generating forms, but that doens't need to include the Id value on the end of the Url. Basically I want to do some sort of reverse lookup in my routes but I can't find any doco on how I might go about doing that. I have a ModelBinder setup that is able to build a MessageObject from GET and POST parameters, but I'm not sure how I can reverse the process.

Thanks, Matt

A: 

I'm not sure exactly what you are trying to do since your example URL doesn't match that required for the signature of 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 the ModelBinder constructs the object from the data supplied in the parameters. If you want to pass just the id, then the method typically doesn't take any parameters, you extract the id from the RouteData, and look the object up in persistent storage (or in a cache). If you want to do the latter, your method should look like:

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

I'm not sure exactly what you are trying to do since your example URL doesn't match that required for the signature of 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 the ModelBinder constructs the object from the data supplied in the parameters.

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.

mattcole
A: 

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

and have all the properties of the MessageObject 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'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);
                    }
                }
            });
        }
mattcole
A: 

IF you don't mind adding a method alongside each action in your controller for which you want to generate URLs you can proceed as follows. This has some downsides compared to your lambda expression approach but some upsides too.

Implementation:-

Add this to your Controller for EACH action method for which you want 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 these in your route mapping code ...

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

And you can use them EVERYWHERE you need to generate a link to that action:- e.g.

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

If you do it this way ...

1) You have just one place in your code where the parameters to any action are defined

2) There is just one way that those parameters get converted to routes because Html.RouteLink and Url.RouteUrl can both take HomeController.SomeMethodRouteValues(...) as a parameter.

3) It's easy to set defaults for any optional route values.

4) It's easy to refactor your code without breaking any urls. Suppose you need to add a parameter to SomeMethod. All you do is change both SomeMethodUrl and SomeMethodRouteValues() to match the new parameter list and then you go fix all the broken references whether in code or in Views. Try doing that with new {action="SomeMethod", ...} scattered all over your code.

5) You get Intellisense support so you can SEE what parameters are needed to construct a link or url to any action. As far as being 'strongly-typed', this approach seems better than using lambda expressions where there is no compile-time or design-time error checking that your link generation parameters are valid.

The downside is that you still have to keep these methods in sync with the actual action method (but they can be next to each other in the code making it easy to see). Purists will no doubt object to this approach but practically speaking it's finding and fixing bugs that would otherwise require testing to find and it's helping replace the strongly typed Page methods we used to have in our WebForms projects.

Hightechrider