1

Here's the problem that I have been tearing hair out over since Friday.

I have a single MVC application that serves several different subgroups for a single client. For branding and for some style elements, the Url's need to be formatted like: www.site.com/Login www.site.com/Client1/Login www.site.com/Client2/Login www.site.com/Client3/Login and so on.

We would also like to maintain this structure, moving onto www.site.com/Client1/News, etc.

Static routing is off the table. Even a tool to generate them. The site will have X pages with a unique route for Y clients, and I shudder to think about the performance. And because of teh dynamic nature, creating virtual dirs on the fly is not a route I want to travel down.

At first the solution seemed trivial. I tried two test solutions.

The first derived from CustomRouteBase and was able to determine the correct route in the overridden GetRouteData method and then generate the correct Url using GetVirtualPath. The second solution used constraints to see if a client was in the pattern and route accordingly. Both would hit the correct controllers and generate correct links.

Then I added areas (this is just a prototype, but the real site uses areas).

Both solutions failed. The areas were registered properly and worked as they typically should. But with the first solution, I could not find a way to override GetVirtualPath for area registration. I know there is an extension methods off the Area class, but this doesn't fit what I need.

I also tried using a constraint, but the "client" part of the Url was not being added to any of the action links to the areas and trying to route to a controller using a constraint gave me the good old view not found error (searching from the root even though I had specified the area in the route).

So my first question is am I going about this the wrong way? If there is a better way to accomplish this, I am all eras. If not, then how do I manage areas?

I could put some code up here, but it all works for what I want it to do. I'm sort of lost at how to approach the area issue. But unfortunately as always I inherited this project 3 months before launch and my team simply doesn't have the resources to restructure the site without them.

@Max... I tried something similar, but thh areas still would not display their links correctly when in www.site.com/Client1... This is from prototype 6, I tried a few different ways.

     public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        //throws view engine can't find view error. If I use www.website.com/Client1, hvering over area links does not give the correct path
        routes.MapRoute("Area",
           "{folder}/{area}/{controller}/{action}/{id}",
            new { area = "Authentication", controller = "Authentication", action = "Index", id = UrlParameter.Optional },
            new { folder = new IsFolderContsraint(), area = new IsArea() }
        );

        //works fine.. if I use www.website.com/Client1, hovering over regular (non area) links works fine
        routes.MapRoute("Branded",
        "{folder}/{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = UrlParameter.Optional },
        new { folder = new IsFolderContsraint() }
        );

        routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
            new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
        );
    }

Here is another way I tried to tackle it. Please don't crack on the implementation, it's just a POC. The problem here was that Areas are not part of RouteBase, so I can't modify the virtual paths for them. So every action link, etc, get's rendered correctly and all works well as long as the action is not in an area.

public class Folders
{
    private static Dictionary<string, string> _folders= new Dictionary<string, string>()
                                                           {   {"test1", "style1"},
                                                               {"test2", "style2"},
                                                               {"test3", "style3"}
                                                            };

    public static Dictionary<string, string> FolderNames { get { return _folders; } }

}

public class AreaDefinitions
{
    private static Dictionary<string, string> _areas = new Dictionary<string, string>() 
    {  {"Authentication", "Authentication"} };

    public static Dictionary<string, string> AreaDefinition { get { return _areas; } }
}


public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
    }

    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.Add(new CustomRouteBase());

        routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
            new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
        );

    }

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterRoutes(RouteTable.Routes);
    }
}


public class CustomRouteBase : RouteBase
{
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        //Creates routes based on current app execution pass
        //a bit like doing my own routing constraints, but is more dynamic.
        //get route data (and CreateVirtualPath) will be called for any action needing to be rendered in the current view.
        //But not for areas :( Ugly but just a prototype

        string url = httpContext.Request.AppRelativeCurrentExecutionFilePath;
        url = url.StartsWith("~/") ? url.Substring(2, url.Length - 2) : url;
        string[] urlParts = url.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);

        RouteData rd = new RouteData(this, new MvcRouteHandler());

        if (urlParts.Length == 0)
        {
            rd.Values.Add("controller", urlParts.Length > 0 ? urlParts[0] : "Home");
            rd.Values.Add("action", urlParts.Length > 1 ? urlParts[1] : "Index");
            return rd;
        }

        if (Folders.FolderNames.ContainsKey(urlParts[0]))
        {
            if (urlParts.Length > 1 && AreaDefinitions.AreaDefinition.ContainsKey(urlParts[1]))
            {
                rd.DataTokens["area"] = urlParts[1];
                rd.Values.Add("controller", urlParts.Length > 2 ? urlParts[2] : "Home");
                rd.Values.Add("action", urlParts.Length > 3 ? urlParts[3] : "Index");
            }
            else
            {
                rd.Values.Add("controller", urlParts.Length > 1 ? urlParts[1] : "Home");
                rd.Values.Add("action", urlParts.Length > 2 ? urlParts[2] : "Index");

            }
            rd.DataTokens.Add("folder", urlParts[0]);
        }
        else
        {
            rd.Values.Add("controller", urlParts.Length > 0 ? urlParts[0] : "Home");
            rd.Values.Add("action", urlParts.Length > 1 ? urlParts[1] : "Index");

        }
        return rd;

    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        //Assembled virtial path here using route and values.
        //Worked (ugly but functioned, but was never called for areas.) 
        string url = "";
        if (requestContext.RouteData.DataTokens.ContainsKey("folder"))
            url += requestContext.RouteData.DataTokens["folder"] + "/";
        if (values.ContainsKey("controller"))
            url += values["controller"] + "/";
        if (values.ContainsKey("action"))
            url += values["action"];

        var vpd = new VirtualPathData(requestContext.RouteData.Route, url);
        return vpd;
    }
}

Thanks Liam... that's similar to how our view engine is customized, to read from a "plugins" folder first for view overrides. But the problem is a little different here. This is one client that already has a site with view overrides, but in turn has multiple clients of their own. The behavior here is just to control style sheets, logos, etc. After a user is logged in, I can identify what the style and branding should be, but for landing pages the client wants to use a(n) url like "www.site.com/Client1" to identify this. I've given up and written a handler that just turns the request into www.site.com/?client=client1 so I can handle landing page styling, but it would be so much nicer to leave the url as www.ste.com/Client1/login, etc. This is a conversion from a classic asp app that used vdirs to host different directories prior to this. Because of the number of potential clients (100's), static routing gets heavy. My solutions all work to a point... it's the areas that are causing all the problems. If I could just find a way to remap their virtual paths dynamically like I can with the routes I create in RouteBase, I would be in business... I think.

4

0 回答 0