5
Vote

LinkExtensions.BuildUrlFromExpression<T>() is broken

description

The method uses internally LinkBuilder.BuildUrlFromExpression<T>().
The latter calls
routeCollection.GetVirtualPath(context, routeValues)
instead of
routeCollection.GetVirtualPathForArea(context, routeValues);
which causes invalid results when using areas.

comments

maksm wrote Dec 21, 2010 at 8:47 AM

Since it's used by other helpers in the MvcFutures library (like FormExtensions.BeginForm()) I use the following UrlHelper extension method to build url's until this issue is resolved:

public static string Action(this UrlHelper helper, Expression> action)
        where TController : Controller
    {
        var routeValues = ExpressionHelperInternal.GetRouteValuesFromExpression(action);
        var vpd = helper.RouteCollection.GetVirtualPathForArea(helper.RequestContext, routeValues);
        return (vpd == null) ? null : vpd.VirtualPath;
    }

flopo wrote Dec 21, 2010 at 11:11 AM

I used the same solution as maksm but later I discovered that links between areas may not work if the two areas have controllers with the same names (i.e Controllers.Customer.HomeController and Controllers.Admin.HomeController).
WARNING: I'm not sure GetAreaByNamespace is implemented correctly but it does the job in my scenarios.

public static string BuildUrlFromExpression(RequestContext context, RouteCollection routeCollection, Expression> action) where TController : Controller
    {
        RouteValueDictionary routeValues = Microsoft.Web.Mvc.Internal.ExpressionHelper
                                                .GetRouteValuesFromExpression(action);

        //fzawada: in MVC Futures area is not extracted from expression            
        var area = GetAreaByNamespace(routeCollection, typeof(TController).Namespace);
        if (area != null)
        {
            routeValues.Add("area", area);
        }

        //fzawada: in MVC Futures GetVirtualPath() is used causing problems when building urls with areas...
        var vp = routeCollection.GetVirtualPathForArea(context, routeValues);
        if (vp != null)
        {
            return vp.VirtualPath;
        }
        return null;
    }
private static string GetAreaByNamespace(RouteCollection routeCollection, string controllerNamespace)
    {
        controllerNamespace = controllerNamespace.ToLower();

        foreach (Route route in routeCollection)
        {
            var dt = route.DataTokens;
            if (route.DataTokens != null && 
                route.DataTokens.ContainsKey("namespaces") && 
                route.DataTokens.ContainsKey("area"))
            {
                var areaNamespaces = (ICollection)route.DataTokens["namespaces"];
                foreach (var areaNamespace in areaNamespaces)
                {
                    if (controllerNamespace.StartsWith(areaNamespace.ToLower().Replace("*", "")))
                    {
                        return (string)route.DataTokens["area"];
                    }
                }
            }
        }
        return null;
    }