views:

521

answers:

2

I have a web application using MVC 2 Preview 2 and after all routes are registered, I need to wrap each route in a decorator further down the chain. The problem is, doing so breaks routing. What ends up happening is the GetVirtualPath method will match falsely for other areas in the application (I'm using single-project areas). It doesn't matter if the decorator does anything useful or not. Using the following passthrough is all you need to break it.

public class RouteDecorator: RouteBase
{
    readonly RouteBase _route;

    public RouteDecorator(RouteBase route)
    {
        _route = route;
    }

    public override RouteData GetRouteData(HttpContextBase context)
    {
        return _route.GetRouteData(context);
    }

    public override VirtualPathData GetVirtualPath(RequestContext context, RouteValueDictionary values)
    {
        return _route.GetVirtualPath(context, values);
    }
}

I'm assigning the decorator in a simple loop after all routes are registered.

var routes = RouteTable.Routes;
for (var i = 0; i < routes.Count; i++)
{
    routes[i] = new RouteDecorator(routes[i]);
}

How can I safely insert a decorator without breaking routes and areas?

I have a reproduction solution available to download here. In the reproduction, the route decorator is commented out. Commenting it back in will break routing and the first dummy area's routing data will match the links that normally will correctly match only the corresponding namespace.

+3  A: 

I think it's down to the way areas use the DataTokens dictionary to store the area/namespace information. As you are inheriting from RouteBase you probably need to implement the IRouteWithArea interface too as you don't have the DataTokens that a Route has.

The ActionLink helper seems to indirectly call this hence the need for this new interface:

public static string GetAreaName(RouteBase route)
{
    IRouteWithArea area = route as IRouteWithArea;
    if (area != null)
    {
        return area.Area;
    }
    Route route2 = route as Route;
    if ((route2 != null) && (route2.DataTokens != null))
    {
        return (route2.DataTokens["area"] as string);
    }
    return null;
}

[Edit - 2009-11-12] I believe the following will fix the issue, as the decorator seems to end up wrapping the route more than once:

Additional property on decorator:

  public RouteBase InnerRoute
        {
            get
            {
                return _route;
            }
        }

Interface implementation:

  public string Area
        {
            get
            {

                RouteBase r = _route;
                while (r is RouteDecorator)
                    r = ((RouteDecorator) r).InnerRoute;
                string s = GetAreaToken(r);
                if (s!= null) return s;
                return null;
            }
        }

        private string GetAreaToken(RouteBase r)
        {
            var route = r as Route;
            if (route != null && route.DataTokens !=null && route.DataTokens.ContainsKey("area"))
            {
                return (route.DataTokens["area"] as string);
            }
            return null;
        }
    }
MVC-dot-net
Thanks for the effort. Implementing IRouteWithArea doesn't correct the problem carte blanche but it might take me in a new direction. Thank you.
Daniel Crenna
The decorator appears to end up wrapping the route more than once. I have edited my answer providing the code to implement a working solution, though I would ideally like to find out why this happens as it's rather strange.
MVC-dot-net
This is definitely the answer to the problem. I didn't catch the spawning decorator issue. This is some strange behavior, or, expected behavior with a lack of understanding why it happens.
Daniel Crenna
A: 

What happens if you decorate the Route class instead of RouteBase?

Think of something like this:

public class RouteDecorator: Route
{
    readonly Route _route;

    public RouteDecorator(Route route)
    {
        _route = route;
    }

    public override RouteData GetRouteData(HttpContextBase context)
    {
        return _route.GetRouteData(context);
    }

    public override VirtualPathData GetVirtualPath(RequestContext context, RouteValueDictionary values)
    {
        return _route.GetVirtualPath(context, values);
    }
}

I also recommend checking out System.Web.Routing.dll with Reflector, it might give you an insight of what is happening.

And also, what happens, if you do this:

var routes = RouteTable.Routes.ToList();
RouteTable.Routes.Clear();
//or, alternatively, if the above doesn't work:
//RouteTable.Routes = new RouteCollection();
foreach (var r in routes)
{
    RouteTable.Routes.Add(new RouteDecorator(r));
}

I hope very much that it helps.

Venemo
I can't use Route rather than RouteBase because RouteCollection is a collection of RouteBase. I have used Reflector and the MVC 2 source code to try to deduce the problem before posting it here. Your other suggestion (of clearing the collection?) has no effect.
Daniel Crenna