本文我们详细介绍asp.net MVC开发模式中的C,Controller控制器。
1.Controller类
Controller的执行体现在对其Excute方法的调用,在IController这个接口里只定义了一个Excute方法,这个方法是以同步的方式执行的。
public interface IController{ void Excute(RequestContext RequestContext);}
为了异步方式,在System.Web.Mvc.Async命名空间下定义了一个IAsyncController的接口,如下面代码所示
public interface IAsyncController : IController { IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state); void EndExecute(IAsyncResult asyncResult); }
IAsyncController接口派生于IController接口,Controller异步执行通过先后调用BeginExcute和EndExecute方法。还有一个非常重要的基类ControllerBase,默认所有的Controller类都会继承它,它是一个抽象类,ControllerBase也实现了IController接口。这个类显示的实现了接口的Excute方法,这个实现的Excute方法会调用受保护的虚方法Excute,后者将继续调用抽象方法ExcuteCore方法,作为ControllerBase的继承者,必须通过ExcuteCore方法来完成Controller的执行。以下是ControllerBase的代码片段
public abstract class ControllerBase : IController { public ViewDataDictionary ViewData { get; set; } public dynamic ViewBag { get; } public TempDataDictionary TempData { get; set; } protected virtual void Execute(RequestContext requestContext); protected abstract void ExecuteCore(); protected virtual void Initialize(RequestContext requestContext); void IController.Execute(RequestContext requestContext); }
目标Controller在执行后会返回一个View作为响应,此时Controller可以通过TempData、ViewBag、ViewData向View传值。从定义可以看到TempData和ViewData均返回字典结构的数据容器,两者不同的地方是TempData如其名字,存储的数据是暂时的,用过一次后就不能再用了,ViewData则可以多次使用。ViewBag则是可以存储动态对象,也就是可以指定任何属性,属性名则为数据字典的Key。还有ControllerContext这个类,这个类的对象可以看做是RequestContext和Controller对象的封装。从上面ControllerBase类的定义可以看到还有一个Initialize方法,该方法具有一个RequestContext的参数,这个方法被执行的时候就会创建ControllerContext对象。受保护的虚方法Excute在执行ExcuteCore方法前就先执行Initialize方法。我们通过VS创建的控制器实际上继承自抽象的Controller类,在VS里可以看到Controller除了继承ControllerBase类,实现IController接口和IAsyncController接口外,还实现了5种过滤器的接口。除此之外,它还实现了一些其他接口,比如IDisposable接口,MVC的Controller激活系统在Controller执行结束后会调用其Dispose方法完成相应的资源回收工作。
2.同异步、ControllerFactory、ControllerBuilder
从抽象类Controller的定义可以看出它实现了IAsyncControll接口,而这个接口又继承自接口IController接口,因此Controller既可以实现同步的方式(Excute),也可以实现异步的方式(BeginExcute/EndExcute方法)。Controller类有一个DisableAsyncSupport属性,默认是false,如果设置它为true则它只以同步的方式执行,为false时是异步方式执行。
ASP.NET MVC为Controller的激活定义了相应的工厂,即ControllerFactory类,所有的ControllerFactory类都实现了IControllerFactory接口,如下面代码片段
public interface IControllerFactory { IController CreateController (RequestContext requestContext,string controllerName); SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName); void ReleaseController(IController controller); }
ControllerFactory将负责创建和释放Controller对象的工作。创建体现在CreateController这个方法中,释放则是ReleaseController方法中。还有一个GetControllerSessionBehavior方法返回类型为SessionStateController的枚举值。其4个属性为:Default,表示使用默认ASP.NET逻辑来确定请求的会话状态行为;Requeired为请求启用完全的读/写会话状态行为;ReadOnly:为请求启用只读会话状态行为;Disabled禁用会话状态。具体采用何种行为取决于http上下文,HttpContext的静态属性Current。
用于激活Controller对象的ControllerFactory最终是通过ControllerBuilder注册到ASP.NET MVC框架里的。
public class ControllerBuilder { public ControllerBuilder(); public static ControllerBuilder Current { get; } public HashSetDefaultNamespaces { get; } public IControllerFactory GetControllerFactory(); public void SetControllerFactory(IControllerFactory controllerFactory); public void SetControllerFactory(Type controllerFactoryType); }
ControllerBuilder的Current返回当前使用的对象,两个重载的SetControllerFactory方法实现ControllerFactory的注册,不同的是第一个注册的是一个具体的ControllerFactory对象,第二个注册的是一个ControllerFactory的类型。如果我们注册采用第二种方式,那么GetControllerFactory在执行时通过调用Activator的静态方法CreateInstance的方式来创建对象,也就是说这种情况下每次调用GetControllerFactory方法总是伴随着对象的实例化,然而MVC机制并不会对实例的对象进行缓存。因此采用第一种方式比较好,这样会得到具体的对象直接从GetControllerFactory返回。
我们知道路由系统对请求实施解析后会生成RouteData对象,它的Values属性保存着目标Controller的名称,如果不指定命名空间时又有同名的控制器名,则会报异常。Controller激活系统为我们提供了2种提升命名空间优先级的方式:第一种则是在注册路由时指定,此时命名空间列表保存在Route对象的DataTokens属性表示的RouteValueDictionary对象中(这个地方还可以指定后备命名空间);另一种方式则是在ControllerBuilder指定DefaultNamespaces属性,ControllerBuilder.Current.DefaultNamespaces.Add("命名空间")。不过前者具有较高的优先级。
3.Controller的激活与路由
路由系统可以看成是请求被IIS分发给ASP.NET管道后的第一道屏障,接下来学习Controller的激活系统与路由系统是如何有机的结合起来。由前面的知识我们知道,整个ASP.NET的路由系统建立在一个叫做UrlRoutingModule的HttpModule之上,它会通过注册HttpApplication为当前当前的请求动态的地映射一个HttpHandler。也就是通过全局路由表对请求实施路由解析并生成一个RouteData对象,然后借助这个对象的RouteHandler得到最终被映射到当前请求的HttpHandler。RouteHandler对象在MVC机制里是一个MvcRouteHandler对象。那么,我们可以看到最简单的本质,那就是RouteData的MvcRouteHandler对象创造出了HttpHandler对象。我们来看一下MvcRouteHandler,
public class MvcRouteHandler : IRouteHandler { public MvcRouteHandler(); public MvcRouteHandler(IControllerFactory controllerFactory); protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext); protected virtual SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext); }
MvcRouteHandler里维护着ControllerFactory对象,该对象在构造函数指定,如果没有指定则会调用当前ControllerBuilder对象的GetControllerFactory方法得到。除了得到HttpHandler对象外,MvcRouteHandler还会设置当前http的会话状态,该方法会先从RequestContext的RouteData对象拿到目标Controller的名称,这个名称和RequestContext对象会作为参数调用ControllerFactory的GetControllerSessionBehavior方法得到类型为SessionStateBehavior的枚举,最后再对会话状态进行设置。RouteData的RouteHandler属性最初来源于对应的路由对象,对于调用RouteCollection的扩展方法MapRoute注册的Route对象来说,它对应的RouteHandler就是一个MvcRouteHandler对象,由于在创建这个对象时并没有显示指定ControllerFactory,因此通过调用当前ControllerBuilder对象的GetControllerFactory方法的得到的ControllerFactory会默认被使用。
GetHttpHandler方法里最后会retutn new MvcHandler(requestContext);,通过ControllerBuilder的GetControllerFactory方法的ControllerFactory仅仅被MvcRouteHandler用来获取会话状态模式而已,而只有MvcHandler才是真真的创建目标Controller对象。由于MvcHandler同时实现了IHttpHandler和IHttpAsyncHandler接口,所有它总是以异步方式执行的,最终激活Controller的工作是由当前ControllerBuilder提供的ControllerFactory对象激活目标Controller对象。最后MvcHandler会实施释放清理工作。
4.Controller默认激活
Controller对象的激活最终是通过注册的ControllerFactory完成的,当我们没有显示调用ControllerBuilder的SetControllerFactory方法指定ControllerFactory对象时,Controller激活系统会使用一个DefaultControllerFactory对象来激活目标Controller,这就是ASP.NET MVC采用的默认Controller激活机制。
目标Controller对象被激活的前提是得到目标Controller的真实类型,在DefaultControllerFactory里包括了解析Controller类型的一些路由信息,比如Controller的名称,命名空间,当前ControllerBuilder的默认命名空间等。大家可能感觉有了这3个条件,应该就可以通过名称拿到Controller实例,然而这不可以。首先,在RouteData里的Controller名称的变量值是不区分大小写的,而类型名称则是对大小写敏感的;对于命名空间,通过前面的学习了解到一般都是在名称后加一个.*;因此我们无法通过名称去解析目标Controller的类型了。正确的做法是DefaultControllerFactory先调用BuildManager的静态方法GetReferencedAssemblies得到所有可用的程序集,再从中挑选所有实习了接口IController的类型,最后通过Controller名称和命名空间去与目标名称匹配,简单点说,后者的方式保证了目标Controller和命名空间没有改动过,是最真实的数据。
出于对性能的考虑,对于解析出来的所有有效的Controller类型作了全局缓存。针对Controller类型的缓存同样实现在DefaultControllerFactory里,而且这个列表是持久化的,即应用重启后仍然存在。用于激活目标Controller对象的ControllerFactory不仅仅用于创建目标Controller对象,还有2个功能:一个是对激活的Controller对象进行释放和回收,另一个则是通过GetControllerSessionBehavior方法返回控制当前会话状态行为的SessionStateBehavior枚举对象。
5.Ioc应用
控制反转,简称Ioc。以前学MVC时听老师讲过这个词,不过没怎么深刻体会这个词,今天我要征服它。Ioc的意思简单点是应用本身不负责依赖对象的创建和维护,而将这个任务交给外部容器,这样应用权就由应用转移到了这个外部Ioc容器,这样控制权就实现了反转,也就是说,控制反转是控制权被反过来给别人了。比如A类需要使用B类的实例,而B实例的创建并不由A来负责,而是通过外部容器来创建。这种思想在实现针对目标Controller的激活具有重要的意义。
Ioc和DI(依赖注入)往往联系在一起。DI就是由外部容器在运行时动态地将依赖的对象注入到组件之中。书作者蒋先生比较喜欢将依赖注入分为一种映射和3种注入,我感觉理解起来很不错。
(1)映射是类型映射,虽然我们可以通过接口或者抽象类来调用某个对象的方法,但是对象本身是一个具体的类型,所以需要某种类型注册机制来解决接口/抽象类和实现类/具体子类之间的匹配关系。
(2)构造器注入,Ioc容器会智能地选择和调用合适的构造函数来创建依赖的对象。如果被选择的构造函数具有相应的参数,Ioc容器会在调用构造函数之前解析注册的依赖关系并自行创建相应的参数对象。
(3)属性注入,如果需要使用到被依赖对象的某个属性,在被依赖对象被创建之后Ioc会自动初始化该属性。
(4)方法注入,如果被依赖对象需要调用某个方法进行相应的初始化,在该对象被创建之后Ioc容器会自动调用该方法。下面来看一个例子,
public interface IA { } public interface IB { } public interface IC { } public interface ID { } public class A : IA { public IB B { get; set; } public ID D { get; set; } //构造器注入 public A(IB b) { this.B = b; } //属性注入 public IC c; public IC C { get { return c; } set { c = value; } } //方法注入 public void Initialize(ID d) { } }
我们知道用户请求过来时,如果涉及到数据业务,Controller会直接调用Model,如果需要呈现业务数据,则会将相应的数据转为ViewModel,此时Controller对Model是直接依赖的。现在可以将Ioc的思想应用到Controller激活系统中,如果采用Ioc的方式提供用于被处理请求的Controller对象,那么Controller和Model之间的依赖程度会被降低。我们还可以定义IModel接口,以这种方式对Model进行抽象,让Controller依赖于这个抽象化Model接口,而不是具体的Model实现。