先来看下面两个个url,对比一下:
- http://xxx.yyy.com/Admin/UserManager.aspx
- http://xxx.yyy.com/Admin/DeleteUser/1001
对于第1个Url,假设它与服务器上的文件有直接的关系,那么服务器在接受客户端请求并将对应的文件传送给客户端。我们大概可以猜到它是对用户管理的一个页面,它的物理文件UserManager.aspx在网站根目录下面的Admin文件夹中。而第2个url,在不知道Mvc路由以及Url重写时,很难猜到这个Url背后具体有些什么,前提条件是基于.Net框架开发的Web项目。 那么在这里,我们引入Asp.Net Mvc Url路由这个概念,也正是本文所要阐述的主题,Url路由模块是负责映射从浏览器请求到特定的控制器动作。
基于上面提到的Url路由以及其作用,我们就大概能猜到第2个Url背后有些啥了。 自然而然的Admin就是控制器了,DeleteUser是控制器里面的动作及Action了,1001就是Action的参数。到这里我们对Url路由有一个简单的认识,那么接着看下面一组url,假设Home是控制器,Index是控制器里面的Action,至于1001或者2345这类数据我们暂且约定为参数:
- http://xxx.yyy.com
- http://xxx.yyy.com/Home/1001
- http://xxx.yyy.com/Index/1001
- http://xxx.yyy.com/Home/Index/1001/2345
- http://xxx.yyy.com/System/Home/Index/1001
按照约定,从上面的几组Url中可以看出,有的缺控制器,有的缺Action,有的带有好几个参数,有的又莫名的多出了控制器、Action、参数之外的东西,那么他们能正确的访问吗?
注册路由
在vs2012里面新建一个asp.net mvc4 web 应用程序项目,可以在项目的根目录下App_Start里面看到RouteConfig文件,在这个文件里面就可以添加url路由了。在文件里面可以看到MapRoute 这个方法,它是RouteCollection的扩展方法,并且含有多个重载,下面看看MapRoute方法的参数,这里选参数最多的那个:
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces);
从上面的代码片段大概可以看出该方法含有路由名称、路由的Url、默认值、约束、首先查找路由所在的命名空间,该方法是返回对映射路由的引用。不过在RouteConfig文件中我们可以看到添加路由的代码:
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } );
稍微分析一下这段代码可以知道,在路由集合里面添加了一个名为"Default"的路由,并且有一个默认的路由指向,指向名称为Home的控制器,并且Action为Index,可以明显的看到这里 "id = UrlParameter.Optional" 的写法,它的意思就是说Action的参数可以不需要用户来指定,可以缺省。
多个参数如何传递
之前在一个QQ群里面见到一兄弟在问,类似这样的Url“http://xxx.yyy.com/Home/Index/1001/2345/tauruswu”在路由里面怎么配置?其实这个也很简单,我们只需要将路由配置、以及Action稍作调整。
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}/{*catchall}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } );
这个是调整之后的路由配置,只需要在Url后面再加上"{*catchall}"即可,那么Action调整之后如下
public ActionResult Index(string id,string catchall) { ViewBag.Message = "修改此模板以快速启动你的 ASP.NET MVC 应用程序。"; return View(); }
在Action里面定义了一个参数"catchall"用来接收Url中除了Id之外其他所有的参数值,那么上面那个Url中接收的参数就是“2345/tauruswu”,这样看起来好不好了,不过我觉得怪怪的,有没有更好的解决方法了?Url有必要写成那么长吗?这个问题在后续文章中会涉及到。
你也许会犯的错误
不知各位兄弟在刚刚接触MVC的时候,有没有碰到过这样的问题,如下图
那么这个错误是如何引起的了?代码是这么写的
namespace MvcDebug.Controllers { public class HomeController : Controller { public ActionResult Index(string id,string catchall) { ViewBag.Message = "修改此模板以快速启动你的 ASP.NET MVC 应用程序。"; return View(); } } } namespace MvcDebug.Controllers.Tauruswu { public class HomeController : Controller { public ActionResult Index(string id, string catchall) { ViewBag.Message = "修改此模板以快速启动你的 ASP.NET MVC 应用程序。"; return View(); } } }
我们再看英文提示大概就是说匹配出了多个控制器为"Home"的类型,在它的提示中也告诉了我们解决方法,说在MapRoute方法中使用"namespaces"这个参数,我们先将这个放在一边,将抛出这个错误的源码给找出来,具体的源码在DefaultControllerFactory这个类中,看类名就能猜出它的作用是什么了。
private Type GetControllerTypeWithinNamespaces(RouteBase route, string controllerName, HashSet<string> namespaces) { // Once the master list of controllers has been created we can quickly index into it ControllerTypeCache.EnsureInitialized(BuildManager); ICollection<Type> matchingTypes = ControllerTypeCache.GetControllerTypes(controllerName, namespaces);
switch (matchingTypes.Count) { case 0: // no matching types return null; case 1: // single matching type return matchingTypes.First(); default: // multiple matching types throw CreateAmbiguousControllerException(route, controllerName, matchingTypes); } }
上面粗体标出的代码,因为我们在路由中没有配置“namespaces”这个参数,这段代码的意思就是通过控制器名称获取所匹配的控制器类型集合,当获取到集合数据之后就开始Case了,很明显,这里集合的数目是2,自然就抛错了。
问题出来了,该如何解决?在错误提示中说要用到“namespaces”这个参数,我们能不能告诉MVC解析引擎,在解析控制器名称时,能不能对某些命名空间进行优先处理,事实上是可以的,只需要在配置路由的地方稍微调整一下
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}/{*catchall}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces: new[] { "MvcDebug.Controllers.Tauruswu" } );
上面标粗的代码的意思是说优先解析“MvcDebug.Controllers.Tauruswu”这个命名空间里面的控制器。
路由约束
对于路由约束,我个人觉得这个可能在实际开发中用的不是很多,既然MapRoute方法提供了constraints这个参数及约束,那么在某些特定的场合肯定能发挥它的作用。在MVC中提供了三种路由约束方案,分别是: 1)正则表达式 ,2)http方法 ,3)自定义约束 。下面我们分别介绍下这三种约束的使用方法。
1)正则表达式 ,在路由配置中,我们做了这样的规则,只匹配Controller名称以H开头的
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}/{*catchall}", defaults: new { controller = "Demo", action = "Index", id = UrlParameter.Optional }, constraints: new { controller = "^H.*" }, namespaces: new[] { "MvcDebug.Controllers.Tauruswu" } );
2) http方法 ,我们将路由配置稍作修改
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}/{*catchall}", defaults: new { controller = "Demo", action = "Index", id = UrlParameter.Optional }, constraints: new { httpMethod = new HttpMethodConstraint("POST"), }, namespaces: new[] { "MvcDebug.Controllers.Tauruswu" } );
然后在对应的Action上面打个标记
[HttpGet] public ActionResult Index() { ViewBag.Message = "修改此模板以快速启动你的 ASP.NET MVC 应用程序。"; return View(); }
你们说这样行不行了?
3) 自定义约束,如果说上面两种需求还是不能满足你,那么我们可以自定义约束。我们翻看HttpMethodConstraint这个类,可以看到它是继承IRouteConstraint这个接口,其定义是
public interface IRouteConstraint { bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection); }
里面只有一个布尔类型的方法,关于这个例子,是从网上借鉴过来的,如下
public class CustomConstraint : IRouteConstraint { private string requiredAgent; public CustomConstraint(string agentArgs) { this.requiredAgent = agentArgs; } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { return httpContext.Request.UserAgent != null && httpContext.Request.UserAgent.Contains(requiredAgent); } }
这段代码的意思就是检查客户端请求的UserAgent属性值,看它是否含有一个被传递给构造函数的值。那么我们将路由作如下修改
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}/{*catchall}", defaults: new { controller = "Demo", action = "Index", id = UrlParameter.Optional }, constraints: new { customConstraint = new CustomConstraint("IE"), }, namespaces: new[] { "MvcDebug.Controllers.Tauruswu" } );
很显然,这个只能在IE游览器下面游览。
如何创建自定义路由处理程序
在翻阅MapRoute方法源码的时候,看到了这么一段
Route route = new Route(url, new MvcRouteHandler()) { Defaults = CreateRouteValueDictionary(defaults), Constraints = CreateRouteValueDictionary(constraints), DataTokens = new RouteValueDictionary() }; .....
routes.Add(name, route);
在Route实例化的时候,它是用到了“MvcRouteHandler“这个类,该类继承”IRouteHandler“接口,如果我们不用系统里面已经定义好的路由处理方案,我们要自己来实现一套?改怎么下手,此时只需要继承”IRouteHandler“这个接口并实现”GetHttpHandler“方法即可。
最后在添加路由时,像这样操作
routes.Add(new Route("DemoUrl",new DemoRouteHandler());
当我们在游览器里面请求/DemoUrl这个地址时,就会用到我们自定义的处理程序,在实际开发当中,如果真的要用到自定义路由处理程序,那么我们就要实现很多原本框架所实现的空能,虽然这给我们带来了很大的扩展空间,但是又不可控。
总结
Url路由系统是通过请求地址进行解析从而得到以目标Controller/Action名称为核心的路由数据,Url路由系统是建立在Asp.net 之上,我们在调试System.Web.Routing的源码时候可以得知。在这里我们由浅入深的了解了路由系统,接下来我们会讲到控制器以及Action,也是最为核心的东西。