10

With the UrlHelper in MVC3, you can build a URL for a controller and an action using strings:

@Url.Action("Index", "Somewhere")

Which will route a request to SomewhereController.Index().

What I would like to do, is get a URL to a controller and action, but passing the Type for the controller:

@Url.Action("Index", typeof(SomewhereController))

Is there a way to do this?


Edit / Clarification:

I realize the convention for the Controllers is that the controller name routes to a class named {Name}Controller so I could just remove 'Controller' from the end of my Type.Name. I guess I was assuming that there was a way to override this convention with some custom routing. Though the more I look at it, I'm not sure that is possible...

Maybe MVC3 can only ever route to classes named "*Controller"? I'm combing through the MVC3 source looking for "Controller" hard coded somewhere, but haven't found the answer yet... But if it is possible to route "Somewhere" to the class SomewhereFoo instead of SomewhereController, then just removing "Controller" from the class name would be incorrect.

If someone can give me evidence for or against "Controller" being hard-coded into MVC3 somewhere, then I would feel more comfortable with the "Just remove Controller from the name" approach.

CodingWithSpike
  • 42,906
  • 18
  • 101
  • 138
  • It's in there somewhere. Last time I checked the ASP.NET MVC 2 source, I found it. It's a simple string concatenation, buried deep within the class structure. – Robert Harvey Nov 24 '11 at 04:41
  • 2
    The `ControllerTypeCache`, used by the `DefaultControllerFactory`, depends on the Controller having a prefix ending with a string as long as *Controller* as it removes that many characters from the type name when constructing the key for the cache. The cache is used to lookup the controller to use based on the value in the route values. There may be other places as well. I would not recommend violating this convention. – tvanfosson Nov 24 '11 at 04:45
  • 1
    Are you building some 3rd party component or something, why would you need to cover such an edge case? Even if it is possible I would like to meet the person who breaks the {Name}Controller convention and smack them for using a convention based technology and then breaking the convention! – keithwarren7 Nov 24 '11 at 04:48
  • @keithwarren7 : Agreed, stick with the convention, but yes it was for a 3rd party plugin (similar to MvcSitemapProvder) so I wanted to cover all the cases I can. @tvanfosson : thank you for the class names. I will open them up in Reflector and have a look. I guess my fear is someone injecting a different `ControllerFactory` implementation, and blowing everything up :) – CodingWithSpike Nov 24 '11 at 05:07
  • @rally25rs - you can see the actual source at http://aspnet.codeplex.com – tvanfosson Nov 24 '11 at 13:26
  • @keithwarren7: It's incredibly easy to break the convention for legitimate purposes. I have a GenericController and a controller factory for creating those GenericController's from "~/SomeType/" routes. This breaks convention in assuming that the controller name used in routing is the same as the name generated from the controller's type name. – rossisdead May 09 '13 at 19:33

5 Answers5

6

There is no existing extension for this but you could write your own, modeled on the ActionLink from MvcFutures. I suggest a generic method used like @Url.Action<SomewhereController>( c => c.Index )

public static UrlHelperExtensions
{
    public static string Action<TController>( this UrlHelper helper,  Expression<Action<T>> action ) where TController : Controller
    {
        var routeValues = GetRouteValuesFromExpression( action ); 
        return helper.Action( routeValues["action"], routeValues );
    }

    // copied from MvcFutures
    // http://aspnet.codeplex.com/SourceControl/changeset/view/72551#266392
    private static RouteValueDictionary GetRouteValuesFromExpression<TController>(Expression<Action<TController>> action) where TController : Controller
    {
        if (action == null) {
            throw new ArgumentNullException("action");
        }

        MethodCallExpression call = action.Body as MethodCallExpression;
        if (call == null) {
            throw new ArgumentException(MvcResources.ExpressionHelper_MustBeMethodCall, "action");
        }

        string controllerName = typeof(TController).Name;
        if (!controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)) {
            throw new ArgumentException(MvcResources.ExpressionHelper_TargetMustEndInController, "action");
        }
        controllerName = controllerName.Substring(0, controllerName.Length - "Controller".Length);
        if (controllerName.Length == 0) {
            throw new ArgumentException(MvcResources.ExpressionHelper_CannotRouteToController, "action");
        }

        // TODO: How do we know that this method is even web callable?
        //      For now, we just let the call itself throw an exception.

        var rvd = new RouteValueDictionary();
        rvd.Add("Controller", controllerName);
        rvd.Add("Action", call.Method.Name);
        AddParameterValuesFromExpressionToDictionary(rvd, call);
        return rvd;
    }
}
tvanfosson
  • 524,688
  • 99
  • 697
  • 795
  • Useful method, thanks! I tried to write one of these to take a Lambda when I first started with MVC3, because I didn't like the "magic strings" approach. I never got it actually working though, but it looks like you did. +1. – CodingWithSpike Nov 24 '11 at 04:47
  • @rally25rs *untested* -- and I've made one minor correction so that it is an actual extension method now. – tvanfosson Nov 24 '11 at 04:49
  • Marking this as the answer because it includes the `.Replace("Controller", "")` answer, plus takes it a step further into a nice extension method. Thanks for the help everyone! – CodingWithSpike Nov 24 '11 at 05:09
  • this was exactly the starting point I needed; I simplified it a bit for [my answer](http://stackoverflow.com/a/21733175/1037948) (tested in MVC3/4); I think you used `` rather than `` in your `Expression`. – drzaus Feb 12 '14 at 16:07
3
@Url.Action("Index", typeof(SomewhereController).Name.Replace("Controller", ""))
keithwarren7
  • 14,094
  • 8
  • 53
  • 74
1

There's a number of ways you could do this. If you are using a type, the easiest way I can think of is to implement a Name property, like this:

public static string Name { get { return "Somewhere"; } }

And then call your Action like this:

@Url.Action("Index", SomewhereController.Name);
Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
  • 3
    The one thing I dont like about this is that it does not withstand a refactor, if the class name is changed you have to update the string. Maybe a better approach is a GetName extension method off of the base controller class? – keithwarren7 Nov 24 '11 at 04:49
0

Based on accepted answer:

Usage

var url = Url.RouteUrl<MyController>(c => c.MyAction(dummy,args));

or:

@using (Html.BeginForm(Url.Route<MyController>(c => c.MyAction(dummy,args))))
{
    // yadda yadda
}

Extension Method

public static class ActionRouteHelper
{
    /// <summary>
    /// Given a controller/action selector, return its URL route
    /// </summary>
    /// <typeparam name="TController"></typeparam>
    /// <param name="url">helper extended</param>
    /// <param name="action">A lambda for choosing an action from the indicated controller.  May need to provide dummy method arguments.</param>
    /// <returns></returns>
    public static string RouteUrl<TController>(this UrlHelper url, Expression<Action<TController>> action) where TController : Controller
    {
        return url.RouteUrl(url.Route(action));
    }
    /// <summary>
    /// Given a controller/action selector, return its URL route
    /// </summary>
    /// <remarks>See inspiration from https://stackoverflow.com/a/8252301/1037948 </remarks>
    /// <typeparam name="TController">the controller class</typeparam>
    /// <param name="url">helper extended</param>
    /// <param name="action">A lambda for choosing an action from the indicated controller.  May need to provide dummy method arguments.</param>
    /// <returns></returns>
    public static RouteValueDictionary Route<TController>(this UrlHelper url, Expression<Action<TController>> action) where TController : Controller
    {
        // TODO: validate controller name for suffix, non-empty result, if that's important to you here rather than the link just not working
        var controllerName = typeof(TController).Name.Replace("Controller", string.Empty);

        // strip method name out of the expression, the lazy way (https://stackoverflow.com/questions/671968/retrieving-property-name-from-lambda-expression/17220748#17220748)
        var actionName = action.ToString(); // results in something like "c => c.MyAction(arg1,arg2)"
        var startOfMethodName = actionName.IndexOf('.')+1;
        var startOfArgList = actionName.IndexOf('(');
        actionName = actionName.Substring(startOfMethodName, startOfArgList - startOfMethodName);

        return new RouteValueDictionary {
            { "Controller", controllerName },
            { "Action", actionName }
        };
    }
}
Community
  • 1
  • 1
drzaus
  • 24,171
  • 16
  • 142
  • 201
0

You could create a Razor helper:

@helper MyAction(string action, Type controller) {
    var controllerName = typeof(controller)
        .Name.Replace("Controller", "");
    @Url.Action(action, controllerName);
}

Which can then be used in place of @Url.Action:

@MyAction("ActionName", typeof(MyController))
Chris Fulstow
  • 41,170
  • 10
  • 86
  • 110
  • I was working off the assumption that override the "*Controller" naming convention, through some kind of explicit routing or a custom route resolver of some sort, so this approach wouldn't work 100% of the time. That 'override' of the naming might not be possible in MVC3 though? I added clarifications to my original question... – CodingWithSpike Nov 24 '11 at 04:44