views:

765

answers:

2

Is it possible to get a URL from an action without knowing ViewContext (e.g., in a controller)? Something like this:

LinkBuilder.BuildUrlFromExpression(ViewContext context, Expression<Action<T>> action)

...but using Controller.RouteData instead of ViewContext. I seem to have metal block on this.

+1  A: 

Here's how I do it in a unit test:

    private string RouteValueDictionaryToUrl(RouteValueDictionary rvd)
    {
        var context = MvcMockHelpers.FakeHttpContext("~/");
        // _routes is a RouteCollection
        var vpd = _routes.GetVirtualPath(
            new RequestContext(context, _
                routes.GetRouteData(context)), rvd);
        return vpd.VirtualPath;
    }

Per comments, I'll adapt to a controller:

string path = RouteTable.Routes.GetVirtualPath(
    new RequestContext(HttpContext, 
        RouteTable.Routes.GetRouteData(HttpContext)),
    new RouteValueDictionary( 
        new { controller = "Foo",
              action = "Bar" })).VirtualPath;

Replace "Foo" and "Bar" with real names. This is off the top of my head, so I can't guarantee that it's the most efficient solution possible, but it should get you on the right track.

Craig Stuntz
I do not need the URL for the current request, but for some other action.
Tim Scott
I understand. The solution above still applies. There is *no* connection to the current request there.
Craig Stuntz
I don't understand then. Let's say I want to get the Url for FooController.Bar("foo", "bar"), which would be "/MyVirtualDir/Foo.mvc/Bar/foo/bar". How could I use this to get that in a (different) controller method?
Tim Scott
See new revision.
Craig Stuntz
Craig, can you also update your answer above, using a strongly typed controller (ie. Microsoft.Web.MVC) instead of having to hardcode the controller name and action? is that possible?
Pure.Krome
All controllers are strongly typed. You can certainly reflect the controller and action name if you can take the performance hit, but don't forget about ActionNameAttribute. If I were doing this, I'd try to find a way to use the framework's internal controller and action name caches.
Craig Stuntz
+1  A: 

Craig, Thanks for the correct answer. It works great, and it also go me thinking. So in my drive to eliminate those refactor-resistent "magic strings" I have developed a variation on your solution:

public static string GetUrlFor<T>(this HttpContextBase c, Expression<Func<T, object>> action)
    where T : Controller
{
    return RouteTable.Routes.GetVirtualPath(
        new RequestContext(c, RouteTable.Routes.GetRouteData(c)), 
        GetRouteValuesFor(action)).VirtualPath;
}

public static RouteValueDictionary GetRouteValuesFor<T>(Expression<Func<T, object>> action) 
    where T : Controller
{
    var methodCallExpresion = ((MethodCallExpression) action.Body);
    var controllerTypeName = methodCallExpresion.Object.Type.Name;
    var routeValues = new RouteValueDictionary(new
    {
        controller = controllerTypeName.Remove(controllerTypeName.LastIndexOf("Controller")), 
        action = methodCallExpresion.Method.Name
    });
    var methodParameters = methodCallExpresion.Method.GetParameters();
    for (var i = 0; i < methodParameters.Length; i++)
    {
        var value = Expression.Lambda(methodCallExpresion.Arguments[i]).Compile().DynamicInvoke();
        var name = methodParameters[i].Name;
        routeValues.Add(name, value);
    }
    return routeValues;
}

I know what some will say...dreaded reflection! In my particular application, I think the benefit of maintainability outweighs performance conerns. I welcome any feedback on this idea and the code.

Tim Scott
Okay, this is a problem for testing controllers (any approach that uses Routes and RequestContext). I cannot figure how to correctly stub HttpContext (even after some fun with Reflector). I will put behind an interface to make controller more easily testable.
Tim Scott
I was able to figure out how to stub HttpContext to make this work in tests. The missing price was:SetupResult.For(request.AppRelativeCurrentExecutionFilePath).Return("~/");
Tim Scott