《ASP.NET本质论》 MVC

  模型-试图-控制器(MVC)体系结构模式将应用程序分成三个主要组件:模型、视图和控制器。ASP.NET的MVC框架提供ASP.NET Web窗体模式的替代开发模式,是一个可测试性非常高的轻型框架。

 ASP.NET MVC是表现层的MVC
  
  经典的WebForm为我们提供了一个高效的Web开发模式,但是,基于ASP.NET的Web开发模式并不只有一种模式。2009年3月,微软发布了ASP.NET MVC 1.0,2010年5月,发布了ASP.NET MVC 2.0 。随着Visual Studio 2010的发布,ASP.NET MVC集成进了Visual Studio 2010中。MVC为我们听哦跟了另外一种Web开发的选择。
  MVC是Xerox PARC在20世界80年代为编程语言Smalltalk—80发明的一种软件设计模式。ASP.NET MVC是基于MVC设计模式、解决 .NET环境下Web开发的又一种技术。MVC之间的关系如图所示。
 
  不管是Web Form还是ASP.NET MVC都是解决Web开发中用户界面层的问题,也就是经典的UI问题。
  在经典的Web Form模式中,页面的现实效果通过.asp模板进行描述,请求的处理逻辑和页面显示所需的数据在后台代码文件中处理。在MVC模式中,模型表示软件处理的数据描述,试图iaoshi软件的交互界面,控制器用来控制软件的处理逻辑。MVC将软件的这三个方面明确进行了划分,以便于复杂软件的组织。经典的WebForm相当于将控制器和模型合并在一起。
  不同于经典的WebForm模式,ASP.NEt MVC遵循MVC设计模式,明确将Web层的处理划分为职责明确的三个部分,使得三个部分各司其职,简化了软件每个部分的复杂性,提高了软件的可测试性。
  典型的MVC模式将一个应用实现分成三个角色的框架技术:模型、视图和控制器。
  Model(模型)是负责保持状态的角色。这个状态在数据库中通常是持久的,例如,Product类用来代表关系数据库中产品数据。
  View(视图)是负责显示用户界面的组件。这个界面通常是使用模型数据来创建的,例如,产的编辑视图,根据当前Product对象的状态,显示一个用户编辑界面。
  Controller(控制器)是处理用户交互、操作模型和最终选择用哪个视图来显示处理结果的组件。
  三个组件之间通过契约进行协作,这使得针对界面层的测试可以方便地进行。


 在HttpApplication中的ASP.NET MVC 
  从 .NET 3.5 SP1 开始,引入了System.Web.Routing 程序集。通过Url Routing的机制,将针对一个虚拟路径的请求映射到一个Action方法上,使得用户可以使用一个友好的URL来访问ASP.NET网站。例如,用户请求的地址为: http://www.petshop.com/prouct/1 ,但是,服务器实际上可能通过调用名为Product的控制器的名为Detail的Action方法来获得实际的回应数据,并用Detail.aspx来返回查询的结果。
        在ASP.NET MVC中,Ruote类指定ASP.NET应用程序中对虚拟路径请求的处理方式,可以为每种URL模式创建一个Route对象。Route类的定义如下:

public class Route : RouteBase

        为了完成针对请求的路由工作,在ASP.NET MVC中引入了称为路由表的数据结构来定义各种URL到实际处理程序之间的映射。在ASP.NET MVC中,这个路由表的类型为RouteTable。RouteTable的Routes'shi一个static类型的属性,它的类型是RouteCollection,从类的命名就可以看出,这是个强类型的Route对象集合,用来表示应用程序中所有的路由。
public class RouteTable
{
    public static RouteCollection Routes { get; }
}

        为了在HttpApplicaiton的处理管道中将普通的请求护理转换到MVC的处理总,UrlRoutingModule注册了HttpApplication的PostResolveRequestCache事件 和PostMapRequestHandler事件,以便将URL映射到MVC的处理程序中,使得请求进入ASP.NET MVC的处理中。

    
    
    创建RoteTable
    
        一个网站应用程序只会有一个路由表,针对请求的路由表必须在请求被处理之前提前创建,默认情况下,创建路由表的工作将在Global.asax.cs文件中的 Application_Start中完成。下面的代码包含了使用 Visula Studio 2010新建ASP.NET MVC Web应用程序时默认的Global.asax.cs文件。
public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default", // 路由名称
                "{controller}/{action}/{id}", // 带有参数的 URL
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // 参数默认值
            );

        }

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);
        }

        应用程序是用的路由表由RoteTable表示。RouteTable的Routes属性表示了路由对象的集合。爱上便的Global.asax.cs文件中,我们在应用程序首次处理请求之前为路由表增加了两个路由对象。
        在路由表内部通过路由对象来表示URL到Handler的映射。在上面的哦代码中,我们创建了两个路由对象。第一个路由是一个用于忽略特殊请求的路由,这个路由忽略素有扩展名为.axd的请求,这些请求将被按照经典的方式进行处理。这样,经典的一些特殊类型的请求,例如:Trace.axd.WebResource.axd等将被路由忽略,还会按照经典的放出进行处理。第二个路由映射任何符合{ controller }/{ action }/{ id } 模式的URL到MvcRouteHandler。其中第二个路由还提供了一个默认的参数,如果明确写出的话,应该是如下形式:
       
routes.MapRoute(
                "Default", // 路由名称
                "{controller}/{action}/{id}", // 带有参数的 URL
                new { controller = "Home", action = "Index", id = UrlParameter.Optional },// 参数默认值
                new MvcRouteHandler()
            );

        当我们请求经典 ASP.NET 应用程序页面的时候,通常情况下,每一个页面请求都会映射到网站的一个Web Form 。例如,如果我们请求一个叫做Product.aspx的页面,在Web服务器上应该存在一个叫做 Product.aspx的页面。 ASP.NET应用程序的工作方式是我们向页面发出请求的时候,由HttpApplication找到页面所在的处理程序PageHandlerfactory然后,由PageHandlerFactory通过请求的页面名称定为到具体的页面处理程序,最后执行由Web Form生成的处理程序,从而处理请求,把处理结果发回浏览器。
        ASP.NET MVC应用程序不是以这种方式工作的。当我们请求一个ASP.NET MVC应用程序的地址时,网站中不一定存在对应请求的页面。一般来说,对于ASP.NET MVC网站程序,我们甚至都不会再请求着 .aspx 扩展名的地址,而会请求一个有意义的虚拟地址,ASp.NET MVC通过路由表,将这个请求转发到一个叫做控制器的类上,控制器负责生成内容并把它发回浏览器。
  当我们编写经典ASP.NET应用程序的时候,会创建很多页面。URL和页面之间是意一一对应的,每一个页面请求对应相应的页面。与此相反,当我们创建爱你ASP.NET MVC应用程序的时候,创建的是一批控制器。使用控制器的优势是可以在URL和页面之间有多对一的映射。例如,所有如下的URL都可以映射到相同的控制器上。
http://www.petshop.com.product/1
http://www.petshop.com.product/2
http://www.petshop.com.product/3
        这些URL可以被映射到一个控制器上,通过从URL中提取产品ID来显示正确的产品。这种控制器比传统的ASp.NET方式更灵活。


    UrlRoutingModule事件处理

         那么,ASP.NET MVC网站应用程序又是如何通过路由表将请求映射到ASP.NET MVC中,然后在ASP.NET的处理管道中进行处理的呢?UrlRoutingModule负责解决这个问题。
        在ASP.NET MVC中,HttpApplication的处理管道并没有变化,变化的只有处理部分。UrlRouting的目的是将搜索引擎友好的URL映射到服务器内部的特定处理程序上,而不是完全按照经典的文件映射方式来寻找处理程序,所以,必须在执行处理程序之前,将HttpApplication经典的查找处理程序的方式进行修改,以便完成映射任务。
        由于在经典方式下,会通过请求路径来定为具体的处理程序或者文件,如果不能通过请求路径找到对应的处理程序或者文件,将会直接返回文件未找到的404错误。所以,在HttpApplication徐兆处理程序之前,必须设置一个肯定可以找到的处理器的请求路径,以便满足HttpApplication的需要,这个时间点显然就是PostResolveRequestCache事件。
        在HttpApplication完成处理后层序的查询,并且成功初始化处理程序之后,我们再通过路由表,将真正的处理程序设置到HttpApplication中,以便重新回到Application的处理管道中。在HttpApplication的处理管道中,标志这个时间点的事件就是PostMapRequestHandler。
        当我们对ASP.NET MVC应用程序发起请求的时候,请求被送入HttpApplication的处理管道,UrlRoutingModule注册了HttpApllication的PostResolverRequestCache事件和PostMapRequestHandler事件。
       UrlRoutingModule在PostResolverRequestCache事件处理中做的第一件事情就是包装当前的HttpContext为HttpContextWrapper2对象。HttpContextWrapper2类和派生自HttpContextBase的普通HttpContext类不同,哟与HttpContext对象是一个高度复杂的对象,所以,在单元测试中,很难对它进行模拟,创建的HttpContext的包装可以使得在单元测试中的模拟变得更简单。
        接着,UrlRoutingModule把包装后的HttpContext传给在之前步骤中创建的RouteTable。通过请求参数在路由表中匹配路由对象,然后返回一个匹配的路由对象RouteData。如果UrlRoutingModule成功获取了RouteData对象,UrlRoutingModule然后就会创建表示当前HttpContext和RouteData的RouteContext对象,然后把RouteContext传给Handler的构造函数,实例化基于RouteTable的新HttpHandler。由于HttpApllication马上还要进行一次处理程序的查询,所以这个路由对象将被通过HttpContext对象的Items集合保持起来,以便在PostMapRequestHandler事件中使用。
        最后,通过HttpContext的RewritePath方法,将当前请求的路径设置为一个肯定存在的路径:~UrlRouting.axd,以满足HttpApplication的需要,这样,它将不会返回找不到页面。
         当PostMapRequestHandler事件触发的时候,ASP.NET已经完成了经典的获取处理程序的操作,但是,显然不可能获取ASP.NET MVC的处理程序,UrlRoutingModule将检查通过HttpContext对象的Items传递的路由对象,如果成功的话,那么将通过路由对象的处理程序来重新设置当前的处理程序。


     从URL进入MVC之门

         Web处理都是从请求开始的。对于ASP.NET MVC来说,此时的请求不同于WebForm下的请求了。
         在WebForm模式下,通常我们的请求的扩展名为.aspx 或者 .ashx。这些特殊的扩展名被IIS映射到 aspnet_isapi.dll 中,然后请求通过这个隧道被传递到 .NET 网站应用程序开始处理的过程。
         在请求中,我们有下类几种方式来传递请求参数:
         Form中的请求参数
         附带在URL地址上的,通过?分隔的参数
        Cookie参数
        视图状态

       对于Form参数和视图状态来说,显然必须通过页面中的表单来提交这样一次请求,Cookie则必须是预先保留在客户端的。那么,用户的第一次请求如何开始呢?只能是通过在地址栏中输入的一个地址,或者是在搜索引擎中搜索出来的一个地址。
         如果我们提供一个产品的展示网站,而用于必须先通过一个产品的查询页面,然后,在提交某个请求之后,通过Post方法传递了一组Form参数回到服务器,用户需要查询的产品才能在网页上显示处理。那么,对于搜索引起来说,就会难以搜录查询具体产品的地址,很难帮助用户直接查询到所需的产品信息。
        在这个搜素时代,我们希望每一种产品都可以通过一个简单、明确、易记的地址标记出来,这样用户可以直接在地址栏中输入这个对应的地址来访问产品的信息,搜素引起也可以更加容易地将这个地址收录下来,以便用户通过搜索引起进行查询。这个工作成为搜索引擎优化,简称为SEO。
        例如,在WebForm模式下,我们可能这样查询产品。首先,在地址栏中输入如下网址:
        http://www.petshop.com/product.aspx
        然后,用户在产品的搜索窗口中输入了产品的名称appler,提交之后,appler被通过Form参数传递到了服务器上。由于我们通常使用Post方式提交表单,之后,在浏览器的地址栏中显示的实际上还是刚才的地址。
       虽然,此时用户看到了查询的结果,但是用户要经过两步操作之后,其中还包含了输入产品的名称,才能得到需要的结果。
       在ASP.NET MVC模式下,用户可能需要的是输入如下的地址即可:
         http://www.petshop.com/product/apple
     这样,用户只需要一部就可以得到需要的结果,而这个地址也可以被搜索引起所收录,用户只需要在搜索引擎的查询结果中点击就可以看到需要的结果。显然,这可以给用户更加美好的体验。


     有意义的URL

         在ASP.NET MVC中,URL不仅仅表示 一个地址,更重要的是URL本身的意义。比如说,我们希望IETF的网站上查询rfc2612的文件,取得纯文本格式的文件,可以通过这个地址得到  http://www/ietf.org/rfc/rfc2612.txt 
        如果希望得到pdf格式的文件,则希望使用这样的地址:http://www/ietf.org/rfc/rfc2612.pdf
        但是,IETF的碗盏上却显示文件不存在。。。。
        实际上,应该使用如下的方式取得: http://tools.ietf.org/pdf/rfc2616
        而要取得HTML格式的文件,则需要使用这种方式  http://tools.ietf/org/html/rfc2616
        在IETF网站中,我们可以看到,可以通过不同的URL地址来得到不同格式的文件,此时的URL已经不是一个间的服务器上文件的名称了。


    在IIS 6.0  和 IIS7 中的配置
        
        在IIS 6.0 中,需要通过请求的扩展名将不同的请求映射到不同的应用程序扩展中,例如,对于经典的ASP.NET网站,我们需要将 .aspx 扩展名映射到 aspnet_isapi.dll中。这个映射在我们安装 .NET 的时候,就已经由安装程序设置到IIS的应用程序配置中了,如下图:

        对于我们刚刚创建的有意义的URL来说,地址中根本就没有扩展名。对于这些特殊的请求,我们可以通过通配符应用程序映射将所有的请求映射到 .NET 网站应用程序中来,如下图

        注意,一定不要选中“检查文件是否存在”复选框
        在 IIS7 中,网站应用程序被分为两种类型运行模式:经典模式和集成模式。在应用程序池配置界面中可以对运行模式进行调整的。在集成模式下, .NET 网站可以参与到IIS处理过程中,对于MVC项目来说,不需要进行文件扩展名的映射配置,就可以将请求传递到 .NET 网站应用程序中。
       IIS7 中默认使用的是集成管线模式,默认情况下经典模式也被支持,所以就有两套扩展名与处理程序的映射配置:一套用于经典模式,一套用于集成模式。



    在URL到Route 

        在请求到达 .NET 网站之后,请求参数将被封装为 HttpRequest类型的对象,然后,网站将创建HttpContext对象实例封装处理此次请求所需的参数对象。这个对象随后被传递给HttpApplication类型的对象,进入ASP.NET的处理管道。
        在ASP.NET MVC中,从URL 到RouteData的映射通过Route对象表示,需要首先在RouteTable中注册Route信息,RouteTable中保存了当前应用程序的路由信息。具体来说,RouteTable的静态属性Routes包含了当前应用程序用于从URL映射到处理请求的所有路由信息。添加到路由表的路由顺序非常重要,应用程序将会从前往后地在路由表中查找匹配当前请求的路由对象,所以,短的、特殊的路由应该加入到路由表的前方,一般化的路由应该在后面加入。在应用程序启动的时候,我们需要将所有的映射添加到路由表中,这个工作一般在Global.asax中进行。
        所有的Route必须派生自基类RouteBase,RouteBase的定义如下:
namespace System.Web.Routing
{
    public abstract class RouteBase
    {
        protected RouteBase();

        public abstract RouteData GetRouteData(HttpContextBase httpContext);
        public abstract VirtualPathData GetVirtualPath(RouteDirection values);
    }
}

        每个路由对象必须能够通过GetRouteData获取对应的路由数据,而GetVirtualPath用来检测请求参数,查看当前的路由对象是否匹配这个请求。
        在Route中,实际的路由数据被以RoteData的类型保持。
namespace System.Web.Routing
{
    public class RouteData
    {
        public RouteData();
        public RouteData(RouteBase route,IRouteHandler routeHandler);

        public RouteValueDictionary DataTokens { get; }
        public RouteBase Route { get; set; }
        public IRouteHandler RouteHandler { get; set; }
        public RouteValueDictionary Values { get; }

        public string GetRequiredString(string valueName);
    }
}

        我们使用的Route派生自RouteBase,提供了许多不同的构造函数,使得我们可以以不同的方式来构建Route对象。这里面设计以下几个主要的参数:
        URL:请求参数的模板
        routeHandler这个请求的处理器对象
        默认哦处理参数
        另外,Contraints属性提供了约束URL的信息,用于限制URL的范围。
         我们通常通过RouteCollection的MapRoute扩展方法来加入一个路由,这个方法提供了6个重载来方便我们添加路由。使用这种方法计入的路由将会使用一个默认的路由处理程序MvcRouteHandler来处理路由。
       不使用MapRoute,而是自定义一个Route对象,将会允许我们有更大的灵活性。例如,我们可以创建一个使用自定义路由处理器的路由对象。
RouteTable.Routes.Add(
                new Route(
                    "{controller}/{action}/{id}",
                    new RouteValueDictionary(
                        new {Actin="Index",id=(string)null}),
                    new ShowRouteHandler()
                    )
                );

        在ASP.NET MVC 中,我们通常在URL地址中包含请求希望使用的控制器的名称和在控制器中执行的动作,在其后附加上执行动作所需要的参数。在ASP.NET MVC中提供了一个默认的URL格式,以便于我们开始工作。如果使用默认路由的话,在IETF网站中你哦个霍格PDF格式的RFC2612的地址可能需要如下形式: http://tools.ietf.org/rfc/get/rfc2616/pdf
        rfc表示IETF网站中处理rfc文件的控制器,get表示要获取网站中文件的动作,rfct2612则是文件的编号,而pdf则表示文件的哦格式。不过,这个地址有些太冗长了。
        显然我们也可以规定,入股RUL地址中仅仅提供了两个参数,则可以假定一定需要使用文件管理的控制器,而此时使用的方法一定是从服务器获取,这两个参数一次是二年间的格式和文件的编号。那么。此时的URL模板可能需要如下形式。
             {format}/{id}
       也就是,URL不一定要使用默认的方式来构成。但是不管怎么构成,你会发现,现在URL路径中没有原来的文件扩展名了。
       刚才谈到我们希望使用更加灵活的URL地址,而不总是默认的{ controller }/{  action }/{ id }。例如,我们希望使用下面的URL地址,pdf表示文件的格式,而rfc2616则表示标准的编号。在这个地址中,没有明显的Controller和Action。但是,在MVC程序中处理的时候,我们希望那个通过一个名为RFC的控制器来控制请求的处理,通过名为GET的方法来取得指定的文档,而rfc2616和pdf将是这个Action的参数如下:
            http://tools.ietf.org/pdf/rfc2616
        在这种情况下,URL包含了一些隐含的信息来制定处理的Controller 和 Action
        在MVC中,通过Route来表示请求,然后,通过Route再映射到实际物理的Controller和Action上。可以通过路由映射将这种URL映射到特定的Controller和Action进行处理,下面的路由映射将URL地址翻译为路由对象,通过默认的参数来制定Controller和Action。
           
routes.MapRoute(
                "rfc",
                "{type}/{rfcid}", 
                new {controller="rfc",action="get" }
                );
        在大型网站中,往往存在众多的Controller,从ASP.NET MVC 2.0 开始,提供了Area用来对大型网站的支持。Area用于对Controller进行逻辑分组,这个问题也同样通过Route进行了映射,这时候的URL如下所示:
            "AreaName/{ controller }/{ action }/{ id }"
        在ASP.NET MVC中,通过Route中增加一个Area属性来解决这个问题,这个属性通过接口IRouteWithArea来指定。
namespace System.Web.Mvc
{
    using System;

    public interface IRouteWithArea { string Area { get; } }
}

        不过也可以来自DataTokens中的area来表示Area名称。
        DataTokens["area"]
        含有Area的网站,默认生成了一个名为 XXXX AreaRegistration.cs的代码文件,用于注册含有Area的URL映射。注册含有Area的路由如下所示,这个类派生自AreaRegistration,通过重写AreaName提供了当前Area的名字,这样,以后就可以通过这个名字来匹配Area,在这个Area中查询相应的控制器。
public class BlogAreaRegistration : AreaRegistration
{
    public override string AreaName
    {
        get { return "Blog"; }
    }

    public override void RegisterArea(AreaRegistrationContext context)
    {
        context.MapRoute(
            "Blog_default",
            "Blog/{controller}/{action}/{id}",
            new { action="Index",id=UrlParameter.Optional}
            );
    }
}

           在Global.asax中增加了注册含有Area的路由。

protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);      
        }

        我们也可以通过MapRoute提供一个默认值,当URL中没有提供这部分信息的时候,将使用默认值作为当前的值。例如,在前面的Default路由中Controller的默认值为Home,Action的默认值为Index,当用户使用/作为URL请求网站的时候,将使用名为Home的Controller来处理请求,调用名为Index的Action来完成具体的处理。大多数情况下,这种默认值为我们带来了方便。
         但是,对于id来说,是这么情况呢?如果我们设置默认值如下:
             
routes.MapRoute(
                "Default", // 路由名称
                "{controller}/{action}/{id}", // 带有参数的 URL
                new { controller = "Home", action = "Index", id = "" },
                new MvcRouteHandler()// 参数默认值
            );
              然后,使用一个简单的Model来传递参数。
public class HomeModel
{
    public int Id { get; set; }
}

        就意味着,在URL中没有提供id部分的话,那么id将使用默认的空串,也就是说,在路由数据中将提供一个值为空串的id条目。在Action中传递参数的时候,这个值会被应用到参数上。
        有的时候,我们需要当URL中没有提供id部分的话,视同确实没有提供这个参数,而不是在Routing中听过默认值,在RouteData中也不出现这个参数。这样的话,在绑定到Action参数的时候,就会认为不存在这个参数,而不对这个属性进行绑定,属性将会取Model的默认值。
<ul>
    <li><a href="/Home/ActionWithdId/10">Action with Id</a></li>
    <li><a href="/Home/ActionWithId/">Action with Nothing</a></li>
</ul>

        在第一种情况下,传递到RouteData中将会有三个参数,而在第二种情况下,RouteData将会只有两个参数。




    约束

        在上面的定义之后,会发现原来有效的Home/Action出现了问题,不能进行路由了在设置的路由中,使用了两个参数,Home/About也是两个参数,所以,两个参数的请求被路由到rfc上来了。
       注意,我们仅仅需要处理类型为pdf,html类型的文档,所以,我们可以限制type参数必须为pdf和thml之一的是,才会使用我们的路由,这可以通过约束来完成。
       路由约束可以用来限制匹配一个特殊路径的请求,约束运行使用正则表达式来进行设置。对于我们的问题,设置type必须为pdf和html之一的正则表达式为 pdf | html ,如下设置之后,Home/About可以正常工作了。
 routes.MapRoute(
                "rfc",
                "{type}/{rfcid}",
                new { controller = "rfc", action = "get" },
                new { type="pdf|html"}
                );

        除了可以在MapRoute中直接指定约束之外,也可以自定义一个约束对象来完成约束。
public class DocumentTypeConstraint : System.Web.Routing.IRouteConstraint
{
    private static readonly System.Text.RegularExpressions.Regex regex = new System.Text.RegularExpressions.Regex("^(pdf|html)$");

    #region IRouteConstraint Members

    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        bool result = true;
        if ((routeDirection == RouteDirection.IncomingRequest) && (parameterName.ToLower(CultureInfo.InvariantCulture) == "type"))//using System.Globalization;
        {
            System.Text.RegularExpressions.Match match = regex.Match(values[parameterName] as string);
            result = match.Success;
        }
        return result;
    }

    #endregion
}

        然后,在Global.asax中使用这个约束类
 routes.MapRoute(
                "rfc",
                "{type}/{rfcid}",
                new { Controller = "rfc", Action = "get" },
                new {type=new DocumentTypeConstraint() }
                );

        最后,所有的路由对象都会被加入到管理路由对象的对象RouteTable中。RouteTable对象的经他属性Routes为RouteCollection类型。



    Routing

        当PostMapRequestHandler事件触发的时候,ASP.NET已经完成了经典的获取处理程序的操作,但是,显然,不可能通过传统方式获取ASP.NET MVC的处理程序,UrlRoutingModule将检查通过HttpContext对象的Items传递的路由对象,如果成功获取的话,将通过路偶对象的处理程序来重新设置当前的处理程序。
        对于ASP.NET MVC程序来说,将会创建一个实现IRouteHandler接口的对象,这个对象将被用来作为处理程序使用。不过,此时的处理程序并不像在经典的ASP.NET时代,直接用于处理请求,而是用于获取一个真正的处理程序,定义在命名空间System.Web.Routing下的解耦IRouteHandler用于完成这个任务。

public interace IRouteHandler
{

IHttpHandler GetHttpHandler(  RequestContext requestContext);

}

        在ASP.NET中,默认的处理对象如下:
         public class MvcRouteHandler:IRouteHandler
        默认情况下,MvcRouteHandler将会创建一个MvcHandler对象开始吹过程。
       public class MvcHandler:IHttpAsyncHandler,IHttpHandler,IRequiressSessionState
       如果在创建路由表的时候不使用MapRoute,而是通过创建Route对象的方法,那么,可以自行制定特定的路由处理程序。



    RequestContext的前世今生

        在经典的ASP.NET中,HttpRequest,HttpResponse,HttpContext都是非常复杂的类型,我们设置都难以在测试程序中创建一个对象实例,这是的经典的ASP.NET中,很难实现测试驱动的单元测试。
        从ASP.N 3.5 SP1 开始,在ASP.N中定义了一组新的类型,以增强对于测试的支持。这组新的类型通过分别提供一个抽象的基类,使得我们可以简单的创建一个用于测试的派生类来完成测试工作。
       表示请求参数的基类HttpRequestBase定义了经典的HttpRequest同样的成员,但它现在是一个抽象类,允许我们集成。
 
            public abstract class HttpRequestBase

        在ASP.NET MVC使用的是它的派生类HttpRequestWrapper。
         public class HttpRequestWrapper:HttpRequestBase
        同样回应对象的基类为HttpResponseBase,包含了与HttpReponse同样的内容。
          public abstract class HttpResponseBase
        它的派生类HttpResponseWrapper就是在ASP.NET  MVC中使用的实现。
         public class HttpResponseWrapper:HttpResonseBase
        而重新定义的HttpContextBase则是将这些参数组织起来,构成一个可测试的上下文类型。
            public abstract class HttpContextBase:IServiceProvider
        一个新的Wrapper也用来实现这个抽象类。
          public clas HttpContextWrapper:HttpContextBase
        ASP.NET MVC中的基本类型见下表:

经典的类型

抽象基类

实现

HttpRequest

HttpRequestBase

HttpRequestWrapper

HttpResponse

HttpResponseBase

HttpResponseWrapper

HttpApplicationState

HttpApplicationStateBase

HttpApplicationStateWrapper

HttpServerUtility

HttpServerUtilityBase

HttpServerUtilityWrapper

HttpSessionState

HttpSessionStateBase

HttpSessionStateWrapper

HttpContext

HttpContextBase

HttpContextWrapper

        需要注意的是,在ASP.NET MVC 中,经过Routing之后,相比经典的ASP.NET 模式,增加了Routing信息,经典的HttpContext中没有Routing信息,RequestContext在HttpContext的基础上,增加了Routing数据。

public class RequestContext
{
    public HttpContextBase HttpContext { get; internal set; }
    public RouteData RouteData { get; internal set; }
}




    在ASP.NET MVC 中防盗链
  防盗链是网站的一个常见技术,下面的程序通过一个自定义的路由处理程序来完成对图片盗链的检测,并进行相应的处理。
        防盗链在处理程序中进行,在这个例子中,我们通过检查用户的上一次请求是否来自本站cnblogs.com来确定是否是一次盗链请求,参见代码:

namespace AntiTheft
{
    public class AntiTheftHandler:IHttpHandler
    {

        public AntiTheftHandler(System.Web.Routing.RequestContext requestContext)
        {
            ProcessRequest(requestContext);
        }

        private void ProcessRequest(System.Web.Routing.RequestContext requestContext)
        {
            HttpContextBase context = requestContext.HttpContext;
            HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;
            HttpServerUtilityBase server = context.Server;

            string imageName = requestContext.RouteData.Values["imagename"].ToString();
            var imageFolder = server.MapPath("~/images/");

            //检查是否盗链请求
            string referrer = request.ServerVariables["HTTP_REFERER"];

            response.Clear();
            response.ContentType = "image/jpeg";

            if (referrer != null && referrer.Contains("cnblogs.com"))
            {
                //设置MIME
                response.ContentType = GetContentType(request.Url.AbsolutePath);

                //正常访问
                response.TransmitFile(System.IO.Path.Combine(imageFolder, imageName));//将指定的文件直接写入 HTTP 响应输出流,而不在内存中缓冲该文件。 
            }
            else
            { 
                //显示警告图片
                response.TransmitFile(System.IO.Path.Combine(imageFolder,imageName));
            }
            response.End();
        }

        private string GetContentType(string url)
        {
            switch (System.IO.Path.GetExtension(url))
            { 
                case ".gif":
                    return "image/gif";
                case ".jpg":
                    return "image/jpeg";
                case ".png":
                    return "image/png";
                default:
                    break;
            }
            return null;
        }


        #region IHttpHandler Members

        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
        }

        #endregion
    }
}

        这个处理程序将被一个路由处理程序进行调用,这个RouteHandler的定义如下:
namespace AntiTheft
{
    public class AntiTheftRouteHandler:System.Web.Routing.IRouteHandler
    {
        #region IRouteHandler Members

        public IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
        {
            return new AntiTheftHandler(requestContext);
        }

        #endregion
    }
}

        只有在路由表注册之后,路由处理器才能发挥作用。我们的路由处理器匹配的参数只有两个,所以,必须注册在默认的路由处理器之前,否则将会永远匹配不上。
        通常情况下,ASP.NET 网站允许直接访问一些静态文件,例如:图片文件、CSS文件等。RouteCollection中有一个非常强大的属性 RouteExistingFiles,可以用来设置是否针对存在的文件进行路由。通过将这个属性设置为TRUE,可以阻止直接针对网站中文件的直接访问。无论是js还是css,或者是图片文件都将需要经过路由处理。注意,一定要设置RouteExistingFiles为真,以便将所有针对静态文件的请求通过路由处理。
 routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.Add(
                "ImageRoute",
                new Route("images/{imagename}",new AntiTheftRouteHandler())
                );

            routes.RouteExistingFiles = true;

        在这种情况下,需要对所有的静态文件分配一个路由处理器进行处理。




     控制器

        在默认情况下,MvcRouteHandler是标准的路由处理程序,这个处理程序将会创建一个MvcHandler的对象实例,在构造函数中,将会把当前的请求参数对象传递给这个实际的处理对象来处理当前的请求,MvcHandler是一个标准的处理程序,但是,它唯一的构造函数需要一个RequestContext类型的参数,所以,并不能被注册到网站的处理程序列表中。
       在MVC模式下,将会通过实现IControllerFactory接口的对象来获取当前请求的控制器对象。
namespace System.Web.Mvc
{
    using System.Web.Routing;

    public interface IControllerFactory 
    {
        IController CreateController(RequestContext requestContext,string controllerName);
        void ReleaseController(IController controller);
    }
}

        实现IControllerFactory接口的对象是控制器的创建工厂,这个工厂通过ControllerBuilder提供给MvcHandler使用。ControllerBuilder的Current属性获取当前的ControllerBuilder对象实例,这个类提供两个方法用户设置或者获取当前的控制器工程。
public void SetControllerFactory(IControllerFactory controllerFactory)
        public IControllerFactory GetControllerFactory()

        默认情况下,在ControllerBuilder内部将会创建一个DefaultControllerFactory类型的对象,以提供处理请求的Controller对象实例。



    控制器工厂

        控制器工程必须实现接口IControllerFactory,它定义在命名空间System.Web.Mvc下:
namespace System.Web.Mvc
{
    using System.Web.Routing;

    public interface ICOntrollerFactory
    {
        IController CreateController(RequestContext requestContext, string controllerName);
        void ReleaseController(IController controller);
    }
}



    使用自定义的控制器工厂
     
        在MVC中获得控制器工厂的方式是借助于ControllerBuilder,这个类使用典型的单例模式创建,我们可以通过其Current属性获取这个唯一对象的引用。
        默认情况下,它将会通过DefaultControllerFactory来创建控制器对象,通常情况下,通过ControllerBuilder.Current.GetControllerFactory方法来获取当前的Controller工厂对象实例,也可以通过ControllerBuilder.Current.SetControllerFactory方法来设置自定义的控制器工厂。

    为Controller类传递构造的参数

        默认情况下,Controller类需要提供默认构造函数,因为DefaultControllerFactory将会通过反射来创建Controller对象的实例。如果我们定义的Controller需要构造函数来创建,或者通过某个IoC的容易管理Controller,那么可以通过自定义的ControllerFactory来实现
        在ASP.NET MVC提供的实例程序中,帐号管理通过两个服务 FormsAuthenticationService 和 AccountMembershipService 实现具体的帐号管理。FormsAuthenticationService实现了接口IFormsAuthenticationService,AccoutMembershipService实现了接口IMembershipService。
       为了在帐号控制器中完成管理,重写了控制器的Initialize方法,创建两个管理对象。
public class AccountController : Controller
{

    public IFormsAuthenticationService FormsService { get; set; }
    public IMembershipService MembershipService { get; set; }

    public AccountController(
        IFormsAuthenticationService formsAuthenticationService,
        IMembershipService membershipService)
    {
        this.FormsService = formsAuthenticationService;
        this.MembershipService = membershipService;
    }

    protected override void Initialize(RequestContext requestContext)
    {
        if (FormsService == null) { FormsService = new FormsAuthenticationService(); }
        if (MembershipService == null) { MembershipService = new AccountMembershipService(); }

        base.Initialize(requestContext);
    }
}

        为了便于测试,我们可以将两个对象的引用以构造注入的形式提供给AccountController,这样,前台的开发人员需要和自己使用两个管理对象,就不再需要特意创建这两个对象,实现与后台的解耦。
public class AccountController : Controller
{

    public IFormsAuthenticationService FormsService { get; set; }
    public IMembershipService MembershipService { get; set; }

    public AccountController(
        IFormsAuthenticationService formsAuthenticationService,
        IMembershipService membershipService)
    {
        this.FormsService = formsAuthenticationService;
        this.MembershipService = membershipService;
    }
}

       为了能够创建我们的AccountController对象,创建一个自定义的控制器工厂。
namespace Sample.Controllers
{
    public class MyControllerFactory : IControllerFactory
    {
        private static readonly IFormsAuthenticationService FormsService
            = new FormsAuthenticationService();
        private static readonly IMembershipService MembershipService
            = new AccountMembershipService();
        private static readonly DefaultControllerFactory factory
            = new DefaultControllerFactory();

        #region IControllerFactory Members

        public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
        {
            IController controller = null;
            if (controllerName == "AccountController")
            {
                controller = new AccountController(
                    FormsService,
                    MembershipService
                    );
            }
            else
            {
                controller = factory.CreateController(requestContext, controllerName);
            }
            return controller;
        }

        public void ReleaseController(IController controller)
        {
            factory.ReleaseController(controller);
        }

        #endregion
    }
}

        最后,在Global.asax中注册自定义的控制器工厂
  protected void Application_Start()
        {
            MyControllerFactory factory = new MyControllerFactory();
            ControllerBuilder.Current.SetControllerFactory(factory);

            AreaRegistration.RegisterAllAreas();

            RegisterRoutes(RouteTable.Routes);
        }

        Controller完成服务器端的处理逻辑,所有的Controller都必须实现接口IController,并且实现类的名称也必须以Controller为后缀。



      Controller的继承关系

        IController是一个非常简单的接口,仅仅定义了一个方法Execute,用来完成针对请求的处理。
namespace System.Web.Mvc
{
    using System.Web.Routing;

    public interface IController
    {
        void Execute(RequestContext requestContext);
    }
}

        在ASP.NET中,首先使用ControllerBase实行了IController,并实现了基本的处理逻辑。ControllerBase中对接口Execute的实现如下:
public abstract class ControllerBase : IController
{
    protected virtual void Execute(RequestContext requestContext)
    {
        if (requestContext == null)
        {
            throw new ArgumentNullException("requestContext");
        }

        VerifyExecuteCalledOnce();
        AppDomainInitializer(requestContext);
        ExecuteCore();
    }
    protected abstract void ExecuteCore();
}

        而真正的处理方法ExecuteCore留到了派生类Controller实现出来。
        在派生类Controller中,通过ActionInvoker属性的InvokeAction方法,实现最终对于Action调用。这个ActionInvoker实际上是一个ControllerActionInvoker对象实例。
protected override void ExecuteCore()
        {
            PossiblyLoadTempData();
            try
            {
                string actionName = RouteData.GetRequireString("action");
                if (!ActionInvoker.InvokeAction(ControllerContext, actionName))
                {
                    HandleUnknownAction(actionName);
                }
            }
            finally
            {
                PossiblySaveTempData();
            }
        }

 



    Controller中的状态管理

        在ASP.NET MVC的Controller中提供了一种新的状态管理方式TempData,这个属性定义在Controller的基类ControllerBase中。
public TempDataDictionary TempData { get; set; }//http://msdn.microsoft.com/zh-cn/library/system.web.mvc.controllerbase.tempdata(vs.98).aspx

       这样在处理不同请求的时候,可以在Controller中使用如下的形式来临时保持状态数据:
            this.TempData["Name"]="Hello,world";
       然后,在后继的请求中,在Controller中使用如下的形式来取得临时保存的状态数据:
            string message=this.TempData["Name"] as string;
       Controller类中的PossiblyLoadTempData和PossiblySaveTempData分别用来在不同的请求之间保持状态,这两个方法定义如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值