THE CASE AGAINST NAMED ROUTES

Một phần của tài liệu Giáo trình lập trình ASP.NET Apress pro ASP NET MVC3 framework pre release (Trang 339 - 369)

The problem with relying on route names to generate outgoing URLs is that doing so breaks through the separation of concerns which is so central to the MVC design pattern. When generating a link or a URL in a view or action method, we want to focus on the action and controller that the user will be directed to, not the format of the URL that will be used – by bringing knowledge of the different routes into the views or controllers, we are creating dependencies that we would prefer to avoid. We tend to avoid naming our routes (by specifying

null for the route name parameter) and use code comments to remind ourselves of what each routes is intended to do.

Customizing the Routing System

You have seen how flexible and configurable the routing system is, but if it doesn’t meet your requirements there are two different points by which we can customize the behavior. In this section, we’ll show you each of them and provide a simple example.

Creating a Custom RouteBase Implementation

If you don’t like the way that standard Route objects match URLs, or want to implement something unusual, we can derive an alternative class from RouteBase. This gives us control over how URLs are matched, how parameters are extracted and how outgoing URLs are generated. To derive a class from RouteBase, we need to implement two methods:

GetRouteData(HttpContextBase httpContext): This is the mechanism by which inbound URL matching works—the framework calls this method on each RouteTable.Routes entry in turn, until one of them returns a non- null value.

GetVirtualPath(RequestContext requestContext, RouteValueDictionary values): This is the mechanism by which outbound URL generation works—the framework calls this method on each RouteTable.Routes entry in turn, until one of them returns a non-null value.

To demonstrate this kind of customization, we are going to create a RouteBase class which will handle legacy URL requests. Imagine that we have migrated an existing application to the MVC framework but that some users have bookmarked our pre-MVC URLs or hard- coded them into scripts. We still want to support those old URLs – we could handle this using the regular routing system, but this problem provides a nice example for this section.

To being with, we need to create a controller that we will send legacy requests to – we have called ours LegacyController and its contents are shown in Listing 11-42.

Listing 11-42. The LegacyController class using System.Web.Mvc;

namespace URLsAndRoutes.Controllers { public class LegacyController : Controller {

public ActionResult GetLegacyURL(string legacyURL) { return View((object)legacyURL);

} } }

This is a very simple controller – the GetLegacyURL action method takes the parameter and passes it as a view model to the view. If we were really implementing this controller, we would use this method to retrieve the files we were asked for, but as it is we are simply going to display the URL in a view.

Tip Notice that we have cast the parameter to the View method in Listing 11-41. One of the overloaded versions of the View method takes a string specifying the name of the view to render and without the cast, this is would be the version overload that the C# compiler thinks we want. To avoid this, we cast to object so that we call the version overload that passes a view model and uses the default view.

We could also have solved this by using the overload that takes both the view name and the view model, but we prefer not to make explicit associations between action methods and views if we can help it.

The view that we have associated with this action is called GetLegacyURL.cshtml and is shown in Listing 11-43.

Listing 11-43. The GetLegacyURL view

@model string

@{

ViewBag.Title = "GetLegacyURL";

Layout = null;

}

<h2>GetLegacyURL</h2>

The URL requested was: @Model

Once again, this is very simple – we want to demonstrate the custom route behavior, so we are not going to spend any time created complicated actions and views. We have now reached the point where we can create our derivation of RouteBase.

Routing Incoming URLs

We have created a class called LegacyRoute, which we put in a top-level folder called

Infrastructure (which is where we like to put support classes that don’t really belong anywhere else). The class is shown in Listing 11-44.

Listing 11-44. The LegacyRoute class using System;

using System.Linq;

using System.Web;

using System.Web.Mvc;

using System.Web.Routing;

namespace URLsAndRoutes.Infrastructure { public class LegacyRoute : RouteBase { private string[] urls;

public LegacyRoute(params string[] targetUrls) { urls = targetUrls;

}

public override RouteData GetRouteData(HttpContextBase httpContext) { RouteData result = null;

string requestedURL =

httpContext.Request.AppRelativeCurrentExecutionFilePath;

if (urls.Contains(requestedURL, StringComparer.OrdinalIgnoreCase)) { result = new RouteData(this, new MvcRouteHandler());

result.Values.Add("controller", "Legacy");

result.Values.Add("action", "GetLegacyURL");

result.Values.Add("legacyURL", requestedURL);

}

return result;

}

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {

return null;

} } }

This constructor of this class takes a string array which represents the individual URLs that this routing class will support – we’ll specify these when we register the route later. Of note

in this listing is the GetRouteData method, which is what the routing system calls to see if we can handle an incoming URL.

If we can’t handle the request, then we can just return null and the routing system will move on to the next route in the list and repeat the process. If we can handle the request, then we have to return an instance of the RouteData class containing the values for the controller and action variables and anything else we want to pass along to the action method.

When we create the RouteData object, we have to pass in the handler that we want to deal with the values we generate. We are going to use the standard MvcRouteHandler class, which is what assigns meaning to the controller and action values:

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

Later in the chapter, we’ll show you how to implement a replacement for

MvcRouteHandler, but for the vast majority of MVC applications, this is the class that you will require, since it connects the routing system to the controller/action model of an MVC

application.

In this routing implementation, we are willing to route any request for the URLs that were passed to our constructor – when we get such a URL, we add hardcoded values for the controller and action method to the RouteValues object. We also pass along the requested URL as the legacyURL property - notice that the name of this property matches the name of the parameter of our action method, ensuring that the value we generate here will be passed to the action method via the parameter.

The last step is to register a new route that uses our RouteBase derivation – you can see how to do this in Listing 11-45.

Listing 11-45. Registering the custom RouteBase implementation public static void RegisterRoutes(RouteCollection routes) { routes.Add(new LegacyRoute(

"~/articles/Windows_3.1_Overview.html", "~/old/.NET_1.0_Class_Library"));

routes.MapRoute("MyRoute", "{controller}/{action}/{id}",

new { controller = "Home", action = "Index", id = UrlParameter.Optional });

}

We create a new instance of our class and pass in the URLs we want it to route – we then add the object to the RouteCollection using the Add method. Now when we request one of the legacy URLs we defined, the request is routed by our custom class and directed towards our controller, as shown in Figure 11-812.

Figure 11-812. Routing requests using an custom RouteBase implementation

Generating Outgoing URLs

To support outgoing URL generation, we have to implement the GetVirtualPath method. Once again, if we are unable to deal with the request, we let the routing system know by returning null. Otherwise, we return an instance of the VirtualPathData class. Listing 11-46 shows our implementation.

Listing 11-46. Implementing the GetVirtualPath method

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {

VirtualPathData result = null;

if (values.ContainsKey("legacyURL") &&

urls.Contains((string)values["legacyURL"], StringComparer.OrdinalIgnoreCase)) { result = new VirtualPathData(this,

new UrlHelper(requestContext)

.Content((string)values["legacyURL"]).Substring(1));

}

return result;

}

We have been passing segment variables and other details around using anonymous types, but behind the scenes the routing system has been converting these into

RouteValueDictionary objects. So, for example, when we add something like this to a view:

@Html.ActionLink("Click me", "GetLegacyURL", new { legacyURL = "~/articles/Windows_3.1_Overview.html" })

the anonymous type created with the legacyURL property is converted into a

RouteValueDictionary class which contains a key of the same name. In this example, we decide we can deal with a request for an outbound URL if there is a key named legacyURL and if its

value is one of the URLs that was passed to the constructor – we could be more specific and check for controller and action values, but for a simple example this is sufficient.

If we get a match, then we create a new instance of VirtualPathData, passing in a reference to the current object and the outbound URL. We have used the Content method of the UrlHelper class to convert the application-relative URL to one that can be passed to browsers – unfortunately, the routing system prepends an additional / to the URL, so we have to take care to remove the leading character from our generated URL.

Creating a Custom Route Handler

We have relied on the MvcRouteHandler in our routes because it connects the routing system to the MVC framework. And, since our focus is MVC, this is what we want pretty much all of the time. Even so, the routing system lets us define our own route handler by implementing the IRouteHandler interface. Listing 11-47 provides a demonstration.

Listing 11-47. Implementing the IRouteHandler interface using System.Web;

using System.Web.Routing;

namespace URLsAndRoutes.Infrastructure {

public class CustomRouteHandler : IRouteHandler {

public IHttpHandler GetHttpHandler(RequestContext requestContext) { return new CustomHttpHandler();

} }

public class CustomHttpHandler : IHttpHandler { public bool IsReusable {

get { return false; } }

public void ProcessRequest(HttpContext context) { context.Response.Write("Hello");

} } }

The purpose of the IRouteHandler interface is to provide a means to generate

implementations of the IHttpHandler interface, which is responsible for processing requests. In the MVC implementation of these interfaces, controllers are found, action methods are invoked, views are rendered and the results are written to the response. Our implementation is a little

simpler – it just writes the word Hello to the client. Not an HTML document containing that word you understand – just the text. We can register our custom handler when we define a route, as shown in Listing 11-48.

Listing 11-48. Using a custom routing handler in a route public static void RegisterRoutes(RouteCollection routes) {

routes.Add(new Route("SayHello", new CustomRouteHandler()));

routes.MapRoute("MyRoute", "{controller}/{action}/{id}",

new { controller = "Home", action = "Index", id = UrlParameter.Optional });

}

When we request the URL /SayHello, our handler is used to process the request, the result of which is shown in Figure 11-913.

Figure 11-913. Using a custom request handler.

Implementing custom route handling means taking on responsibility for functions that are usually handled for us – such as controller and action resolution. But it does give us incredible freedom – we can co-opt some parts of the MVC framework and ignore others, or even implement an entirely new architectural pattern.

Working with Areas

The MVC framework supports organizing a web application into areas, where each area represents a functional segment of the application – for example, administration, billing, customer support, and so on. This is useful in a large project, where having a single set of folders for all of the controllers, views and models can become difficult to manage.

Each MVC area is has its own folder structure, allowing us to keep everything separate – this makes it more obvious which project elements relate to each functional area of the application and helps multiple developers to work on the project without colliding with one another.

Areas are supported largely through the routing system, which is why we have chosen to cover this feature alongside our coverage of URLs and routes. In this section, we’ll show you how to set up and use areas in your MVC project.

Note We created a new MVC project for this part of the chapter – we used the Internet Application template and called the project WorkingWithAreas.

Creating an Area

To add an area to an MVC application, right click on the project item in the Solution Explorer window and select AddArea. Visual Studio will prompt you for the name of the area, as shown in Figure 11-108.

Figure 11-108. Adding an area to an MVC application

In this case, we have created an area called Admin – this is a pretty common thing to do, because many web applications need to separate out the customer-facing and administration functions. Press the Add button to create the area and you’ll see some changes applied to the project.

First of all, the project contains a new top-level folder called Areas. This contains a folder called Admin that represents the area that we just created. If we were to create additional areas, further folders would be created here.

Inside of the Areas/Admin folder you will see that we have a mini-MVC project – there are folders called Controllers, Models and Views – the first two are empty, but the Views folder contains a Shared folder (and a Web.config file that configures the view engine, but we are not interested in that right now).

The other change is that there is a file called AdminAreaRegistration.cs, which contains the AdminAreaRegistration class, shown in Listing 11-49.

Listing 11-49. The AdminAreaRegistration class using System.Web.Mvc;

namespace WorkingWithAreas.Areas.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 } );

} } }

The interesting part of this class is the RegisterArea method – as you can see from the listing, this method registers a route with the URL pattern Admin/{controller}/{action}/{id}. We can define additional routes in this method, which will be unique to this area.

Tip If you assign names to your routes, you must ensure that they are unique across the entire application and not just the area for which they are intended.

We don’t have to take any action to make sure that this registration method is called – it is handled for us automatically by the Application_Start method of Global.asax, which you can see in Listing 11-50.

Listing 11-50. Area registration called from Global.asax protected void Application_Start() {

AreaRegistration.RegisterAllAreas();

RegisterGlobalFilters(GlobalFilters.Filters);

RegisterRoutes(RouteTable.Routes);

}

The call to the static AreaRegistration.RegisterAllAreas method causes the MVC framework to go through all of the classes in our application, find those which are derived from the AreaRegistration class and call the RegisterArea method on each of them.

Caution Don’t change the order of the statements related to routing in the Application_Start method – if you call RegisterRoutes before AreaRegistration.RegisterAllAreas is called, then your routes will be defined before the area routes. Given that routes are evaluated in order, this will mean that requests for area controllers are likely to be matched against the wrong routes.

The AreaRegistrationContext class that is passed to the each area’s RegisterArea class method contains exposes a set of MapRoute methods which are used tothe area can use to register routes in the same as we wouldyour main application does in the RegisterRoutes method of Global.asax.

Tip The MapRoute methods in the AreaRegistrationContext class automatically limit the routes we register to the namespace that contains the controllers for the area. This means that when you create a controller in an area, you must not change theleave it in its default namespace - otherwise the routing system won’t be able to find it.

Populating an Area

We can create controllers, views and models in an area just as we have done previous. For example, to create a controller, right click on the Controllers folder within the area and select AddController from the popup menu. The Add Controller dialog box will be displayed, allowing us to enter the name for our new controller class, as shown in Figure 11-911.

Figure 11-119. Adding a controller to an area

Pressing Add creates an Empty controller, as shown in Listing 11-51. We have called our class HomeController so we can demonstrate the separation between areas in an application.

Listing 11-51. A controller created inside an MVC area using System.Web.Mvc;

namespace WorkingWithAreas.Areas.Admin.Controllers { public class HomeController : Controller {

public ActionResult Index() { return View();

} } }

To complete this simple example, we can create a view by right clicking inside the Index action method and selecting Add View from the pop-up menu. We accepted the default name for our view (Index) – when you create the view, you will see that it appears in the Areas/Admin/Views/Home folder. The view we created is shown in Listing 11-52.

Listing 11-52. A simple view for an area controller

@{

ViewBag.Title = "Index";

}

<h2>Admin Area Index</h2>

The point of all of this is to show that working inside an area is pretty much the same as working in the main part of an MVC project. We have seen that the workflow for creating project items is the same and we have created a controller and view which share their names with counterparts in the main part of the project. If you start your application and navigate to /Admin/Home/Index you will see the view that we created, as illustrated by Figure 11-120.

Figure 11-1012. Rendering an area view

Resolving the Ambiguous Controller Issue

OK – so we lied slightly. In the previous sectionexample, when you started the applicationif you had navigated to the application’s root URL, you would have seen an error, similar to the one shown in Figure 11-1113.

Figure 11-1113. The ambiguous controller error

When an area is registered, any routes that we define are limited to the namespace associated with the area – this is how we were able to request /Admin/Home/Index and get the HomeController class in the WorkingWithAreas.Areas.Admin.Controllers namespace.

However, routes defined in the RegisterRoutes method of Global.asax are not similarly restricted. You can see the default routing configuration that Visual Studio puts in place in Listing 11-53.

Listing 11-53. The default MVC project routing configuration public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(

"Default", // Route name

"{controller}/{action}/{id}", // URL with parameters

new { controller = "Home", action = "Index", id = UrlParameter.Optional } );

}

The route named Default translates the incoming URL from the browser to the Index action on the Home controller – at which point we get an error because there are no namespace restrictions in place for this route and the MVC framework can see two HomeController classes.

To resolve this, we need to prioritize the main controller namespace in Global.asax, as shown in Listing 11-54.

Listing 11-54. Resolving the area namespace conflict public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(

"Default", // Route name

"{controller}/{action}/{id}", // URL with parameters

new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] {"WorkingWithAreas.Controllers"}

);

}

This change ensures that the controllers in the main project are given priority in resolving requests – of course, if you want to give primary preference to the controllers in an area, you can do that instead.

Generating Links to Actions in Areas

You don’t need to take any special steps to create links that refer to actions in the same MVC area that the user is already on. The MVC framework detects from that the current request is an area is being usedrelates to a particular area, and will ensure that the routes defined in the area registration are used to generate the URLthen outbound URL generation will find a match only among routes defined for that area. For example, this addition to the view in our Admin area:

@Html.ActionLink("Click me", "About")

generates the following HTML:

<a href="/Admin/Home/About">Click me</a>

To create a link to an action in another a different area, or no area at all, we have to create a variable called area and use it to specify the name of the area we want, like this:

@Html.ActionLink("Click me to go to another area", "Index", new { area = "Support" })

It is for this reason that area is reserved from use as a segment variable name. The HTML generated by this call is as follows (assuming that you created an area called Support which has the standard route defined):

Một phần của tài liệu Giáo trình lập trình ASP.NET Apress pro ASP NET MVC3 framework pre release (Trang 339 - 369)

Tải bản đầy đủ (PDF)

(603 trang)