How to catch all areas with MapRoute

Viewed 1895

I am new to MVC and editing an existing application. Currently I see the following in RouteConfig.cs:

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

        routes.MapRoute(
            "Util",
            "util/{action}",
            new {controller = "util"});

        routes.MapRoute(
            "Catchall",
            "{*url}",
            new {controller = "Main", action = "CatchUrl"});
    }
}

Inside the Main controller there is logic on that basically does a RedirectToRoute and sets the area, controller, action, and querystring called location to a certain value.

public class MainController : Controller
{
    public ActionResult CatchUrl()
    {
        var agencyId = 9;

        var routeValues = new RouteValueDictionary
        {
            {"area", "area1"},
            {"controller", "dashboard"},
            {"action", "index"},
            {"location", "testLocation"}
        };

        return RedirectToRoute(routeValues );
    }
}

This seems to work fine, when you give it an invalid area it correctly goes to the default one.

I also see a file called CustomAreaRegistration.cs:

public abstract class CustomAreaRegistration : AreaRegistration
{
    public override void RegisterArea(AreaRegistrationContext context) 
    {
        context.MapRoute(
            AreaName + "Ajax",
            AreaName + "/{controller}/{action}",
            new { action = "Index" }
        );

        context.MapRoute(
            AreaName + "ShortUrl",
            AreaName + "/{controller}",
            new {action = "Index"}
            );
    }
}

I am having trouble understanding how the Area routing works and how it knows how to go to the correct controller.

Furthermore, I am trying to get it so that when you visit

/{area}/ it does some logic and redircts you to the correct controller. Similar to how CatchUrl works

My attempt:

    routes.MapRoute(
        "AreaIndex",
        "{module}/",
        new {controller = "Main", action = "Index"});

MainController :

public class MainController : Controller
{
    public ActionResult Index()
    {
        var requestHost = HttpContext.Request.Url?.Host;
        var location= requestHost == "localhost" ? Request.QueryString["location"] : requestHost?.Split('.')[0];


        var routeValues = new RouteValueDictionary
        {
            {"area", ControllerContext.RouteData.Values["module"]},
            {"controller", "dashboard"},
            {"action", "index"},
            {"location", location}
        };

        return RedirectToRoute(routeValues );
    }

    public ActionResult CatchUrl()
    {
        var routeValues = new RouteValueDictionary
        {
            {"area", "area1"},
            {"controller", "dashboard"},
            {"action", "index"},
            {"location", "testLocation"}
        };

        return RedirectToRoute(routeValues );
    }
}

And I get

No route in the route table matches the supplied values.

I am not sure why CatchUrl works and mine does not.

2 Answers

I actually don't get what you're asking, but by just looking at the code, that's not the standard way to create/use Areas in MVC 3,4 and 5.

You shouldn't need to write logics inside each controller and do the redirects.

In my RouteConfig, I usually just have the default route mapping. And when you have the needs for Areas, you can right click on the MVC web project in Visual Studio and click'Add -> Area'. That will create a folder with the area name inside an Areas folder right under the root of the web project. And within the area folder, you should find the AreaRegistration.cs for the area name and mappings.

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

        routes.MapRoute(
            name: "default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "dashboard", action = "index", id = UrlParameter.Optional },
            namespaces: new[] { "Company.Project.Web.UI.Controllers" }
        );
    }
}

And let's say you want to create an area called 'Admin':

public class AdminAreaRegistration : AreaRegistration 
{
    public override string AreaName 
    {
        get 
        {
            return "admin";
        }
    }

    public override void RegisterArea(AreaRegistrationContext context) 
    {
        context.MapRoute(
            "admin_default",
            "admin/{controller}/{action}/{id}",
            new { action = "index", id = UrlParameter.Optional },
            namespaces: new[] { "Company.Project.Web.UI.Areas.Admin.Controllers" }
        );
    }
}

Lastly, I think a screenshot might be helpful. enter image description here

-- updates --

Based on the comments, if you want the route /apple?location=home to go to Apple Controller and its Home method, while the route /orange?location=dashbard to go to Orange Controller and its Dashboard method, it's better to define a route in RouteConfig.

Ideally you wish you can have something like this in RouteConfig:

        routes.MapRoute(
            name: "Area-CatchAll",
            url: "{area}?location={controller}"
        );

But that's not possible as MVC will error out saying "The route URL cannot start with a '/' or '~' character and it cannot contain a '?' character.".

Instead, you can direct the traffic to a controller and you can define the area and location as parameters.

routes.MapRoute(
    name: "Area-CatchAll",
    url: "{area}",
    defaults: new { controller = "Area", action = "Translate" }
);

public class AreaController : Controller
{
    // The {area} from route template will be mapped to this area parameter.
    // The location query string will be mapped to this location parameter.

    public ActionResult Translate(string area, string location)
    {
        // This also assumes you have defined "index" method and
        // "dashboard" controller in each area.

        if (String.IsNullOrEmpty(location))
        {
            location = "dashboard";
        }
        return RedirectToAction("index", location, new { area = area });
    }
}

Again, I wouldn't create route like that to redirect traffic to areas, if I don't have to.

I am trying to get it so that when you visit

/{area}/ it does some logic and redircts you to the correct controller. Similar to how CatchUrl works

First of all, lets be clear about this. The catch-all route is not routing it is redirecting. It is issuing an HTTP 302 response to the browser, which tells the browser to lookup a new location on your server. This sort of thing is not very efficient because it requires an extra round trip across the network, but it is the only way to get the URL in the browser to change from the server side (if that is what you intend to do).

Routing, on the other hand, is taking the initial request and sending it directly to a specific resource (controller action) without the extra round trip across the network and without changing the URL in the browser.

Your routes are already setup to use

/Area/Controller

to route to the correct controller here:

public abstract class CustomAreaRegistration : AreaRegistration
{
    public override void RegisterArea(AreaRegistrationContext context) 
    {
        context.MapRoute(
            AreaName + "Ajax",
            AreaName + "/{controller}/{action}",
            new { action = "Index" }
        );

        context.MapRoute(
            AreaName + "ShortUrl",
            AreaName + "/{controller}",
            new {action = "Index"}
        );
    }
}

(assuming you have a subclass of this for each area that sets the AreaName).

If you indeed want to route (not redirect) the /module/ URL to the DashboardController.Index method in the corresponding Area, you change your ShortUrl to make the controller optional and default it to the DashboardController.

public abstract class CustomAreaRegistration : AreaRegistration
{
    public override void RegisterArea(AreaRegistrationContext context) 
    {
        context.MapRoute(
            AreaName + "Ajax",
            AreaName + "/{controller}/{action}",
            new { action = "Index" }
        );

        context.MapRoute(
            AreaName + "ShortUrl",
            AreaName + "/{controller}",
            new { controller = "Dashboard", action = "Index" }
        );
    }
}

This will send the URL /Foo/ to the Foo area's DashboardController.Index method, but send the URL /Foo/Bar/ to the Foo area's BarController.Index method.

The above assumes that you are diligent enough to ensure that none of your AreaNames are the same as controller names in the non-area part of your project. If they are, you will never be able to reach those non-area controllers without some extra configuration such as a route constraint.

The MainController.Index method isn't needed at all (unless you have some specific reason why you want to change the URL in the browser).

Related