ASP.NET MVC URL、路由、与区域 Part2

感谢作者的翻译,这里只是译文。 原书名:Pro ASP.NET MVC 3 Framework

 

第十一章 URL、路由、与区域 PART2

 

生成输出URL

处理输入URL只是故事的一部分。我们还需要能够把我们的URL Schema 用来生成嵌入到我们视图的输出URL。这样,用户点击链接、以及把表单回递给应用程序,都能以正确的控制器和动作为目标。在本节中,我们将向你演示生成输出URL的不同技术。

WHAT NOT TO DO: MANUALLY DEFINEDURLS
不要做:手工定义的URL 

------------------------------------------------------------------------------------------------------------------------------------------------- 

定义输出URL最快、且最容易的方式是手工做这件事。例如,根据清单11-28所定义的简单方案,我们可以把以下链接添加到我们的一个视图:

< href ="/Home/About" >About this application </ a >

这个HTML元素以一个URL生成了一个链接,当它被我们的应用程序作为输入URL接收时,它将以Home控制器的About动作方法为目标。

手工定义URL快而简单,但它们也有很大的危险。每一次你修改应用程序的URL方案时,你都要放弃所有输出URL。然后,你必须拉网式地检查应用程序的所有视图,并更新所有控制器和动作方法的引用。正如我们在后面几节所解释的,可以用路由系统根据URL方案来生成URL,这样,当方案变化时,你视图中的输出URL也会随之而变。这是一种有意义得多的办法,它只需要一点点初期的投入,但却有长期的巨大好处。 

准备项目

我们打算继续使用本章前面的项目,但借此机会删掉一些示例。我们已经修改了Global.asax中的RegisterRoutes方法,使之如清单11-30所示,并删掉了AdditionalControllers项目。

Listing 11-30. The Tidied Up RegisterRoutes Method

public  static  void RegisterRoutes(RouteCollection routes) {
    routes.MapRoute( " MyRoute ", " {controller}/{action}/{id} ",
         new { controller =  " Home ", action = " Index ", id = UrlParameter.Optional });
}

在视图中生成输出(Outgoing)URL

在一个视图中生成一个输出(outgoing)URL最简单的办法是,在视图中调用@Html.ActionLink方法,如清单11-31所示。

Listing 11-31. Calling the ActionLink Helper Method

@Html.ActionLink( " About this application "" About ")

传递给ActionLink方法的参数是,链接文本、以及该链接的目标动作名称。ActionLink方法所生成的HTML是基于当前路由方案的。例如,运用清单11-30所定义的方案(并假设该视图是通过对Home控制器的请求来渲染的),我们得到这样的HTML

< href ="/Home/About" >About this application </ a >

但是,假设我们通过添加一条新路由修改了这个方案,像这样:

public  static  void RegisterRoutes(RouteCollection routes) {
    routes.MapRoute( " NewRoute ", " App/Do{action} ",
         new {controller =  " Home " });
    routes.MapRoute( " MyRoute ", " {controller}/{action}/{id} ",
         new { controller =  " Home ", action = " Index ", id = UrlParameter.Optional });
}

那么,当我们渲染这个页面时,我们从ActionLink辅助方法得到的是以下HTML

< href ="/App/DoAbout" >About this application </ a >

你可以看出针对这种维护问题来生成链接的方式,我们可以修改我们的路由Schema,并在我们的视图中自动地获得反映这种修改的输出连接。

UNDERSTANDING OUTBOUND URL ROUTEMATCHING
理解出站URL的路由匹配

-------------------------------------------------------------------------------------------------------------------------------------------------

你已经看到了,改变定义URL方案的路由,会改变生成输出URL的方式。应用程序通常会定义几条路由,而重要的只是理解生成URL的路由是如何选择的。路由系统是按照传递给RegisterRoutes方法的RouteCollection对象中的路由顺序来处理路由的。每一条路由都会被检测,以考查它是否是一个匹配,这需要满足三个条件:

·         URL模式中所定义的每个片段变量,必须有一个可用的值。为了找到每个片段的值,路由系统首先查看我们已经提供的值(用匿名类型的属性),然后是当前请求的变量值,最后是该路由中定义的默认值。(本章稍后我们会再次回到这些值的话题。)

·         在我们给片段变量提供的值中,应当没有值会违背这条路由所定义的只默认变量。这些已经提供了默认值、但不在URL模式中出现的变量(指只默认变量 —译者注)。例如,在以下路由定义中,myVar就是一个只默认变量:(原文:None of the values we provided for the segment variables may disagree with the default-only variables defined in the route.These are variables for which default values have been provided, but which do not occur in the URL pattern. For example, in this route definition, myVar is a default-only variable:

routes.MapRoute( " MyRoute ", " {controller}/{action} ",
     new { myVar = "true" });

为了匹配这条路由,我们必须小心不要给myVar提供一个值,或者,要确保我们肯定提供了与这个默认值匹配的值。

·         所有片段变量的值必须满足路由约束

要十分清楚地意识到:路由系统不会试图查找最佳匹配的路由。它只找出最先匹配,此时,它用这条路由生成URL,任何后继的路由都被忽略。出于这一原因,你应该首先定义最明确的路由。测试你生成的出站URL重要的 。如果你试图生成一个无法找到匹配路由的URL,你将会生成一个含有空href属性的链接,像这样:

< href ="" >About this application </ a >

这个链接会被渲染到视图中,但在用户点击它时没有目标功能。如果你恰好生成了这种URL(稍后我们演示如何生成),那么,其结果将为null,它在视图中渲染成空字符串。你可以通过使用命名路由的办法对路由匹配施加一些控制。参见本章稍后的生成特定路由的URL”小节。 

满足这些条件的第一条Route对象将产生一个非空的URL,并终止URL生成过程。所选的参数值将替换各个片段参数,忽略任何后继的默认值。如果你明确地提供了一些参数,它们与片段参数或默认参数不相符,那么,该方法将把它们以名字/对的查询字串形式进行追加。

单元测试:测试输出URL

测试输出URL生成最简单的办法是使用静态的UrlHelper.GenerateUrl方法,它含有指导路由生成的各种办法的参数例如,通过指定路由名、控制器、动作、片段值等等。以下是根据清单11-29所定义的路由,来检验URL生成的一个测试方法。

[TestMethod]
public  void TestOutgoingRoutes() {
     //  Arrange
    RouteCollection routes =  new RouteCollection();
    MvcApplication.RegisterRoutes(routes);
    RequestContext context =  new RequestContext(CreateHttpContext(),  new RouteData());
     //  Act - generate the URL
     string result = UrlHelper.GenerateUrl( null, " Index "" Home "null,
    routes, context,  true);
     //  Assert
    Assert.AreEqual( " / ", result);
}

我们生成了一个URL,而不是一个链接,因此我们不必考虑HTML的相关测试。UrlHelper.GenerateUrl方法需要一个RequestContext对象,它是我们用CreateHttpContext测试辅助方法模仿HttpContextBase对象生成的。

以其它控制器为目标

ActionLink方法的默认版本是:假设你希望以引发视图渲染的同一个控制器中的动作方法为目标。为了生成一个以不同控制器为目标的输出URL,你可以用它的一个重载方法,它允许你指定控制器名,如清单11-32所示。

Listing 11-32. Targeting a Different Controller Using the ActionLink Helper Method

@Html.ActionLink( " About thisapplication "" About "" MyController ")

When you render the view, you will see the following HTML generated:
当你渲染该视图时,你将看到生成了以下HTML

< href ="/MyController/About" >About this application </ a >

注意:路由系统在生成输出URL时,对我们应用程序的了解并不比处理输入请求时多。这意味着,不会对你提供给动作方法和控制器的值进行校验,因此,你必须小心不要指定不存在的目标。

传递额外的值

你可以用一个匿名类型把值传递给片段变量,在匿名类型中以属性表示片段。清单11-33提供了一个示例。

Listing 11-33. Supplying Values for Segment Variables

@Html.ActionLink( " About this application "" About "new { id =  " MyID " })

上例中,我们给名为id的片段变量提供了一个值。如果我们的应用程序使用清单11-30所示的路由,那么,在渲染视图时,我们便得到了以下HTML

< href ="/Home/About/MyID" >About this application </ a >

注意,我们所提供的值已经被添加为一个URL片段,以匹配我们应用程序路由的URL模式。

UNDERSTANDING SEGMENT VARIABLE REUSE
理解片段变量重用

--------------------------------------------------------------------------------------------------------------------------------------------------在我们描述对出站URL进行路由匹配的方式时,我们提到,在为一条路由的URL模式中的每个片段变量查找值的过程中,路由系统将考查当前请求的值。这是一种让许多程序员迷惑的行为,并可能导致较多的调试。

假设我们的应用程序只有一条路由,如下所示:

routes.MapRoute( " MyRoute ", " {controller}/{action}/{color}/{page} ");

现在,假设一个用户当前所处的URL/Catalog/List/Purple/123,而且,我们渲染了一个如下所示的连接:

@Html.ActionLink( " Click me ", " List "" Catalog "new {page= 789},  null)

你可能期望路由系统不能匹配这条路由,因为我们还没有给颜色片段变量提供值,并且也没有定义默认值。然而你错了。路由系统将根据我们已经定义的这条路由进行匹配。它将生成以下HTML

< ahref ="/Catalog/List/Purple/789" >Click me </ a >

路由系统是渴望形成一条路由匹配的,在这个意义上,它将重用输入URL的片段变量值。在这种情况下,根据我们假想的用户开始的这条URL,最终color变量的值为Purple

这不是最终采取的行为。路由系统将运用这一技术作为它常规评估路由的一部分,即使后面可能有匹配的路由,它们不需要重用当前请求的值。路由系统将只对某些片段变量使用重用值,这些片段变量在URL模式中的出现早于提供给Html.CactionLink方法的参数。假设我们试图生成一个像这样的连接:

@Html.ActionLink( " Click me ", " List "" Catalog "new {color= " Aqua "},  null)

我们已经给color提供了一个值,但对page没提供。但在这个URL模式中,color的出现早于page,因此,路由系统不会重用输入URL的值,于是这条路由将不匹配。

处理上面这种行为最好的办法是阻止类似情况的发生。我们强烈建议你不要依赖这种行为,并建议你为URL模式中的所有片段变量都提供值。依靠上面行为将不仅使你的代码更难于阅读,也会使你最终要设想用户形成请求的顺序,这会最终在你的应用程序进入维护期时,对你造成伤害。

当我们给属性提供的值与片段变量不一致时,这些值将作为查询字串被追加到输出URL。例如,考虑以下对ActionLink辅助方法的调用:

@Html.ActionLink( " About this application "" About ",
     new { id=  " MyID ", myVariable =  " MyValue " })

它会生成以下HTML

< ahref ="/Home/About/MyID?myVariable=MyValue" >About this application </ a >

如果我们给一个变量提供的一个值碰巧匹配我们在路由中指定的默认值,那么,路由系统在输出URL中会忽略这个值。例如,考虑以下对ActionLink方法的调用:

@Html.ActionLink( " About this application "" Index "" Home ")

我们已经为动作方法和控制器传递了参数,它们匹配清单11-27路由的默认值。调用该辅助方法所生成的HTML如下:

< href ="/" >About this application </ a >

再次引用清单:

Listing 11-27. A Route Whose URL Pattern Corresponds to a Disk File

public  static  void RegisterRoutes(RouteCollection routes) {
    routes.RouteExistingFiles =  true;
    routes.MapRoute( " DiskFile ", " Content/StaticContent.html ",
         new {
            controller=  " Account ", action =  " LogOn ",
            },
             new {
            customConstraint=  new UserAgentConstraint( " IE ")
        });
    routes.MapRoute( " MyRoute ", " {controller}/{action}/{id}/{*catchall} ",
         new { controller =  " Home ", action = " Index ", id = UrlParameter.Optional },
         new {
            controller =  " ^H.* ", action = " Index|About ",
            httpMethod =  new HttpMethodConstraint( " GET ", " POST "),
            customConstraint = newUserAgentConstraint( " IE ")
        },
         new[] {  " URLsAndRoutes.Controllers "});

路由系统忽略默认值以生成带有最少片段的出站URL,当点击它并使之再次成为输入URL时,它匹配同样的路由。

指定HTML属性

我们已经关注了ActionLink辅助方法生成的URL,但要记住,该方法可以生成一个完整的HTML锚点(<a>)元素。我们可以为该元素设置标签属性,通过提供一个匿名类型,它的属性对应于我们所需要的标签属性。清单11-34提供了一个演示,它设置了id属性,并把一个CSSclass赋给了这个HTML元素。

Listing 11-34. Generating an Anchor Element with Attributes

@Html.ActionLink( " About this application "" Index "" Home "null,
     new {id=  " myAnchorID ", @class =  " myCSSClass "})

我们已经生成了一个新的匿名类型,它具有idclass属性,并把它作为参数传递给ActionLink方法。我们给其余片段变量值传递了null,表示我们没有提供任何值。

提示:注意,我们以一个@字符作为class属性的前缀。这是一条C#语言特性,它让我们使用保留的关键词作为class成员的名字

当这个对ActionLink的调用被渲染时(使用清单11-27匹配的路由默认值),我们得到以下HTML

< class ="myCSSClass" href ="/"  id ="myAnchorID" >About this application </ a >

在链接中生成全限定URL

到目前为止,我们生成的链接含有相对URL,但我们也可以使用ActionLink辅助方法来生成全限定URL,如清单11-35所示。

Listing 11-35. Generating a Fully Qualified URL

 @Html.ActionLink(

    "About this application"

    "Index"

    "Home",
    "https",

    "myserver.mydomain.com"

    "myFragmentName",
    new { id= "MyId"},
    new { id= "myAnchorID", @class = "myCSSClass"

})

这是带有最多参数的ActionLink重载方法:

Html.ActionLink( " linkText ", " actionName ", " controlName ", " protocol ", " hostName ", " fragment ",routeValues,htmlAttributes)

相应的其他重载方法:

 Html.ActionLink( " linkText ", " actionName ")
 Html.ActionLink( " linkText ", " actionName ", " controlName ")
 Html.ActionLik( " linkText ", " actionName ",routeValues)
 Html.ActionLink( " linkText ", " actionName ",routeValues,htmlAttributes)
 Html.ActionLink( " linkText ", " actionName ", " controlName ",routeValues,htmlAttributes)
 Html.ActionLink( " linkText ", " actionName ", " controlName ", " protocol ", " hostName ", " fragment ",routeValues,htmlAttributes)

它允许我们1.提供协议值(在我们的例子中是https)、2.目标服务器名(myserver.mydomain.com)、和3.URL片段(myFragmentName)、以及你之前看到的所有其它选项(4.路由属性,5.HTML属性)。当在视图中渲染时,上述清单的调用会生成以下HTML

< class ="myCSSClass"  href ="https://myserver.mydomain.com/Home/Index/MyId#myFragmentName"
id
="myAnchorID" >About this application </ a >

我们建议尽可能用相对URL。全限定URL生成依赖于你的应用程序基础架构呈现给你的用户的方式。我们曾看到过许多依赖于绝对URL的大型应用程序,由于对网络基础架构或域名策略不一致的改变(这些通常超出了程序员的控制)而最终衰败的例子。

生成URL(而不是链接)

Html.ActionLink辅助方法生成完整的HTML<a>元素,这是我们大多数时候想要做的。然而,也有些时候,我们只需要一个URL,这可能是因为我们想要显示一条URL、手工为一个链接建立HTML、显示一条URL的值、或是把一个URL作为一个要被渲染的HTML页面的数据元素。

在这种情况下,我们可以用Url.Action方法来生成URL而不是围绕HTML,如清单11-36所示。

Listing 11-36. Generating a URL Without the Surrounding HTML

...
My URL  is: @Url.Action( " Index ", " Home "new { id =  " MyId " })
...

Url.Action方法除了它只生成URL之外,它的工作方式与Html.ActionLink方法相同。这两种方法的重载方法以及它们所接受的参数都相同,而且你可以用Url.Action完成我们在上一小节用Html.ActionLink所演示的所有示例。清单11-36在视图中渲染时的生成如下:

My URL is: /Home/Index/MyId

根据路由数据生成链接和URL

正如我们前面提到的,路由系统在处理URL时,并不把任何特定的含义赋给controlleraction片段变量。其含义是由MVC框架附加上去的,这样路由系统才可以更广泛地用于其它各类的ASP.NET应用程序。

有时,把controlleraction像其它变量一样处理,并通过提供一组名字/对的方式生成连接或URL是有用的。我们可以通过使用不是MVC专用的辅助方法来完成这种事,如清单11-37所示。

Listing 11-37. Generating a Link Using an Anonymous Type

@Html.RouteLink( " Routed Link "new {controller =  " Home ", action =  " About ",id= " MyID "})

RouteLink方法没有参数来表示controlleraction的值。我们必须把它们作为属性包含在匿名类型中。当上述清单在视图中渲染时,该调用生成以下HTML

< href ="/Home/About/MyID" >RoutedLink </ a >

我们可以用Url.RouteUrl辅助方法来只生成URL,如清单11-38所示。

Listing 11-38. Generating a URL Using an Anonymous Type

@Url.RouteUrl( new { controller =  " Home ",action =  " About ", id =  " MyID " })

这些方法很少使用,因为我们通常知道并希望明确地指定控制器和动作方法的值。但知道有这些方法存在、并知道在你需要的时候,它们可以使工作更加简单(它们或许不经常使用)。这对你来说这是很有用的。

在动作方法中生成输出URL

大多数地,我们希望在视图中生成输出URL,但有时候,我们希望在一个动作方法中做类似的事情。如果我们只需要生成一个URL,我们可以使用在视图中所用的同样的辅助方法,如清单11-39所示。

Listing 11-39. Generating an Outgoing URL in an Action Method

public ViewResult MyActionMethod() {
    stringmyActionUrl = Url.Action( " Index "new { id =  " MyID " });
    stringmyRouteUrl = Url.RouteUrl( new { controller =  " Home ", action = " Index " });
    ... dosomething with URLs...
}

一个更经常的需求是将客户端浏览器重定向到另一个URL。我们可以借助于返回RedirectToAction方法的调用结果来做这件事,如清单11-40所示。

Listing 11-40.Redirecting to Another Action

public ActionResult MyActionMethod() {
    returnRedirectToAction( " Index ");
}

RedirectToAction方法的结果是一个RedirectToRouteResult,它指示MVC框架把一条重定向指令发布给一个URL,由这个URL调用指定的动作。RedirectToAction方法有一些常用的重载版本,它们给这个URL的片段变量指定控制器和值。

如果你想用一个URL发送一个重定向,这条URL只是根据对象的属性生成的,你可以用RedirectToRoute方法,如清单11-41所示。

Listing 11-41.Redirecting to a URL Generated from Properties in an Anonymous Type

public ActionResult MyOtherActionMethod() {
     return RedirectToRoute( new { controller = " Home ", action =  " Index ", id =  " MyID " });
}

这个方法也返回一个RedirectToRouteResult对象,并与调用RedirectToAction方法具有相同的效果。

根据特定的路由生成URL

我们本章所定义的每个路由都指定了一个名字。例如,当我们像这样定义路由时:

routes.MapRoute( " MyRoute ", " {controller}/{action} ");
routes.MapRoute( " MyOtherRoute ", " App/{action} "new { controller =  " Home " });

我们所生成的路由所赋予的名字是作为MapRoute方法的第一个参数传递的上例的MyRouteMyOtherRoute

命名路由有两个原因:

·         作为路由目的的一种提醒

·         这样你可以选择特定的路由,以用来生成一个输出URL

我们调整了路由顺序,以使得最不特殊的路由首先出现在路由表中。意即,如果我们像以下这样用ActionLink方法来生成一条连接:

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

那么,输出路由将总是用MyRoute来生成。

你可以用Html.RouteLink方法来克服这种默认的路由匹配行为,它让你指定你想用哪条路由,像这样:

@Html.RouteLink( " Click me ", " MyOtherRoute "new { action =  " About " });

你可以使用Url.RouteUrl方法只生成URL。如果你不想为路由排序而烦恼,这一特性可能是很有用的。

THE CASE AGAINST NAMED ROUTES
反路由命名的情形

依赖路由名称来生成输出URL的问题是,这样做打断了MVC设计模式的中心思想 — 分离关注点。当在一个视图或动作方法中生成一条链接或一个URL时,我们想要关心的是用户导向(directed to)的动作和控制器,而不是所要用的URL格式。把不同路由的信息带到相应的视图或控制器,其实产生了我们希望避免的依赖性。

我们倾向于避免命名路由(通过对路由名参数指定为null)。我们更喜欢用代码注释来提醒我们,各条路由意欲做什么。 

定制路由系统

你已经看到了路由系统是多么灵活和可配置,但如果它不满足你的需求,你可以定制其行为。在本小节中,我们将向你演示两种方法来做这种事。

生成一个自定义的RouteBase实现

如果你不喜欢标准的路由对象匹配URL的方式,或者想实现一些不寻常的事,你可以通过RouteBase派生一个替代的类。这使你可以控制如何匹配URL、如何提取参数、以及如何生成输出URL

为了从RouteBase派生一个类,你需要执行两个方法:

·         GetRouteData(HttpContextBase httpContext):这是入站URL匹配工作的机制。框架依次对RouteTable.Routes的每个条目调用这个方法,直到其中一个返回一个非空值。

·         GetVirtualPath(RequestContext requestContext, RouteValueDictionary values):这是出站URL生成工作的机制。框架依次对RouteTable.Routes的每个条目调用这个方法,直到其中之一返回一个非空值。

为了演示这种自定义,我们将生成一个RouteBase类,它处理遗留URL请求(遗留URL,指下述的旧式URL — 译者注)。假设我们把一个现存的应用程序迁移到MVC框架,但有些用户已经收藏了MVC之前的一些URL,或把它们硬编码到了脚本中。我们仍然希望支持那些旧式的URL。当然,我们可以用规则的路由系统来处理它,但这一问题为本小节提供了一个很好的例子。

开始,我们需要生成一个控制器,它将接收我们的遗留请求。我们把这个控制器称为LegacyController,它的内容如清单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); // 传递视图模型
        }
    }
}

在这个简单的控制器中,GetLegacyURL动作方法获取一个参数,并把它作为一个视图模型传递给视图。如果我们真正执行这个控制器,我们将用这个方法来接收我们所请求的文件,但正如它所做的那样,我们只打算在视图中显示这个URL

提示:注意,在清单11-42中,我们转换了送给View方法的参数。View方法有一个重载版本,它以一个字符串指定要渲染的视图名,如果不进行这种转换,C#编译器会认为我们所要的便是这个重载方法。为了避免这种事情发生,我们把它转换成对象,以使我们调用传递视图模型并使用默认视图的重载方法。我们也可以借助于使用既有视图名也有视图模型为参数的重载方法来解决这一问题,但在能够解决问题的情况下,我们更喜欢在动作方法与视图之间不采用明确关联。

与这个动作关联的视图名为GetLegacyURL.cshtml,如清单11-43所示。

Listing 11-43. The GetLegacyURL View

@model string
@{
    ViewBag.Title = "GetLegacyURL";
Layout = null;
}
< h2 >GetLegacyURL </ h2 >
The URL requested was: @Model

这仍然很简单。我们只是想演示自定义的路由行为,因此,我们不打算把太多的时间花费在生成复杂的动作和视图上。现在,我们已经可以生成RouteBase的派生了。

对输入URL进行路由

我们已经生成了一个名为LegacyRoute的类,我们把它放在名为Infrastructure的顶级文件夹中(这是我们喜欢的用来放置不属于任何其它方面的支持类的地方)。这个类如清单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( thisnew  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;
        }
    }
}

这个类的构造器以一个字符器数组为参数,它表示这个路由将要支持的各个URL。我们将在稍后注册路由时指定它们。这个清单中要说明的是GetRouteData方法,它是这个路由系统所要调用的方法,以考查我们是否能够处理一个输入URL

如果我们不能处理这个请求,那么我们可以只返回null,于是路由系统将移动到路由表中的下一条路由,并反复进行处理。如果我们能够处理这个请求,我们需要返回RouteData类的一个实例,它含有controlleraction变量的值,以及其它我们想要传递给动作方法的东西。

当我们生成这个RouteData对象时,我们需要在这个处理程序中传递我们想要处理的我们所生成的值。我们打算用标准的MvcRouteHandler类,这是我们把含义赋给控制器和动作的值:

result =  new RouteData( thisnew MvcRouteHandler());

对于大多数MVC应用程序,这是我们需要的一个类,因为它把路由系统与MVC应用程序的控制器/动作模型连接起来了。但你可以实现MvcRouteHandler的替代程序,正如我们将在本章的生成自定义路由处理程序小节向你演示的那样。

在这个路由实现中,我们意欲对传递给控制器的任何URL进行路由。当我们获取一个URL时,我们为控制器和动作方法把硬编码的值加到了RouteValues对象。我们也传递所请求的URL作为legacyURL属性的值。注意,该属性的名字与我们动作方法的参数名匹配,确保我们在这里生成的值将通过该参数被传递给这个动作方法。

最后一步是注册一个新的路由,它使用我们继承自RouteBase类的派生类LegacyRoute。你可以在清单11-45中看到这是如何做的。

Listing 11-45.Registering the Custom RouteBase Implementation

public  static  void RegisterRoutes(RouteCollectionroutes) {
    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 });
}

我们生成了这个类的一个新实例,并在其中传递了我们想让它进行路由的URL。然后我们用Add方法把这个对象添加到RouteCollection。现在,当我们请求我们所定义的一个遗留URL("~/articles/Windows_3.1_Overview.html")时,该请求由我们的这个自定义路由类进行路由,并定向到我们的控制器,如图11-8所示。

图11-8

Figure 11-8. Routingrequests using a custom RouteBase implementation

生成输出URL

为了支持输出URL的生成,我们需要实现GetVirtualPath方法。再一次地,如果我们不能对请求进行处理,我们通过返回null让路由系统明白。否则,我们便返回VirtualPathData的一个实例。清单11-46演示的我们的实现。

Listing 11-46.Implementing the GetVirtualPath Method

public  override VirtualPathData GetVirtualPath(RequestContextrequestContext,
        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;
}

我们一直在用匿名类型传递片段变量和相关的其它细节,但在事情的背后,路由系统已经把这些转换成了RouteValueDictionary对象。因此,当我们像下面这样,把某些东西添加到视图时:

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

legacyURL属性生成的匿名类型被转换到了含有同名键的RouteValueDictionary类之中。在这个例子中,如果有一个名为legacyURL的键、并且如果它的值是一个传递给构造器的URL,我们便可以确定,我们能够为出站URL处理请求。我们可以对controlleraction的值做更特殊的事情并进行检查,但对于一个简单的例子,这就足够了。

如果我们获得了一个匹配,我们便生成VirtualPathData的一个新实例,在其中传递一个对当前对象的引用和出站URL。我们已经使用了UrlHelper类的Content方法,以便把应用程序的相对URL转换成可以传递给浏览器的URL。不幸的是,路由系统预先把一个/字符附加到这个URL上,因此,我们必须小心地从我们生成的URL上删掉这个前导字符。

生成自定义路由处理程序

我们的路由已经依赖于MvcRouteHandler,因为它把路由系统连接到MVC框架。而且因为我们关注的是MVC,这正是我们一直想要的。即使如此,借助于实现IRouteHandler接口,路由系统仍然可以让我们定义自己的路由处理程序。清单11-47提供了一个演示。

Listing 11-47.Implementing the IRouteHandler Interface

using System.Web;
using System.Web.Routing;
namespace URLsAndRoutes.Infrastructure {
    publicclass CustomRouteHandler : IRouteHandler {
        publicIHttpHandler GetHttpHandler(RequestContext requestContext) {
            returnnew CustomHttpHandler();
        }
    }
    publicclass CustomHttpHandler : IHttpHandler {
        publicbool IsReusable {
             getreturn  false; }
        }
        publicvoid ProcessRequest(HttpContext context) {
            context.Response.Write( " Hello ");
        }
    }
}

IRouteHandler接口的目的是提供一种手段,以生成IHttpHandler接口的实现,由它负责对请求进行处理。在这些接口的MVC实现中,找到控制器、调用动作方法、渲染视图、并把结果写入到响应中去。我们的实现有点简单。它只是把单词Hello写到客户端(而不是含有该单词的HTML文档,只是文本)。在我们定义一条路由时,我们可以注册这个自定义处理程序,如清单11-48所示。

Listing 11-48. Using a Custom Routing Handler in a Route

public  static  void RegisterRoutes(RouteCollection routes) {
    routes.Add(newRoute( " SayHello "new CustomRouteHandler()));
    routes.MapRoute( " MyRoute ", " {controller}/{action}/{id} ",
         new{ controller =  " Home ", action =  " Index ", id =UrlParameter.Optional });
}

当我们请求/SayHello这个URL时,我们的处理程序被用来处理这个请求。图11-9演示了其结果。

图11-9

Figure 11-9. Using a custom request handler

实现自定义路由处理,意味着你要负责常规功能的处理,如控制器及动作的解析。但它却给了你难以置信的自由。你可以接收MVC框架的某些部分而忽略其它部分,或者甚至实现一个全新的体系结构模式。

 

与区域一起工作

MVC框架支持把一个web应用程序组织成一些区域,每个区域代表应用程序的一个功能段,如管理、记账、客户支持等。这对大型项目是很有用的,对所有控制器、视图、和模型只用一组文件夹,可能会使管理变得困难。

每个MVC区域有它自己的文件夹结构,以允许你保持事物分离。这会使项目元素与应用程序的每个功能区的相关性更加明显。这有助于多个开发人员在同一个项目上工作而不会引起相互冲突。

通过路由系统,区域得到了广泛的支持,这是为什么在论述URL和路由之后,我们要涉及这一特性的原因。在本小节中,我们将向你演示如何在你的MVC项目中建立和使用区域。

我们为本章的这一部分生成一个新MVC项目。我们采用“Internet应用程序模板,并把该项目称为WorkingWithAreas

生成一个区域

为了把一个区域添加到一个MVC应用程序,在Solution Explorer窗口中右击该项目,并选择AddAreaVisualStudio将提示你输入该区域的名字,如图11-10所示。这里,我们生成了一个名为Admin的区域。这是要生成的一个十分常用的区域,因为许多web应用程序都需要分隔面向用户和管理的功能。点击添加按钮以生成这个区域。

图11-10

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

点击Add之后,你将看到项目有一些变化。首先,项目包含了一个新的名为Areas的顶级文件夹。它含有一个名为Admin的文件夹,它表示我们刚刚生成的区域。如果你要生成其它区域,这里就是生成其它文件夹的地方。

Areas/Admin文件夹中,你将看到我们有了一个最小的MVC项目。有名为ControllersModels、以及Views的文件夹。前两个是空的,但Views文件夹含有一个Shared文件夹(和一个配置视图引擎的Web.config文件,但直到第15章之前,我们对它不感兴趣)。

另一个变化是,有了一个名为AdminAreaRegistration.cs的文件,它含有AdminAreaRegistration类,如清单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 }
          );
        }
    }
}

这个类有趣的部分是RegisterArea方法。正如你从清单所看到的,该方法注册了一个带有URL模式为Admin/{controller}/{action}/{id}的路由。我们还可以在该方法中定义在该区域唯一的其它路由。

小心:如果你给路由赋名,你必须确保这些名字在整个应用程序而不仅仅在区域中是唯一的

我们不需要采用任何行为来保证这个注册方法被调用。这是由Global.asaxApplication_Start方法为我们自动进行处理的,请看清单11-50

Listing 11-50. AreaRegistration Called from Global.asax

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

对静态方法AreaRegistration.RegisterAllAreas的调用,导致MVC框架检查应用程序的所有类,找到派生于AreaRegistration类的所有类,并调用这些类上的RegisterArea方法。

小心:不要修改Application_Start方法中与路由相关的语句顺序。如果你在AreaRegistration.RegisterAllAreas调用之前调用RegisterRoutes,那么你的路由将在区域路由之前被定义。给定了路由被评估的顺序,这意味着对区域控制器的请求有可能被错误的路由匹配。

传递给每个区域的RegisterArea方法的AreaRegistrationContext类,暴露了一组MapRoute方法,区域可以用这些方法来注册路由,这与你的主应用程序在Global.asax中的RegisterRoutes方法注册路由的方式是相同的。

注:AreaRegistrationContext类中的MapRoute方法自动地把你注册的路由限制到包含该区域控制器的命名空间。这意味着当你在一个区域中生成一个控制器时,你必须把它放在它的默认命名空间中,否则,路由系统将找不到它。

填充区域

就像你在前面的例子中所看到的,你可以在一个区域中生成控制器、视图、以及模型等。

要生成一个控制器,在区域中的Controllers文件夹上右击,并从弹出菜单选择AddController。这将显示Add Controller对话框,让你输入这个新控制器的类名,如图11-11所示。

图11-11

Figure 11-11. Adding acontroller to an area

点击Add来生成一个空的控制器,如清单11-51所示。在本例中,我们用一个名为HomeController的类来演示一个应用程序中区域之间的分离。

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();
        }
    }
}

要完成这个简单的例子,我们可以通过在Index动作方法中右击,并从弹出菜单选择添加视图的办法,来生成一个视图。我们对该视图接收默认的视图名(Index)。当你生成这个视图时,你将看到它出现在Areas/Admin/Views/Home文件夹中。我们生成的这个视图如清单11-52所示。

Listing 11-52. A Simple View for an Area Controller

@{
    ViewBag.Title = "Index";
}
< h2 >Admin Area Index </ h2 >

所有这一切的目的是为了演示,在一个区域内的工作与在一个MVC项目主区中的工作是相当类似的。你已经看到,在项目中生成某个项的工作流是相同的。我们生成了一个控制器和视图,它们与项目主区中的对应物共享它们的名字(指名字可以相同译者注)。如果你启动这个应用程序,并导航到/Admin/Home/Index,你将看到我们生成的这个视图,如图11-12所示。

图11-12

Figure 11-12.Rendering an area view

 

解析不明确的控制器问题

是的,我们有点撒谎了。在前面的例子中,如果你导航到应用程序的根地址,你将看到类似于图11-13的错误。

图11-13

Figure 11-13. The ambiguous controller error

当一个区域被注册时,我们所定义的任何路由都被限制到与这个区域关联的命名空间。这是为什么我们能够请求/Admin/Home/Index,并得到WorkingWithAreas.Areas.Admin.Controllers命名空间中的HomeController类的原因。

然而,在Global.asaxRegisterRoutes方法中定义的路由没有类似的限制。你可以看到Visual Studio此刻的默认路由配置是清单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 }
    );
}

名字为Default的路由把来自浏览器的输入URL转换为Home控制器上的Index动作。这里,我们会收到一个错误消息,因为在适当的位置没有这条路由的命名空间约束,因此MVC框架会看到两个HomeController类。为了解决这个问题,我们需要优先Global.asax中的主控制器命名空间,如清单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"}

    );
}

这一修改确保了在请求解析过程中,主项目中的控制器优先。当然,如果你想优先选择一个区域中的控制器,你也可以这么做。

生成区域动作的链接

对用户所处的同一个MVC区域中的动作,你不需要采取任何特别的步骤来生成对该动作的链接。MVC框架会检测到当前请求相关的特定区域,然后出站URL的生成将只在该区域定义的路由中查找一个匹配。例如,以下附加到Admin区域中的视图:

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

会生成以下HTML

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

为了生成对不同区域中的动作、或根本不在区域中的动作(项目主区的动作译者注)的链接,你必须生成一个名为area的变量,并用它来指定你希望的区域名,像这样:

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

正是这一原因,area被保留用作为一个片段变量名。由上述调用所生成的HTML如下(假设你生成了一个名为Support的区域,它有标准的路由定义):

< href ="/Support/Home" >Click me to go to another area </ a >

译者注:所生成的这条HTML有错,似乎应当是(我没做过试验,请读者自行验证一下:

< href =”/Suport/Home/Index” >Click me to go to another area </ a >

如果你链接到一个顶级控制器(一个位于/Controllers文件夹的控制器)上的一个动作,那么你应该把区域指定为一个空字符串,像这样:

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

URLSchema 最佳实践

所有这一切之后,你可能还会疑惑,在哪里开始设计自己的URL方案。你可以只接收Visual Studio为你生成的默认方案,但给你的方案做一些思考是有好处的。

最近几年中,应用程序URL的设计已经日益认真,出现了几个重要的设计原理。如果你遵循这些设计模式,这将改善你应用程序的可用性和搜索引擎排列(指在搜索引擎中的排序译者注)。

使你的URL整洁和人性化

用户会注意你应用程序的URL。如果你不这么认为,那么只要回想你最后一次把Amazon的一个URL发送给某人的情形。以下是本书的URL

http://www.amazon.com/Pro-ASP-NET-MVC-3-Framework/dp/1430234040/ref=sr_1_13?s=books&ie=UTF8&qid=1294771153&sr=1-13

通过e-mail把这样一个URL发送给某人是一件糟糕的事情,除非尝试通过电话读它(其实这也很糟糕译者注)。现在我们需要做这件事的时候,我们会去除查找ISBN号,并要求调用程序自己去找到它。如果我们可以用以下这样的URL来访问这本书,那就很好了:

http://www.amazon.com/books/pro-aspnet-mvc3-framework

这是一种我们可以通过电话读出来的、而且似乎在写一封e-mail消息时不会遗漏什么东西的URL

注:要十分清楚,我们对Amazon只有崇高的敬意,它销售了比我们任何人写作还要多的图书。我们知道一个事实:Amazon团队的每个成员都具有惊人的智慧。他们不会有人幼稚到因为对URL格式的批评而停止销售我们的图书。我们爱Amazon。我们崇拜Amazon。我们只是希望他们会修正他们的URL

以下是生成友好URL的一些简单的纲要:

·         设计URL来描述它们的内容,而不是应用程序的实现细节。使用/Articles/AnnualReport,而不是/Website_v2/CachedContentServer/FromCache/AnnualReport

·         更乐意用内容标题而不是ID号,使用/Articles/AnnualReport而不是/Articles/2392。如果你必须使用一个ID号(以区别具有同样标题的条目,或避免通过标题查找一个条目时,需要多余的数据库查询步骤),那么两者都用(/Articles/2392/AnnualReport)。这需要多打一些字符,但它对人更有意义,且这会改善搜索引擎排列。

·         不要对HTML页面使用文件扩展名(例如,.aspx.mvc),但对特殊文件类型要用扩展名(如,.jpg.pdf.zip。如果你适当地设置了MIME类型,web浏览器不会在意文件的扩展名,但人却希望对PDF文件以.pdf结尾。

·         生成一种层次感(例如,/Products/Menswear/Shirts/Red),于是,你的访问者可以猜出父目录的URL

·         是不分大小写的(有些人可能想通过一个打印的页面来输入URL)。ASP.NET路由系统是默认大小写不分的。

·         避免符号、代码、和字符连续。如果你想用单词分隔符,用短横(如,/my-great-article)。下划线是不友好的,而URL编码的空格是奇异的(/my+great+article)或令人讨厌的(/my%20great%20article)。

·         不要修改URL。打破连接等于失去商务。当你确实要修改URL时,通过永久重定向(301)尽可能长时间地继续支持旧式的URL方案。

·         具有一致性。在整个应用程序中采用一种URL格式

URL应该简短、易于键入、可剪切(人性化可编辑)、且是持久稳定的,而且它们应该形象化网站结构。Jakob Nielsen,可用性宗师,在http://www.useit.com/alertbox/990321.html详述了这一论题。Tim Berners-LeeWeb发明家,提出了类似的忠告(参见http://www.w3.org/Provider/Style/URI)。

GETPOST:选用正确的一个

经验规则是,GET请求应该被用于接收所有只读信息,而POST请求应该被用于各种修改应用程序状态的操作。用与标准相应的术语来说,GET请求是安全交互(除了接收信息无其它影响),而POST请求是不安全交互(作出决定,或修改某些东西)。这些约定是由全球互联网联盟(W3C)在http://www.w3.org/Provider/Style/URI上设定的。

GET请求是可设定地址的所有信息都包含在URL中,因此它可以设为书签并连接到这些地址。

对改变状态的操作不要用GET请求。2005年当Google Web AcceleratorGoogleWeb加速器)发布时,许多web开发者对此很难理解。该应用程序从每个连接的页面预取了全部内容,这在HTTP中是合法的,因为GET请求应该是安全的(意即,加速器采用的是GET方法来获取各个每页的内容译者注)。不幸的是,许多web开发者忽略了这种HTTP约定,并在他们的应用程序中用简单的连接来删除条目添加到购物车。混乱随之发生。

一个公司确信,他们的内容管理系统是不断遭受恶意攻击的目标,因为他们的内容不断地被删除。他们后来才发现,搜索引擎爬行器偶然发现了一个管理页面的URL,并爬行了所有删除连接。认证也许可以阻止这种情况,但它不会阻止web加速器。

小结

在本章中,我们深度考察了路由系统。你已经看到输入路由被如何匹配和处理,以及如何生成输出路由。沿着这条路线,我们介绍了区域的概念,并考察了如何生成有用且有意义的URL方案。

在下一章中,我们回到控制器和动作,他们是MVC模型最核心的部分。我们将详细解释这些是如何工作的,并给你演示,在你的应用程序中如何用它们去获取最好的结果。 

转载于:https://www.cnblogs.com/jeriffe/articles/2395684.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值