[ASP.NET MVC 小牛之路]07 - URL Routing

咱们知道在ASP.NET Web Forms中,一个URL请求每每对应一个aspx页面,一个aspx页面就是一个物理文件,它包含对请求的处理。html

而在ASP.NET MVC中,一个URL请求是由对应的一个Controller中的Action来处理的,由URL Routing来告诉MVC如何定位到正确的Controller和Action。正则表达式

笼统的讲,URL Routing包含两个主要功能:解析URL 和 生成URL,本文将围绕这两个大点进行讲解。浏览器

本文目录服务器

URL Routing 的定义方式

让咱们从下面这样一个简单的URL开始:mvc

http://mysite.com/Admin/Indexasp.net

在域名的后面,默认使用“/”来对URL进行分段。路由系统经过相似于 {controller}/{action} 格式的字符串能够知道这个URL的 Admin 和 Index 两个片断分别对应Controller和Action的名称。dom

默认状况下,路由格式中用“/”分隔的段数是和URL域名的后面的段数是一致的,好比,对于{controller}/{action} 格式只会匹配两个片断。以下表所示:ide

URL路由是在MVC工程中的App_Start文件夹下的RouteConfig.cs文件中的RegisterRoutes方法中定义的,下面是建立一个空MVC项目时系统生成的一个简单URL路由定义:函数

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

    routes.MapRoute( 
        name: "Default", 
        url: "{controller}/{action}/{id}", 
        defaults: new { controller = "Home", action = "Index",  id = UrlParameter.Optional } 
    );
}

静态方法RegisterRoutes是在Global.asax.cs文件中的Application_Start方法中被调用的,除了URL路由的定义外,还包含其余的一些MVC核心特性的定义:网站

protected void Application_Start() { 
    AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); }

RouteConfig.RegisterRoutes方法中传递的是 RouteTable 类的静态 Routes 属性,返回一个RouteCollection的实例。其实,“原始”的定义路由的方法能够这样写:

public static void RegisterRoutes(RouteCollection routes) { 

    Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler()); 
    routes.Add("MyRoute", myRoute); 
}

建立Route对象时用了一个URL格式字符串和一个MvcRouteHandler对象做为构造函数的参数。不一样的ASP.NET技术有不一样的RouteHandler,MVC用的是MvcRouteHandler。

这种写法有点繁琐,一种更简单的定义方法是:

public static void RegisterRoutes(RouteCollection routes) { 

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

这种方法简洁易读,通常咱们都会用这种方法定义路由。 

示例准备

做为演示,咱们先来准备一个Demo。建立一个标准的MVC应用程序,而后添加三个简单的Controller,分别是HomeController、CustomerController和AdminController,代码以下:

public class HomeController : Controller {
            
    public ActionResult Index() {
        ViewBag.Controller = "Home";
        ViewBag.Action = "Index";
        return View("ActionName");
    }
}
HomeController
public class CustomerController : Controller {
        
    public ActionResult Index() {
        ViewBag.Controller = "Customer";
        ViewBag.Action = "Index";
        return View("ActionName");
    }

    public ActionResult List() {
        ViewBag.Controller = "Customer";
        ViewBag.Action = "List";
        return View("ActionName");
    }
}
CustomerController
public class AdminController : Controller {
        
    public ActionResult Index() {
        ViewBag.Controller = "Admin";
        ViewBag.Action = "Index";
        return View("ActionName");
    }
}
AdminController

在 /Views/Shared 文件夹下再给这三个Controller添加一个共享的名为 ActionName.cshtml 的 View,代码以下:

@{ 
    Layout = null; 
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>ActionName</title>
</head>
<body>
    <div>The controller is: @ViewBag.Controller</div>
    <div>The action is: @ViewBag.Action</div>
</body>
</html>
ActionName.cshtml

咱们把RouteConfig.cs文件中项目自动生成的URL Rounting的定义删了,而后根据前面讲的路由定义知识,咱们本身写一个最简单的:

public static void RegisterRoutes(RouteCollection routes) { 

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

程序运行,URL定位到 Admin/Index 看看运行结果:

这个Demo输出的是被调用的Controller和Action名称。

给片断变量定义默认值

在上面咱们必须把URL定位到特定Controller和Action,不然程序会报错,由于MVC不知道去执行哪一个Action。 咱们能够经过指定默认值来告诉MVC当URL没有给出对应的片断时使用某个默认的值。以下给controller和action指定默认值:

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

这时候若是在URL中不提供action片断的值或不提供controller和action两个片断的值,MVC将使用路由定义中提供的默认值:

它的各类匹配状况以下表所示:

注意,对于上面的URL路由的定义,咱们能够只给action一个片断指定默认值,可是不能只给controller一个片断指定默认值,即若是咱们给Controller指定了默认值,就必定也要给action指定默认值,不然URL只有一个片断时,这个片断匹配给了controller,action将找不到匹配。

定义静态片断

并非全部的片断都是用来做为匹配变量的,好比,咱们想要URL加上一个名为Public的固定前缀,那么咱们能够这样定义:

routes.MapRoute("", "Public/{controller}/{action}",  new { controller = "Home", action = "Index" });

这样,请求的URL也须要一个Public前缀与之匹配。咱们也能够把静态的字符串放在大括号之外的任何位置,如:

routes.MapRoute("", "X{controller}/{action}",  new { controller = "Home", action = "Index" });

在一些状况下这种定义很是有用。好比当你的网站某个连接已经被用户广泛记住了,但这一块功能已经有了一个新的版本,但调用的是不一样名称的controller,那么你把原来的controller名称做为如今controller的别名。这样,用户依然使用他们记住的URL,而导向的倒是新的controller。以下使用Shop做为Home的一个别名:

routes.MapRoute("ShopSchema", "Shop/{action}",  new { controller = "Home" }); 

这样,用户使用原来的URL能够访问新的controller:

自定义片断变量

自定义片断变量的定义和取值

contrlloer和action片断变量对MVC来讲有着特殊的意义,在定义一个路由时,咱们必须有这样一个概念:contrlloer和action的变量值要么能从URL中匹配获得,要么由默认值提供,总之一个URL请求通过路由系统交给MVC处理时必须保证contrlloer和action两个变量的值都有。固然,除了这两个重要的片断变量,咱们也可从经过自定义片断变量来从URL中获得咱们想要的其它信息。以下自定义了一个名为Id的片断变量,并且给它定义了默认值:

routes.MapRoute("MyRoute", "{controller}/{action}/{id}",
    new {
        controller = "Home",
        action = "Index",
        id = "DefaultId"
});

咱们在HomeController中增长一个名为CustomVariable的ACtion来演示一下如何取自定义的片断变量:

public ActionResult CustomVariable() {
    ViewBag.Controller = "Home";
    ViewBag.Action = "CustomVariable";
    ViewBag.CustomVariable = RouteData.Values["id"];
    return View("ActionName");
}

能够经过 RouteData.Values[segment] 来取得任意一个片断的变量值。

再稍稍改一下ActionName.cshtml 来看一下咱们取到的自定义片断变量的值:

...
<div>The controller is: @ViewBag.Controller</div> 
<div>The action is: @ViewBag.Action</div> 
<div>The custom variable is: @ViewBag.CustomVariable</div>
...

将URL定位到 /Home/CustomVariable/Hello 将获得以下结果:

自定义的片断变量用处很大,也很灵活,下面介绍一些常见的用法。

将自定义片断变量做为Action方法的参数

咱们能够将自定义的片断变量看成参数传递给Action方法,以下所示:

public ActionResult CustomVariable(string id) { 
    ViewBag.Controller = "Home"; 
    ViewBag.Action = "CustomVariable"; 
    ViewBag.CustomVariable = id; 
    return View("ActionName"); 
}

效果和上面是同样的,只不过这样省去了用 RouteData.Values[segment] 的方式取自定义片断变量的麻烦。这个操做背后是由模型绑定来作的,模型绑定的知识我将在后续博文中进行讲解。

指定自定义片断变量为可选

指定自定片断变量为可选,即在URL中能够不用指定片断的值。以下面的定义将Id定义为可选:

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

定义为可选之后,须要对URL中没有Id这个片断值的状况进行处理,以下:

public ActionResult CustomVariable(string id) { 
    ViewBag.Controller = "Home"; 
    ViewBag.Action = "CustomVariable"; 
    ViewBag.CustomVariable = id == null ? "<no value>" : id; 
    return View("ActionName"); 
} 

当Id是整型的时候,参数的类型须要改为可空的整型(即int? id)。

为了省去判断参数是否为空,咱们也能够把Action方法的id参数也定义为可选,当没有提供Id参数时,Id使用默认值,以下所示:

public ActionResult CustomVariable(string id = "DefaultId") { 
    ViewBag.Controller = "Home"; 
    ViewBag.Action = "CustomVariable"; 
    ViewBag.CustomVariable = id; 
    return View("ActionName"); 
}

这样其实就是和使用下面这样的方式定义路由是同样的:

routes.MapRoute("MyRoute", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "DefaultId" });

定义可变数量的自定义片断变量

咱们能够经过 catchall 片断变量加 * 号前缀来定义匹配任意数量片断的路由。以下所示:

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", 
    new { controller = "Home", action = "Index",  id = UrlParameter.Optional });

这个路由定义的匹配状况以下所示:

使用*catchall,将匹配的任意数量的片断,但咱们须要本身经过“/”分隔catchall变量的值来取得独立的片断值。

路由约束

正则表达式约束

经过正则表达式,咱们能够制定限制URL的路由规则,下面的路由定义限制了controller片断的变量值必须以 H 打头:

routes.MapRoute("MyRoute", "{controller}/{action}/{id}", 
    new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    new { controller = "^H.*" }
);

定义路由约束是在MapRoute方法的第四个参数。和定义默认值同样,也是用匿名类型。

咱们能够用正则表达式约束来定义只有指定的几个特定的片断值才能进行匹配,以下所示:

routes.MapRoute("MyRoute", "{controller}/{action}/{id}", 
    new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    new { controller = "^H.*", action = "^Index$|^About$" }
);

这个定义,限制了action片断值只能是Index或About,不区分大小写。

Http请求方式约束

咱们还能够限制路由只有当以某个特定的Http请求方式才能匹配。以下限制了只能是Get请求才能进行匹配:

routes.MapRoute("MyRoute", "{controller}/{action}/{id}", 
    new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    new { controller = "^H.*", httpMethod = new HttpMethodConstraint("GET") }
);

经过建立一个 HttpMethodConstraint 类的实例来定义一个Http请求方式约束,构造函数传递是容许匹配的Http方法名。这里的httpMethod属性名不是规定的,只是为了区分。

这种约束也能够经过HttpGet或HttpPost过滤器来实现,后续博文再讲到滤器的内容。

自定义路由约束

若是标准的路由约束知足不了你的需求,那么能够经过实现 IRouteConstraint 接口来定义本身的路由约束规则。

咱们来作一个限制浏览器版本访问的路由约束。在MVC工程中添加一个文件夹,取名Infrastructure,而后添加一个 UserAgentConstraint 类文件,代码以下:

public class UserAgentConstraint : IRouteConstraint {
        
    private string requiredUserAgent;

    public UserAgentConstraint(string agentParam) {
        requiredUserAgent = agentParam;
    }

    public bool Match(HttpContextBase httpContext, Route route, string parameterName,
        RouteValueDictionary values, RouteDirection routeDirection) {
            
        return httpContext.Request.UserAgent != null 
            && httpContext.Request.UserAgent.Contains(requiredUserAgent);
    }
}

这里实现IRouteConstraint的Match方法,返回的bool值告诉路由系统请求是否知足自定义的约束规则。咱们的UserAgentConstraint类的构造函数接收一个浏览器名称的关键字做为参数,若是用户的浏览器包含注册的关键字才能够访问。接一来,咱们须要注册自定的路由约束:

public static void RegisterRoutes(RouteCollection routes) {

    routes.MapRoute("ChromeRoute", "{*catchall}",
        new { controller = "Home", action = "Index" },
        new { customConstraint = new UserAgentConstraint("Chrome") }
    );
}

下面分别是IE10和Chrome浏览器请求的结果:

     

定义请求磁盘文件路由

并非全部的URL都是请求controller和action的。有时咱们还须要请求一些资源文件,如图片、html文件和JS库等。

咱们先来看看能不能直接请求一个静态Html文件。在项目的Content文件夹下,添加一个html文件,内容随意。而后把URL定位到该文件,以下图:

咱们看到,是能够直接访问一静态资源文件的。

默认状况下,路由系统先检查URL是否是请求静态文件的,若是是,服务器直接返回文件内容并结束对URL的路由解析。咱们能够经过设置 RouteCollection的 RouteExistingFiles 属性值为true 让路由系统对静态文件也进行路由匹配,以下所示:

public static void RegisterRoutes(RouteCollection routes) {
    
    routes.RouteExistingFiles = true;

    routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
        new { controller = "Home", action = "Index", id = UrlParameter.Optional
    });
}

设置了routes.RouteExistingFiles = true后,还须要对IIS进行设置,这里咱们以IIS Express为例,右键IIS Express小图标,选择“显示全部应用程序”,弹出以下窗口:

点击并打开配置文件,Control+F找到UrlRoutingModule-4.0,将这个节点的preCondition属性改成空,以下所示:

<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition=""/>

而后咱们运行程序,再把URL定位到以前的静态文件:

这样,路由系统经过定义的路由去匹配RUL,若是路由中没有定义该静态文件的匹配,则会报上面的错误。

一旦定义了routes.RouteExistingFiles = true,咱们就要为静态文件定义路由,以下所示:

public static void RegisterRoutes(RouteCollection routes) {
    
    routes.RouteExistingFiles = true;

    routes.MapRoute("DiskFile", "Content/StaticContent.html", new { controller = "Customer", action = "List", });

    routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
        new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}

这个路由匹配Content/StaticContent.html的URL请求为controller = Customer, action = List。咱们来看看运行结果:

这样作的目的是为了能够在Controller的Action中控制对静态资源的请求,而且能够阻止对一些特殊资源文件的访问。

设置了RouteExistingFiles属性为true后,咱们要为容许用户请求的资源文件进行路由定义,若是每种资源文件都去定义相应的路由,就会显得很繁琐。

咱们能够经过RouteCollection类的IgnoreRoute方法绕过路由定义,使得某些特定的静态文件能够由服务器直接返回给给浏览器,以下所示:

public static void RegisterRoutes(RouteCollection routes) {
    
    routes.RouteExistingFiles = true;

    routes.IgnoreRoute("Content/{filename}.html");

    routes.MapRoute("DiskFile", "Content/StaticContent.html",
        new { controller = "Customer", action = "List", });

    routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
        new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}

这样,只要是请求Content目录下的任何html文件都能被直接返回。这里的IgnoreRoute方法将建立一个RouteCollection的实例,这个实例的Route Handler 为 StopRoutingHandler,而不是 MvcRouteHandler。运行程序定位到Content/StaticContent.html,咱们又看到了以前的静态面面了。

生成URL(连接)

前面讲的都是解析URL的部分,如今咱们来看看如何经过路由系统在View中生成URL。

生成指向当前controller的action连接

在View中生成URL的最简单方法就是调用Html.ActionLink方法,以下面在 Views/Shared/ActionName.cshtml 中的代码所示:

...
<div>The controller is: @ViewBag.Controller</div>
<div>The action is: @ViewBag.Action</div>
<div>
    @Html.ActionLink("This is an outgoing URL", "CustomVariable")
</div>
...

这里的Html.ActionLink方法将会生成指向View对应的Controller和第二个参数指定的Action,咱们能够看看运行后页面是如何显示的:

通过查看Html源码,咱们发现它生成了下面这样的一个html连接:

<a href="/Home/CustomVariable">This is an outgoing URL</a> 

这样看起来,经过Html.ActionLink生成URL彷佛并无直接在View中本身写一个<a>标签更直接明了。 但它的好处是,它会自动根据路由配置来生成URL,好比咱们要生成一个指向HomeContrller中的CustomVariable Action的链接,经过Html.ActionLink方法,只须要给出对应的Controller和Action名称就行,咱们不须要关心实际的URL是如何组织的。举个例子,咱们定义了下面的路由:

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

运行程序,咱们发现它会自动生成下面这样的链接:

<a href="/App/DoCustomVariable">This is an outgoing URL</a>

因此咱们要生成指向某个Action的连接时,最好使用Html.ActionLink方法,不然你很难保证你手写的链接就能定位到你想要的Action。

生成其余controller的action连接

上面咱们给Html.ActionLink方法传递的第二个参数只告诉了路由系统要定位到当前View对应的Controller下的Action。Html.ActionLink方法可使用第三个参数来指定其余的Controller,以下所示:

<div> 
    @Html.ActionLink("This targets another controller", "Index", "Admin") 
</div> 

它会自动生成以下连接:

<a href="/Admin">This targets another controller</a> 

生成带有URL参数的连接

有时候咱们想在链接后面加上参数以传递数据,如 ?id=xxx 。那么咱们能够给Html.ActionLink方法指定一个匿名类型的参数,以下所示:

<div>
    @Html.ActionLink("This is an outgoing URL", "CustomVariable", new { id = "Hello" })
</div>

它生成的Html以下:

<a href="/Home/CustomVariable/Hello">This is an outgoing URL</a>

指定连接的Html属性

经过Html.ActionLink方法生成的连接是一个a标签,咱们能够在方法的参数中给标签指定Html属性,以下所示:

<div> 
    @Html.ActionLink("This is an outgoing URL",  "Index", "Home", null, 
        new {id = "myAnchorID", @class = "myCSSClass"})
</div>

这里的class加了@符号,是由于class是C#关键字,@符号起到转义的做用。它生成 的Html代码以下:

<a class="myCSSClass" href="/" id="myAnchorID">This is an outgoing URL</a>

生成完整的标准连接

前面的都是生成相对路径的URL连接,咱们也能够经过Html.ActionLink方法生成完整的标准连接,方法以下:

<div> 
    @Html.ActionLink("This is an outgoing URL", "Index", "Home", 
        "https", "myserver.mydomain.com", " myFragmentName",
        new { id = "MyId"},
        new { id = "myAnchorID", @class = "myCSSClass"})
</div>

这是Html.ActionLink方法中最多参数的重载方法,它容许咱们提供请求的协议(https)和目标服务器地址(myserver.mydomain.com)等。它生成的连接以下:

<a class="myCSSClass" id="myAnchorID"
    href="https://myserver.mydomain.com/Home/Index/MyId#myFragmentName" >
    This is an outgoing URL</a>

生成URL字符串

用Html.ActionLink方法生成一个html连接是很是有用而常见的,若是要生成URL字符串(而不是一个Html连接),咱们能够用 Url.Action 方法,使用方法以下:

<div>This is a URL: 
    @Url.Action("Index", "Home", new { id = "MyId" }) 
</div> 

它显示到页面是这样的:

根据指定的路由名称生成URL

咱们能够根据某个特定的路由来生成咱们想要的URL,为了更好说明这一点,下面给出两个URL的定义:

public static void RegisterRoutes(RouteCollection routes) { 
    routes.MapRoute("MyRoute", "{controller}/{action}"); 
    routes.MapRoute("MyOtherRoute", "App/{action}", new { controller = "Home" }); 
} 

对于这样的两个路由,对于相似下面这样的写法:

@Html.ActionLink("Click me", "Index", "Customer")

始终会生成这样的连接:

<a href="/Customer/Index">Click me</a>

也就是说,永远没法使用第二个路由来生成App前缀的连接。这时候咱们须要经过另外一个方法Html.RouteLink来生成URL了,方法以下:

@Html.RouteLink("Click me", "MyOtherRoute","Index", "Customer")

它会生成以下连接:

<a Length="8" href="/App/Index?Length=5">Click me</a>

这个连接指向的是HomeController下的Index Action。但须要注意,经过这种方式来生成URL是不推荐的,由于它不能让咱们从直观上看到它生成的URL指向的controller和action。因此,非到万不得已的状况才会这样用。

在Action方法中生成URL

一般咱们通常在View中才会去生成URL,但也有时候咱们须要在Action中生成URL,方法以下:

public ViewResult MyActionMethod() { 
    
    string myActionUrl = Url.Action("Index", new { id = "MyID" }); 
    string myRouteUrl = Url.RouteUrl(new { controller = "Home", action = "Index" }); 
    
    //... do something with URLs... 
    return View(); 
}

其中 myActionUrl 和 myRouteUrl 将会被分别赋值 /Home/Index/MyID 和 / 。

更多时候咱们会在Action方法中将客户端浏览器重定向到别的URL,这时候咱们使用RedirectToAction方法,以下:

public RedirectToRouteResultMyActionMethod() { 
    return RedirectToAction("Index");
}

RedirectToAction的返回结果是一个RedirectToRouteResult类型,它使MVC触发一个重定向行为,并调用指定的Action方法。RedirectToAction也有一些重载方法,能够传入controller等信息。也可使用RedirectToRoute方法,该方法传入的是object匿名类型,易读性强,如:

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

URL方案最佳实践

下面是一些使用URL的建议:

  1. 最好能直观的看出URL的意义,不要用应用程序的具体信息来定义URL。好比使用 /Articles/Report 比使用 /Website_v2/CachedContentServer/FromCache/Report 好。

  2. 使用内容标题比使用ID好。好比使用 /Articles/AnnualReport 比使用 /Articles/2392 好。若是必定要使用使用ID(好比有时候可能须要区分相同的标题),那么就二者都用,如 /Articles/2392/AnnualReport ,它看起来很长,但对用户更友好,并且更利于SEO。

  3. 对于Web页面不要使用文件扩展名(如 .aspx 或 .mvc)。但对于特殊的文件使用扩展名(如 .jpg、.pdf 和 .zip等)。

  4. 尽量使用层级关系的URL,如 /Products/Menswear/Shirts/Red,这样用户就能猜到父级URL。

  5. 不区分大小写,这样方便用户输入。

  6. 正确使用Get和Post。Get通常用来从服务器获取只读的信息,当须要操做更改状态时使用Post。

  7. 尽量避免使用标记符号、代码、字符序列等。若是你想要用标记进行分隔,就使用中划线(如 /my-great-article),下划线是不友好的,另外空格和+号都会被URL编码。

  8. 不要轻易改变URL,尤为对于互联网网站。若是必定要改,那也要尽量长的时间保留原来的URL。

  9. 尽可能让URL使用统一的风格或习惯。

 


参考:

  《Pro ASP.NET MVC 4 4th Edition》

  http://msdn.microsoft.com/en-us/library/cc668201.ASPX

  http://www.asp.net/mvc/tutorials/older-versions/controllers-and-routing/asp-net-mvc-routing-overview-cs

相关文章
相关标签/搜索