本文着重讲述Asp.net MVC的路由配置,url灵活动态输出以及Area的使用。

 

      一、路由配置:这里忽略VS2010给默认生成的Route,按照先易后难的顺序来讲解。

      在讲解route之前,先说一下url segment的概念。如对于url:http://mydomain.com/admin/index 它的segment有2个。第一个是admin,第二个是index。基于segment,关于Route匹配规则,有3点特征:1)保守,它只匹配包含相同个数segment的url(Route配置中有默认值或者是optional的例外);2)开明,只要segment个数相同,它就接纳(有特殊constraint配置除外);3)知足,只要一个url匹配了前边的Route,后面的就不在去匹配。下面是示例: 

      1. 最简单的做法是:

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

 当然,相同作用但更常用的写法是:

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

这里myRoute参数是optional。 下边讲解一些更实际的应用。

      2. 定义默认值:

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

对于上述route配置,http://mydomain.com、http://mydomain.com/customer、http://mydomain.com/customer/list均能匹配上,当controller和action对应的segment部分没有指定时,采用默认值(Home、action),否则采用匹配的segment值。但 http://mydomain.com/customer/list/all 不行,因为它的url segment多于route配置的2个。

      3. 包含静态url片段:

      routes.MapRoute("staticFolderRoute""Public/{controller}/{action}");

它要求url有3个segment,并且第一部分必须是public。如:http://mydomain.com/public/customer/list可以匹配,但http://mydomain.com/customer/list/all不行。同时,请注意:public将不会保存在RouteData中。

      除了将静态片段前置外,还可以将它混合在route配置中:

      routes.MapRoute("mixedSegmentRoute""X{controller}/{action}");

可以看到,X和controller混在一起,它所匹配的url的第一片段必须以X开头,如:

http://mydomain.com/xcustomer/list。在RouteData中controller的值当然也就不包含X了。

 

      虽然controller和action是2个内置的route key,并且一般情况下它们并存于route配置中。但是它们并不是必须都存在于route的url配置规则中,如下:

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

将controller直接置死为Home。 而根据示例的配置,发散一下可做它用。试想一下:如果站点的某个action(假设为OldAction)不再被使用了,而它所对应的url已被搜索引擎收录或者不想让它404,这个时候除了可以让action跳转走,还可以通过设置route:

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

后,再次访问它时,就直接映射到了Home/Index了,但是url然为Shop/OldAction。

      4. 定义custom片段:

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

所谓的custom片段,就是除了controller和action的其他的那些,如上文的id。

      5. optional片段:

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

特别之处在于http://mydomain.com/public/customer虽然只包含2个segment,但是它可以匹配上述route的。同时,optional segment和default value segment区别的一点在于,同一url(如http://mydomain.com/public/customer)对于前者,RouteData["id"]是不存在的即RouteData中压根就没有id这一个key,后对于后者它是有的,只不过value为默认值。

     6. segment数可变的route:

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

该segment配置以*开头。http://mydomain.com/Customer, http://mydomain.com/Customer/List/All/Delete/Perm均能匹配上它,前者RouteData对应值为null,后者为Delete/Perm。

      7. 在route中使用namespace:

      Route系统默认情况下,只核对controller和action的名称,而对它们所在的namespace则视而不见。当项目中有相同的controller和action名称(如应用到Area时),此时若一个url刚好经过某个route匹配上它,则会报错,因为它不知道采用哪一对。这时就可以去设置核对的优先级了:

      routes.MapRoute("PrioritizeNamespaceRoute""{controller}/{action}/{id}/{*catchall}",
          new { controller = "Home", action = "Index", id = UrlParameter.Optional },
          new[] { "URLsAndRoutes.Controllers" });

它会优先去从namespace:URLsAndRoutes.Controllers下去搜寻指定的controller和action,如果没有匹配上再去从别的namespace。

       注意,MapRoute方法最后那个参数虽然是数组,但实质上如果你设置了多个namespace,而敲好又有多个有相同名称的controller和action,那么仍然会报相同的异常,因为数组参数中的namespace它们之间又完全是平等的,没有优先级。解决方法是设置多个route,如下:

      routes.MapRoute("PrioritizeNamespaceRoute""{controller}/{action}/{id}/{*catchall}",
                new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                new[] { "URLsAndRoutes.Controllers" });
      routes.MapRoute("PrioritizeNamespaceRoute""{controller}/{action}/{id}/{*catchall}",
                new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                new[] { "AdditionalControllers" });

它们会优先去匹配URLsAndRoutes.Controllers,然后去匹配AdditionalControllers,再去匹配其他的。

      如果要限定controller所在的namespace,怎么办?

      Route myRoute = routes.MapRoute("AddContollerRoute""Home/{action}/{id}/{*catchall}",
                new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                new[] { "AdditionalControllers" });
      myRoute.DataTokens["UseNamespaceFallback"] = false;
      routes.Add(myRoute);

myRoute会仅在AdditionalControllers中查找所指定的controller和action,如果没有直接退出route系统并报错。

      8. 条件限定Route:

      通过正则表达式,可以对route中的各个segment所对应的值进行限定:

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

它限定controller必须以H开头,action只能是index或者about,而http method只能是get方式(这里httpMethod名称可以是其它的,MVC Framework只根据它的值类型HttpMethodConstraint来判断和取值)。

      除了上述限定方法外,你还可以通过实现IRouteConstraint接口,自定义限定方式。如下:

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

然后,使用如下:

     routes.MapRoute("CustomConstraintRoute""{controller}/{action}/{id}/{*catchall}",
         new { controller = "Home", action = "Index", id = UrlParameter.Optional },
         new { controller = "^H.*", action = "Index|About", httpMethod = new HttpMethodConstraint("GET""POST"), customConstraint = new UserAgentConstraint("IE") },
         new[] { "URLsAndRoutes.Controllers" }
     );

可以看到,只有IE浏览器才能去访问这个route所对应的url了,当然这仅仅是一个demo而已,实际应用并不合适。

      9. 用于物理文件的route:

      MVC Framework的RouteTable.Routes对象有一个RouteExistingFiles属性,默认情况下它为false,即不对物理文件使用route系统。如果要使用,可以设置它为true,然后指定route。如下:

    routes.RouteExistingFiles = true;
    routes.MapRoute("DiskFile""Content/StaticContent.html",
        new { controller = "Account", action = "LogOn" },
        new { customConstraint = new UserAgentConstraint("IE") });

当访问http://mydomain.com/content/staticcontent.html时,它实际显示的是http://mydomain.com/account/logon对应的内容。

     注意,不要轻易改变RouteExistingFiles的值,如果改变,务必保持其他不走route的静态内容可以访问到,如设置:

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


     二、自定义route系统:

     1. 通过继承RouteBase,创建新Route:

    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;

            VirtualPathData result = null;
            if (values.ContainsKey("legacyURL") && urls.Contains((string)values["legacyURL"], StringComparer.OrdinalIgnoreCase))
            {
                result = new VirtualPathData(thisnew UrlHelper(requestContext).Content((string)values["legacyURL"]).Substring(1));
            }
            return result;
        }
    }

可以看到在GetData方法中,暗度陈仓地指定了controller和action的值,另外还存入了一个legacyURL。

     2. 实现controller和action:

    public class LegacyController : Controller
    {
        public ActionResult GetLegacyURL(string legacyURL)
        {
            return View((object)legacyURL);
        }
    }

     3. 配置Route:

    routes.Add(new LegacyRoute("~/articles/Windows_3.1_Overview.html"));

这样就简单完成了一个自定义Route系统。效果就是:当浏览http://mydomain.com/articles/Windows_3.1_Overview.html 时会调用刚创建的controller和action,用相同目录其他html文件来访问时,报错404.

     4. 除了上述方案,还可以实现自己的RouteHandler:

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

应用如下:

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

这时访问http://mydomain.com/sayhello,屏幕打印出Hello。

 

      三、使用Areas:

      在vs2010的mvc项目中点右键创建Area,比如为Admin。创建完后,它就又一套自己的结构了,如下:

 

 这个时候,如果有相同的controller和action,就要用到一种描述的在route中指定优先namespace了。另外,需要注意的是view中使用Html.ActionLink方法构造超链接时,需指定area了,如:@Html.ActionLink("Got to Admin Area""Index"new{ area = "Admin"} 。

    源码download