相关概念、总体处理流程参考 Maverick.Net介绍篇,不再罗嗦。一看就懂的地方略,侧重在貌似疑难之处及部分过程的分析。本人水平一般,如有不正确的地方欢迎指正。
Dispatcher
职责:一,HttpHandler,处理Http请求,包括Http Request-Command的映射、Command对象的管理;二,负责对初始化操作的管理。
ExtractCommandName(HttpContext context)
将类似welcome.m的虚拟请求文件名的.m后缀去掉,取welcome作为Command的名称。这个名称与maverick.config配置中 command节点的name属性相对应。
LoadConfigDocument(HttpContext context)
初始化操作函数之一,读取maverick.config配置文件,返回XmlDocument对象。
其中有一个转换处理,读取maverick.config的xml之后,使用一个xsl文件进行转换,返回转换之后的XmlDocument对象。可以这样来理解,Maverick.Net已经确定了配置文件maverick.config的格式,但你可能不是使用这个格式,例如你对 Maverick.Net做了扩展,可能也会相应的调整maverick.config格式,等等情况之下你可以用一个xsl将 maverick.config配置转换成Maverick.Net要求的格式。
ReloadConfig(HttpContext context)
初始化操作函数之一。
a) 创建Loader对象。Loader创建过程中将创建Command Factory、View Factory、Transform Factory这一系列工厂,并利用这些工厂,根据maverick.config的配置创建所有的Command、View、Transform对象。
b) 对forward类型View的检查。Maverick.Net介绍篇中提到过,forward类型的View是重定向到另外一个Command。这段检查代码就是遍历所有Command下面的View,确保forward类型的View重定向的目标Command是存在的。
c) 创建两个特殊的Command并添加到Command集合中。关于这两个特殊Command的说明参考Maverick.Net介绍篇。
Init()
初始化操作的入口函数。
Maverick.Net将Dispatcher的IsReusable属性返回true,因此IIS将使用Application Pool重用这个HttpHandler(参考HttpHandler相关文档)。但注意:重用并不是说 Init()函数对整个应用而言只会执行一次,IIS在并发处理多个Http Request时,会为每个请求分配一个Http Handler对象(Dispather),每一个Dispather实例将执行一次Init()操作。
ProcessRequest(HttpContext context)
IHttpHandler接口方法,参考MSDN文档。
创建MaverickContext对象时,先尝试从HttpContext.Items中获取,是因为如果将多个Commands配置成一个链(Chain)来处理某个Http Request(就是Action Flow的概念,不过Maverick.Net是在前端控制器-Front Controller上实现这个,直觉上看跟View的关系太紧密,有远离Business Logical/Workflow的感觉,但确实是一个Action Flow),需要使用同一个MaverickContext对象,用于Chain中的各个Command之间协作时传递Model等数据消息。这个 Chain中的第一个Command会把MaverickContext对象放入HttpContext.Items中,随后的Command都是从 HttpContext.Items中获取。Chain中的Command如何传递数据消息?Chain中某个Command处理时,可能会更新、处理 Model信息,还可以向MaverickContext中添加特定的参数,接下来的Command就可以从MaverickContext获取更新处理之后的Model和这些特定的参数。
ICommand GetCommand(string name)
名称为*的Command被用作一个特殊的Command,即当Dispatcher接收到一个无效的Command时,将使用名称为*的Command来处理。我们在使用Maverick.Net框架时可以实现这个Command,用于提示用户无效的操作信息。否则将产生一个404的异常。
其它
a) Command大小写敏感选项:处理Command在大小写敏感方面的问题。首先Command对象用Hash Table缓存,使用Command Name的Key值进行索引时存在大小写敏感问题。Command Name通过HttpRequest.ApplicationPath解析出来,某些系统中可能会自动将ApplicationPath转换成大写。因此提供这个功能用于解决大小写敏感问题。
b) 两个特殊的Command:ReloadCommand、CurrentConfigCommand参考Maverick.Net介绍篇。
c) 关于Dispatcher线程安全方面。详细的HttpHandler线程安全方面话题,请参考其它相关资料。
第一点,在初始化的一系列操作中,我们可以看到很多地方将HttpContext对象作为参数传给工厂类,而在Command、View、 Transform等执行时刻(函数Go())也用到HttpContext对象,是否会存在线程安全问题?其实在各个工厂类中,以及Command、 View、Transform等对象创建时刻,如果使用到HttpContext内容,只是用于获取ApplicationPath等对于整个应用而言全局的数据信息,这在每个Http Request期间都是相同的;而Command、View、Transform等执行时刻使用到的HttpContext对象,都是从 MaverickContext中获取,在Dispatcher处理每个Http Request时都会使用当前请求的HttpContext创建一个新的MaverickContext(将Command配置成Chain方式除外)对象。因此对HttpContext的使用上,Maverick.Net不会有线程安全方面的问题。
第二点,ReloadCommand和CurrentConfigCommand这两个特殊Command是非线程安全的。Init()函数说明中提到过,虽然Dispatcher对象是可复用的,但在Application Pool中可能会有多个Dispatcher的实例。当提交一个ReloadCommand时,会从Application Pool中取一个实例用于服务这个请求,因此这个Dispatcher实例会根据当前的maverick.config配置重新进行初始化,但这个初始化不会影响Application Pool中的其它实例,其它实例使用的仍然是根据旧的maverick.config创建的对象。
其实它是违反了HttpHandler的一个线程安全规则:不要使用成员变量或类似的机制,用于不同的请求、线程间保存传递状态、数据信息。这种情况下可以用一个类似Observer模式解决,或者对Dispatcher使用成员变量保存Command对象集合的方式做修改。
这部分类图如下:
图一:初始化、工厂部分类图
Maverick.Net实现了2种类型的Transform和6种类型的View,每种类型的Transform和View都对应到一个工厂类,负责创建特定类型的实例。实际的Transform、View的创建操作,永远都是由该类型的Transform、View对应的工厂对象完成的。
MasterFactory负责管理全部的Transform、View的工厂(分别用两个HashTable成员保存),并聚合了Transform、 View的创建方法。因此,当需要创建Transform或View对象时,调用MasterFactory.CreateView()或者 MasterFactory.CreateTransform(),将请求提交给MasterFactory对象。MasterFactory负责根据 Transform、View的类型,找到对应的工厂对象,然后调用工厂创建Transform或View。
ViewRegistry主要负责Global View的注册,以供随时取用Global View对象;以及对全局View的引用问题(就是CreateViewsMap方法中的处理)。ViewRegistry保存了一个 MasterFactory的对象,当要创建一个View时,通过调用MasterFactory的方法完成。
关于Shunted View相关概念后面介绍。
Loader负责对全部创建工作的管理。经过上面类职责的了解可以知道,其实Loader只需要管理好CommandFactory、 MasterFactory、ViewRegistry这三个类就达到目的了。Loader在构造函数中完成初始化操作,所以Dispatcher只需要创建Loader对象,从Loader获取创建的Command集合就行。
Loader
初始化的执行过程大致如下图所述:
Loader(XmlDocument doc, HttpContext httpContext)
构造函数,完成全部初始化操作。
创建Command的容器HashTable时,将根据Command大小写敏感配置分别进行处理。
SetupCoreModules()
分别创建各种类型的Transform Factory、View Factory,调用这些工厂的初始化方法,将工厂对象注册到viewFactories、transformFactories这两个 HashTable成员中。
工厂的初始化方法给工厂对象提供一个初始化自己的机会,但是现有的一些Transform、View的工厂中,绝大部分初始化方法都不做什么实质性的事情。
LoadDocument(doc)
在调用这个方法之前,各类工厂对象均已经被创建并完成初始化操作,这个方法就是利用这些工厂对象,根据maverick.config配置文件的 XmlDocument对象(doc),创建各个框架对象。
LoadModules(XmlElement modulesNode)
这个函数根据maverick.config文件的配置,创建自定义的Transform、View工厂对象,以及自定义的ShuntFactory。
这是为扩展Transform、View的类型,以及实现ShuntView提供的一种途径。假如需要扩展一种新的View类型,就需要为这个View类型实现一个工厂类,将这个工厂配置到maverick.config文件中,这样在初始化的时候将创建这个工厂对象并注册到MasterFactory的 Transform工厂集合和View工厂集合中。如果需要创建这类型的Transform或View,将使用到这些工厂对象。
MasterFactory
CreatePlainView(XmlElement viewNode)
读取View的类型属性,根据类型值取对应的工厂,使用工厂创建这种类型的View对象并返回。Plain View这个单词意指简单的View,即直接由对应的工厂创建出来的View对象。
CreateView(XmlElement viewNode)
使用CreatePlainView函数得到IView对象之后,再根据这个View是否有配置Transform、参数节点,相应的将对象创建成 ViewWithTransforms、ViewWithParams类型,以便在View的执行时刻(View对象的Go()方法内)能处理 Transform操作和参数。
CreatePlainTransform、CreateTransform 跟上面两个方法完全类似。
ViewRegistry、ViewRegistrySimple、 ViewRegistryShunted
这三个类的关系从图一:初始化、工厂部分类图中可以看出来。
在maverci.config文件中,Command节点下面的View节点可以这样配置:<view name="loginRequired" ref="loginForm"/>,意思是将从全局View中引用id为loginForm的View对象。
这个引用关系并不是在执行时刻处理的,而是在Maverick.Net初始化过程中完成。执行时刻处理可以这样描述:如果Command根据 Controller执行结果,发现需要执行名称为loginRequired的View,而这个View是对Global View中id为loginForm对象的引用,所以从Global View中检索出这个loginForm的View对象,然后执行它。这样的处理方式,给框架带来复杂和不规范性,而把这个操作放在初始化中,则所有 View的执行处理就统一起来了。Command对象聚合了一个或多个View对象,在为Command创建聚合的View或View对象集合时,如果某个View是对Global View对象的引用,则从Global View中检索到这个View对象,直接返回给Command,避免Command在执行时刻从Global View中检索。这就是CreateViewsMap函数的作用。
Global View的创建是在创建Command之前完成的,确保了在Command创建时刻能够引用到Global View对象。Global View的引用关系在初始化时刻已经被处理掉,因此ViewRegistry、ViewRegistrySimple、 ViewRegistryShunted这三个类也只在初始化的过程中用到,在Maverick.Net正常的处理Http Request期间已经不再需要使用。
ViewRegistryShunted类用于在完成上述功能时,对Shunted View特殊处理的支持。
Command承担的工作很简单:执行Controller的处理,根据返回的结果,选择对应的View,运行这个 View。
Command分为两类,一类是没有Controller的,对应CommandSingleView。这类Command不需要根据 Controller的执行结果选择View,因此,这类Command有且只能有一个View,Command的执行其实就是直接对这个View进行呈现。Maverick.Net对这类Command使用一个NullController,这个Controller不做任何事情,只是为了保持 Command行为的一致性。
另一类是有Controller的,对应CommandMultipleViews。在初始化创建Command实例时,已经为Command创建了Controller对象,因此Command只需要调用这个Controller对象的Go()方法执行处理。
CommandBase提供了Command规范性的处理。CommandSingleView和CommandMultipleViews继承 CommandBase,主要实现GetView方法,即怎样从Command对象持有的View对象中选择正确地View来执行。
几个简单的View。
NullView:不做任何事情。可以用于Command节点下面没有配置任何View的情况,但Maverick.Net目前没有这样使用,如果Command下面没有View节点将产生异常。
TrivalView:将Model以字符串的方式直接输出。如果某个Http请求的结果,只是一段文本消息,或者是输出一个文本文件、xml文件内容等,可以使用这种类型的View。
ForwardView、RedirectView:功能上基本是一样的。
ForwardView重定向到另外一个Command。它是直接在View的执行时刻,使用Dispatcher获取Command对象,调用 Command.Go()方法。假如某个View需要重定向到名称为login的Command,View节点配置为:<view name="viewName" command="login"/>。
RedirectView使用Response.Redirect()方法进行重定向,所以重定向的目标可以是某个asp/aspx页面也可以是某个 Command。假如需要重定向到名称为login的Command,RedirectView的实现方式为 Response.Redirect("login.m"),这种情况下可以将ForwardView看作是RedirectView的一种特例。 RedirectView更倾向于重定向到某一个asp/aspx页面,因为这类型的View在重定向之前,会将需要传递的参数,以GET的方式拼写到 url中,参数将包括IDictionary类型的Model对象和MaverickContext.Params。如果Model对象是String类型,则RedirectView将把Model当作目标url,而忽略view节点中配置的url。这类型的view配置示例如:<view name="viewName1" path="login.m"/>、<view name="viewName2" path="default.aspx"/>。
如果整个项目以Maverick.Net架构,是不需要使用RedirectView重定向到另外的asp/aspx页面的,提供这种类型,有利于与其它项目进行基于页面层次的整合,或者类似的用途。
DocumentView:这个View类型复杂些。
在类图结构中,最基础的是DispatchedView,这个类型的View完成asp/aspx等类型的页面文件转换成HTML的操作,它是 internal的类型,只在框架内部使用,实现的也是document类型View最基础的功能。
DocumentView聚合一个DispatchedView对象,因此拥有DispatchedView的功能,派生DocumentView主要是解决Model存放位置问题。它为abstract类型,通过abstract类型的方法SetAttribute提供给子类解决如何传递 Model。
ApplicationDocumentView、SessionDocumentView、RequestDocumentView都继承自 DocumentView,实现自己的SetAttribute方法,分别使用Application、Session、 HttpContext.Items作为存放Model的地方。Type为document类型的View,在配置节点中通过scope属性确定该 View属于这三种类型中的哪一种,如果没有声明scope属性,默认为RequestDocumentView类型。
以一个RequestDocumentView类型的对象来看,在调用这个view的Go()方法的时候,先在DocumentView.Go()方法处判断,如果Model对象不为null,则执行RequestDocumentView.SetAttribute()方法,将Model放入 HttpContext.Items中。这样,这个view对应的aspx页面的服务器端代码,就应当从HttpContext.Items中获取 Model对象进行显示。接下来将调用聚合的DispatchedView对象的Go()方法,将这个aspx页面解析成 HTML。
XmlSerializingView:单纯的从View的功能上看,它也非常简单,就是将 Model反序列化之后的xml直接输出。这类型的View大都用于基于xml的网站方案中,将Model序列化成xml之后,可以使用 XsltTransform将其转换成HTML。
以上介绍的几个类型的View的实例都由相应的View Factory创建,可以说是一种简单类型的View对象。它们实现了几种基本形态View的功能,Document类型的View复杂一点,在View 的显示过程中将使用到Model对象,所以Document类型的View在类的结构上看起来也复杂一些。
剩下的两种类型的View就当作扩展类型来看待吧。
ViewWithParams:有时候我们可能希望为某些View配置一些特殊的参数,例如有两个功能,Web页面98%都是相同的,仅有微小的一点区别,我们可能会希望只写一个页面,通过给它们不同的参数,使这一个页面运用在两个功能中。 ViewWithParams聚合了一个IView对象,它在本身的Go()函数中附加一些对参数的处理,即将参数保存到MaverickContext 这个上下文对象中,然后再调用聚合IView对象的Go()方法。这样,被ViewWithParams封装过的IView对象或者是对应的aspx页面中,就能通过MaverickContext访问到这些参数。
参数的配置示例如下:
<
view
name
="UserView"
path
="UserView.aspx"
>
<
param
name
="param1"
value
="???"
/>
<
param
name
="param2"
value
="???"
/>
</
view
>
因为这种类型的View只是对基础类型View的一个封装扩展,并且也许会有多个类型的View希望能够使用参数,复用这样一个功能特性,所以 Maverick.Net并没有把这种封装过程放入到具体的某一个ViewFactory中,而是放在了MasterFactory中处理。在 MasterFactory.CreateView(XmlElement viewNode)方法中可以看到这一处理。
ViewWithTransforms:这类型的View聚合一个IView和多个 ITransform对象,用于在View执行时对Transform处理。跟ViewWithParams类似,用 ViewWithTransforms封装IView对象的操作也是在MastrerFactory中处理。在Maverick.Net中,虽然从表面上看起来View和Transform的衔接很紧密,但Maverick.Net对View和Transform采用一种较松散的耦合方式来处理。
在现有的View类型中,只有基于DocumentView类型的以及XmlSerializingView类型才能使用Transform操作。
ViewWithTransforms.Go()方法先将Transforms对象放入MaverickContext中,然后调用聚合的IView对象的Go()方法。聚合的IView对象为DocumentView或XmlSerializingView类型,对于DocumentView类型的将调用DispatchedView.Go()方法,在DispatchedView.Go()和XmlSerializingView.Go()方法中,都会通过MaverickContext.NextStep对象间接使用到Transform对象,逐步完成输出内容的转换。
以一个DocumentView对象、具有一个Transform配置节点作为示例,看一下Transform的处理过程。
每一个Transform对象都由位于它前面的一个驱动对象创建一个ITransformStep,用于执行转换操作。第一个Transform的 ITransformStep由DispatcheView创建。
DispatcheView执行时,先使用第一个Transform对象创建ITransformStep,使用Server.Execute()将 DispatcheView的aspx页面转换成HTML,放入ITransformStep创建的MemoryStream中,然后调用 ITransformStep的Go()方法。随后的每一个ITransformStep在Go()方法中,首先使用下一个Transform对象创建下一个ITransformStep,然后从MemoryStream读取上一个驱动对象放入的HTML,用它对应的Transform配置节点中的key 属性值作为Key值,将HTML放入HttpContext.Items中,最后使用Server.Execute()方法,执行它对应的 Transform对象的aspx页面,将输出的HTML放入下一个ITransformStep的 MemoryStream。
当执行达到最后一个Transform位置时,它后面再没有其它Transform,因此创建LastStep类型的ITransformStep对象,这个对象的Go()方法是一个空操作,但是它将HttpContext.Response.OutputStream作为MemoryStream的替代,因此,最后一个Transform的aspx页面在使用Server.Execute()方法执行后,就直接输出到 Response.OutputStream中了。
理解上面的处理过程之后,会有一个疑问,就是只看到了最后一个Transform对应的aspx执行后的HTML代码发送给客户端,它之前的 Transform对应的aspx页面、View对应的aspx页面执行后的HTML代码呢?从这个过程中我们只能看到这些HTML代码被依次放入 HttpContext.Items中,并没有看到向客户端输出。实际上,从Maverick.Net的示例项目Friendbook中,取一个View 最后一个Transform相关联的aspx页面看一下就知道了,在这个aspx页面里,会看到类似<%=Context.Items["wrapped"]%>的服务器端语句,就是这个语句从 Context.Items中取出一段HTML内容并输出。这样就可以明白了,最后一个Transform对应的aspx页面执行后的HTML代码,自然就包括了它前面的Transform、View执行后的HTML。
View对象本身的关系可能会有点复杂,例如可能创建的一个ViewWithTransforms对象,将会聚合一个 RequestDocumentView,而这个RequestDocumentView又聚合一个DispathedView。另外,在View的执行时刻,跟Transform的转换结合在一起。所以,一个IView对象的Go()方法,感觉上转来转去,使对这个处理过程的理解产生疑惑。通过上面的描述,好好的理解这些对象的职责,把它们的关系梳理清楚之后,你会发现其实还是很简单的。
ShuntedView
整体上来看,ShuntedView也是比较简单的。普通类型的View,通过一个name属性作为Key值,ShuntedView则必须使用name 属性加上一个mode属性一起才能作为Key值。这是用于实现多语言之用,比如同一个View UserQuery,可能需要有中文、英文等语言支持,将语言代码作为Mode,使用UserQuery + ch获取中文版本,使用UserQuery + en获取英文版本。相应的,View的配置节点类似如下:
下面这副图帮助理解一下Shunt处理过程。
目前 Maverick.Net并没有使用ShuntedView,在Loader的构造函数中,创建的是ViewRegistrySimple而不是 ViewRegistryShunted对象。如果使用多语言,应当在这个地方使用ViewRegistryShunted,上面的图就是基于这种情况下的处理过程描述。
具体的实现细节根据这个序列从代码上可以看出来,下面讲的是大致的思路。ViewShunted仅仅是聚合一个IShunt对象,并实现IView接口,真正的操作是由IShunt 对象完成的。在多语言的运用情况下,IShunt对象为LanguageShunt类型,这个类型的对象维护了一个HashTable成员modes,它以View配置节点的mode值作为Key,将name相同而mode值不同的多个IView对象保存在这个HashTable中。这样,name相同而语言版本不同的多个IView对象,通过ViewShunted的封装,从外部看起来它就成为一个IView对象了。在上面的xml示例配置中,名称为 UserQuery的ICommand对象,将拥有两个(注意不是三个)IView对象,一个为loginRequired,另外一个为 ShuntedView类型的success。这样对于ICommand对象执行时,对ShuntedView的处理方式上也是规范的,ICommand 对象无需了解后面的细节。ViewShunted类型的View在执行时,尝试从Request.Headers对象中取语言代码,然后根据这个语言代码获得对应的IView对象,继而调用这个IView对象的执行方法。
其实经过上面对ViewWithTransforms执行过程的理解,对于Transform对象也就没有什么悬念了。
尽管DocumentTransform在概念上理解起来比较牵强,因为view的aspx执行之后就是HTML了,感觉上不再需要什么转换处理,Maverick.Net的示例项目Friendbook中将Transform作为几乎所有工作页面的一个整体包装页面的用途。也许你还能发现其它有意思的用处。
对于基于xml的网站,XsltTransform的作用理解起来就非常直接了。对XsltTransform,有几种方式的用法:
2. 将View配置程xml(XmlSerializingView)类型,把Model反序列化成xml,使用xsl解析。
3. 将View配置程trivial类型,由Controller直接构造xml,然后用xsl解析。
4. 将View配置成xml类型,View并不输出任何xml,直接使用Transform处理。这种情况通常第一个Transform输出xml,后面的 Transform解析成HTML。这种跟1基本完全一样。
对xml类型的View使用XsltTransform转换的处理过程,跟DocumentTransform完全一样。
在这个处理的Chain上,IView对象是起点。前面提到过,View跟Transform之间是一种较松散的耦合方式,这样,IView对象无需去管理自己有多少个Transform,应该怎样一步一步的执行转换,以及将最终的结果发送给客户端。IView对象只需要完成自己的职责,它知道在它的后面一定有其它的对象来处理上面这些事情,所以IView处理完之后,将它输出的视图代码抛给下一个对象即可。
LastStep对象是Chain的终结者,负责将最后的输出发送给客户端浏览器。
大概考虑到无论是Server.Execute()或者System.Xml.Xsl.XsltTransform.Transform()方法都可以向 Stream直接输出,Maverick.Net在Chain处理上,采用一个Stream类型的成员变量向后传递输出内容。既然这样,在Chain上每一个执行步骤中动态创建一个ITransformStep就成为一种必要。
Maverick.Net提供一些Controller的基础类,用于实现不同用途下的Controller。
......