Maverick.Net是Java社区开源MVC Web框架
Maverick的.Net版本,相关资料可以查看项目主页。
不管Maverick.Net的是非好坏,了解一下它的思想还是不错的。下面的内容是对Maverick.Net整体做一个简单的介绍,以求能够从全局的角度了解Maverick.Net一些相关概念,大致如何工作。
另外,本人没有使用Maverick.Net开发过项目,只是看了它的架构、源码之后有一些想法,跟大家分享一下,也希望能为想了解Maverick.Net的人提供些参考。
1. 主框架
Maverick.Net处理HttpRequest典型的流程如下图:
Maverick.Net将每一个HttpRequest映射成Command。很容易看到,Command的提交者为用户界面,也就是View;处理者应当是业务逻辑的控制器,也就是Controller。这样,Maverick.Net Framework在处理每一个Command时,主要是解决下面的问题:如何从用户界面(View)提交的参数生成Model?用哪个Controller处理特定的Command以及如何将Model传递给这个Controller?Controller处理结束之后如何将处理结果呈现给用户(对Maverick.Net而言就是根据Controller处理结果,选择返回哪个View给用户界面)以及如何将Model传递给返回的结果View?
Maverick.Net通过一个配置文件进行配置,典型的Command配置节点如下:
这是Maverick.Net项目例子Friendbook中一个提交用户注册的Command配置节点。Dispatcher接收到提交注册的HttpRequest之后,得到signupSubmit的Command Name,从而可以得到这个Command对象。Command将创建指定的Controller对象Friendbook.Ctl.SignupSubmit.Friendbook,并调用Controller进行处理。Controller处理结果应当是注册成功,需要转向注册成功之后的View;或者是发生错误,需要转向重新填写注册资料的View。Maverick.Net Framework规定Controller的返回结果必须是一个View的名字,这样,Command调用Controller处理之后,就知道应该呈现哪个View对象。上图中,如果注册成功(success),将调用name="success"的View;如果注册出错,例如两次输入的密码不一致等,将调用name="error"的View。name="success"的View类型为redirect,将使用当前的MaverickContext对象(聚合了HttpContext、Model等对象)重定向到另外一个名称为edit.m的Command对象(View的类型等后面再介绍);name="error"的View类型为document(默认类型,不需要写type="document"),将直接使用Server.Execute()方法得到View的HTML输出,并使用Transform对象,将用户注册结果View的HTML输出转换成整体页面的HTML代码,返回给客户端(Transform的概念,后面再介绍)。
2. 主要的类、对象
Maverick.Net主要的类或对象如下:Dispatcher、Command、Controller、View、Transform、MaverickContext。
2.1 Dispatcher
Dispatcher是一个HttpHandler类,用于拦截和处理HttpRequest,HttpRequest-Command的映射就是通过Dispatcher完成。
典型的情况下,Maverick.Net使用.m后缀作为ISAPI扩展,映射到Command。这种情况下,所有浏览器端提交的HttpRequest,请求的都是以.m结束的虚拟文件名,被提交给Dispatcher。这个文件在服务器上并不存在,Dispatcher接收到类似login.m的请求之后,将.m后缀去掉,取login作为Command Name,获取Command对象,处理请求。
Maverick.Net老的版本基本上就是照搬Java版本的Maverick,在View的层面上只支持象JSP中Servlet的写法,也就是纯粹的ASP方式,或者是以ASP方式编写的的aspx页面。ASP.Net环境下ViewState、Postback事件机制、用户控件等这些特性都不能使用,这对于ASP.Net环境是一种很大的损失。1.2版本开始,Maverick.Net的Controller比较完全的支持aspx的服务器端控件、用户控件等,这样就可以使用Maverick.Net框架,又基于ASP.Net进行设计和开发。
在使用aspx类型的Controller时,Dispatcher的拦截作用被去掉了,而是使用ASP.Net默认的HttpHandler,页面类继承Maverick.Ctl.Aspx.ControllablePage,ControllablePage在页面的Render方法中切入Maverick.Net框架的控制。这种情况下,Command Name也不再是.m后缀方式,而直接变成对应的aspx页面名称,例如<command name="Default.aspx">。
另外,Dispatcher处理请求时,负责构造MaverickContext对象,使Command、Controller、View、Transform等在各自的处理期间,都能够得到相应的上下文信息。
2.2 Command
Command的作用类似一个指挥官,它根据Maverick.config文件的配置创建Controller对象,调用Controller进行处理;根据Controller的返回结果,选择相应的View对象并执行。
Command可以没有Controller,这种情况下,对Command的请求,实际上就是将特定的View呈现给客户端。在Maverick.Net中,这是CommandSingleView。
另外,也可以继承ICommand接口实现一些特殊功能或者说自定义的Command,例如Maverick.Net框架本身实现了两个特殊的Command:ReloadCommand和CurrentConfigCommand。CurrentConfigCommand用于查看当前配置文件的内容,这个Command读取Maverick.config配置文件的内容,直接输出配置文件中的xml。另外,Maverick.Net在第一次处理请求的时候,会读取Maverick.config中的内容,根据配置创建Command、View、Transform对象并缓存起来,后续的请求只是从缓存中取相应的对象。如果在第一次访问之后这些配置有更新,这些更新不会立即反映到缓存的对象中。ReloadCommand的作用就是根据当前的Maverick.config配置文件内容,重新创建缓存的对象。
2.3 Controller
Controller的职责是负责业务逻辑的处理。
在典型的应用中,Controller首先通过MaverickContext中的HttpContext对象,获取从用户界面也就是View提交过来的参数,创建Model对象,使用Model完成对请求的业务逻辑处理。Controller不同的处理结果,将用来确定选择哪个View。在Controller的处理过程中可能会对Model进行更新,或者是有一些参数需要传递给View,这些将通过MaverickContext这个上下文信息对象完成。
在aspx类型的Controller中,因为Dispatcher的拦截作用被丢弃,因此MaverickContext对象的创建,也是在Controller中完成的。
2.4 View
MVC的概念中,View负责将某个特定的Model,或者是整个Business Domain Model中某些角度的切片呈现给客户端。Maverick.Net中,将View分成多种类型。
DocumentView:这是默认的View类型。这类型的View将配置一个文件,可能是asp或者aspx或者html等,调用Server.Execute()方法得到HTML的输出,发送给客户端。
ForwardView:这类型的View使用当前的MaverickContext重定向到另外一个Command去执行。如果基于structs的方式做系统分析设计,一个业务操作可能会被分解成很多个Command/Action,将这些Command/Action组合起来就形成一个新的Command/Action,或者就对应到某一个业务操作。forward类型的View是用于对这种形式的支持。
RedirectView:这类型的View,将直接使用Response.Redirect()方法重定向过去。因此也可以将forward类型的View看作是redirect类型的一个特例。实际上在Maverick.Net项目的示例中就有不少这样的用法。这是因为Dispatcher拦截了HttpRequest,类似Response.Redirect("edit.m")的语句执行后,仍然会被Dispatcher拦截,开始一个新的Command的执行。
TrivialView:这类型的View,Maverick.Net将MaverickContext的Model对象直接进行输出(取出内容,直接输出)。这要求MaverickContext的Model对象是下面几种类型:String、System.Text.StringBuilder、System.IO.TextReader、Sytem.Xml.XmlNode。
XmlSerializingView:这类型的View,将MaverickContext的Model序列化成xml格式后输出。这种View主要用于基于xml的网站方案,或者是提供系统接口之用途。
2.5 Transform
在Maverick.Net看来,View执行的结果,还不是呈现给客户端的最终输出,之间必须经由Transform进行相应的转换。
Maverick.Net实现的Transform有:DocumentTransform、XsltTransform。从XsltTransform类型更容易理解Maverick.Net的Transform概念。对应于XsltTransform,View是一个xml或者是XmlSerializing类型的View,为了将这个xml转换成HTML输出,需要使用xsl/xslt文件进行转换,这就是XsltTransform的工作。
对于DocumentTransform的理解,Transform这个单词意义上有点牵强。首先,DocumentTransform是针对于asp、aspx、html这种文档类型的View。在我们的映象里,这种类型的View,Maverick.Net使用的是Server.Execute()方法,这样已经直接得到HTML代码,何必再经过一个DocumentTransform处理?
实际上,对于一个系统,工作页面会被分成不同的区域,例如页面的Banner区、菜单/功能列表区、通用工具栏等等。这些不同的区域,在后面是由不同的Controller负责处理,对应的也是不同的Model。在Command的处理中,特定的Command只会使用自己的Controller对象,负责对相应的Model进行处理,因此也只应当对页面中它所辖区域的显示负责。这样,View执行的结果就只是Command所辖区域的更新,而并不是整个页面。至于如何将这个区域的更新显示在整个页面中,这就是DocumentTransform的工作。
目前Maverick.Net对于DocumentTransform用如下方式进行处理。假如是一个document类型的View,对应一个aspx文件。View在执行时使用Server.Execute()方法得到输出的HTML代码。但这个还不是最终发向客户端的完整输出,而只是其中的一部分,Maverick.Net将这个HTML片断保存在HttpContext.Items[""]中。DocumentTransform对象对应的也是一个aspx的页面,这个页面应当完成系统页面的整体布局,并从HttpContext.Items[""]中取出View执行后的HTML,输出在适当的位置。这样,使用Server.Execute()方法调用DocumentTransform的aspx页面后,就是完整的发向客户端的HTML了。
其实Transform是考虑了页面的整体布局,一个View只会对某个区域负责的状况,但是它并不支持复杂的布局,只能说是轻量级吧。复杂的布局通过iFrame等实现,或者做些修改后结合Ajax方式进行局部更新。
2.6 MaverickContext
这是一个上下文对象,主要负责在Command、Controller、View之间传递信息。它包含HttpContext、Model和一个IDictionary类型的Params属性。
HttpContext对象在创建MaverickContext时由创建者(Dispatcher或者ControllablePage)提供;Model由Controller执行处理时创建;Params用于在需要的传递其它的参数。
从MVC的角度来看处理流程,如下图:
3. 简评
在其它一些介绍Maverick.Net的文章里,有看到不少对Maverick.Net,对ASP.Net下如何运用MVC概念的讨论,看法各不一样。其实对.Net下的开发、架构设计的运用,本来都是仁者见仁,智者见智的事情,只是看大家怎么运用了。下面的这些感想,只是针对企业级应用的架构模式方面,对基于快速开发的小型项目当然另当别论。
3.1 M-V-C分离
ASP.Net本身的确就是一种MVC的框架,可以说是一个很强大又很好用的MVC框架,但是有一些障碍在阻挠着我们。ASP.Net本身的许多特性,使得M-V-C之间的概念变得模糊甚至混乱。
看一下这个场景:添加一个Web Form;用Drag-Drop方式在Form中添加GridView、Connection等数据库组建;设置相应的属性,例如数据库连接,GridView的数据源绑定等。可能就简简单单1、2行代码甚至1行都不需要,运行这个Web Form就将数据显示出来了。一切是这么方便和简单,给BS开发带来一种革命性的思想。可是基于企业级应用架构层面来看,从这样的场景中你能得到什么,想到什么?
当然,对富于经验的人,这根本就不是问题所在。随便写一段代码,就能很好的体现MVC的思想,无所谓使用的哪种语言,什么工具。假如现在需要几百个开发者协同完成一个项目,情况会怎样?ASP.Net本身在架构方面没有太多约束力,尽管它是一个很好的MVC框架。而这种情况下,又非常的需要在架构上对协作的开发者进行约束、规范,否则怎样确保按照架构、设计方案进行开发,就是件灾难性的事情。
Maverick.Net在MVC的呈现上是非常清晰的。也许你会发现,在你的架构中正是需要引入这样的一种思想。不管是哪个子团队,哪一个人,使用这一个框架,就需要理解它的思想,按照它的规范进行开发。
3.2 分析设计思想的转变 MDA
从大的视角来看,用户需求与软件系统之间的边界,就是UI表现层的东西,也就是View。一套系统最终都是通过View向用户,或者是通过接口向其它系统,呈现出它的全部内容。系统开发是对需求的实现,而架构设计则是对需求的抽象。
在小型项目中,可以不需要什么方法论。把握了需求之后,就着重在表现层的设计,与用户确认好之后,以这个为目标把系统开发出来就OK了。这是很直接的一种方式,也是人的思维模式中比较容易接受的一种方式。在企业级应用的架构中,如果在不同程度上以这种方式驱动,或多或少的以表现层为主体,就会妨碍我们的抽象行为,也就是影响到架构的质量。
MDA的思想中,强调的是先从需求中提取Business Model,然后以Model为主体,对业务逻辑进行分析。在框架层面将M-V-C比较清晰的区分开来,有利于在分析设计思想方面向MDA方式的转换。
3.3 简单,而又强大的框架
忘了是在哪里看到的这样一句话。说它简单,因为整个Maverick.Net项目的代码很少,用于实现的主要对象也不多,它仅仅用一个框架性的协议把M-V-C之间作分离,对Model、View、Controller方面并不做任何过多的扩展、处理。所有关于View的呈现方面、Controller的架构方面等,完全交由使用者自行决定。说它强大,因为整个框架的可扩展性相当不错。
但并不是说这么好的一个框架,拿过来用就是了。说它好,可能很大程度上是在指它的思想层面,并不能代表它目前的实现就是最好的或者说非常合理的。随便列举几点:
a) 有考虑多语言的方案,但是没有实现。
b) 它的Controller是基于类的层面配置,这个该如何使用?
方案1:把它的Controller当作Business Facade,所以可能需要为每个Command写一个Controller类,用这个类去整合与Business Model相对应的各个Business Rule类。这种方案有利于分布式、SOA的运用,但是每个Command开发个Controller显得有点繁琐。
方案2:把Controller作为完整的Business Logic封装,在上下文中使用参数等其它机制,将Command关联到Controller的方法层面上。
c) Controller + View类型的选择
方案1:使用aspx类型的Controller。这种方式的优点在于能够使用ASP.Net的机制,包括常规的PostBack事件机制、用户控件等,或者整合ASP.Net其它一些东西,例如Atlas等,可能都会比较方便。但以我个人的观点来看,不提倡将*.aspx.cs作为Controller,而是纯粹的作为View的一部分。*.aspx.cs只负责利用ASP.Net的机制,从页面获取参数、创建Model对象传递给控制类;在从控制类重新得到Model等相关信息之后(Controller处理完毕的结果),再利用ASP.Net机制将这些信息进行呈现。
当然,将*.aspx.cs作为轻量级的Business Facade也是可以考虑的方案。
方案2:使用xml或者Maverick.Net标准的document方式。
其实这两种方式之间的差别还是很大的。使用xml方式的话,将使用XsltTransform进行转换,可能需要编写大量的xslt文件。使用Maverick.Net标准的document方式,是指完全类似ASP的方式了,使用DocumentTransform进行转换。这两种方式都是使用Maverick.Net的典型工作方式,即以.m作为请求页面的后缀,由Dispatcher对HttpRequest进行处理。在页面编写时,它们都不能使用ASP.Net特性了,说这种方式下完全抛弃了ASP.Net机制也不过分。所以对于ASP.Net开发者将它们视为同一种方式也不奇怪了。
直接使用这种方式开发,可能没有多少人能够接受。但是基于这种方式,在View的开发方面下一些功夫,例如封装、通用化模板的扩展、添加适用于Maverick.Net的Ajax方案等,是能够在一定程度上降低这种方式下View开发的繁琐性,达到基本可以接受的程度(相对于ASP.Net开发者而言)。
2006-09-11:补充一副图
相关概念、总体处理流程参考
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,将使用到这些工厂对象。
Modules节点配置示例如下:
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<modules>
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<transform-factory type="mytran" provider="mytran.mytranFactory, mytran" >
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<default-key-name value="mytran" />
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
</transform-factory>
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<view-factory type="myview" provider="myview.myviewFactory, myview">
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<default-key-name value="myview" />
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
</view-factory>
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
</modules>
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访问到这些参数。
参数的配置示例如下:
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<view name="UserView" path="UserView.aspx">
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<param name="param1" value="???" />
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<param name="param2" value="???" />
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
</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的配置节点类似如下:
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<command name="UserQuery">
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<controller class="" />
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<view name="loginRequired" ref="loginRequired"/>
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<view name="success" type="document" path="UserQuery-ch.aspx" mode="ch">
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<transform path=""/>
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
</view>
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<view name="success" type="document" path="UserQuery-en.aspx" mode="en">
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<transform path=""/>
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
</view>
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
</command>
Maverick.Net在多语言的实现方式上并不理想。一个View,如果对应于每一个语言都需要写另外一个页面,这种方式只能说太笨了。这种情况下,如果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,有几种方式的用法:
1. 将View配置成document类型,用path直接指定一个静态xml文件,然后使用xsl解析。例如
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<view type="document" path="welcome.xml">
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
<transform path="outside.xsl"/>
![None.gif](https://www.cnblogs.com/Images/OutliningIndicators/None.gif)
</view>
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。
......
posted
ASP.NET出来了很久了,微软一直强调其ASP.NET是给WEB开发带来了很多的方便,code-behind的方式优化了代码的结构,等云云。然而当我们真正用ASP.NET来开发时,我们发现我们还是陷入到了混乱之中,如:
-
MVC如何实现(虽然code-behind从某种意义上说是实现了C和V的分离,但是还是远远不够)
-
页面间的flow如何处理(还是在代码中采用了hard-code的方法,要修改页面的跳转,必须修改源代码,页面间的关系不能清楚地表现和可配置)
-
多语言怎么实现(虽然可以用资源文件来解决,但是一个单词的中文和英文的长度不一致,很多时候还是要重写2个页面,根据不同的浏览器多语言配置如何实现这些不同页面的选择,也不能很好解决)
-
公用页面模块如何尽量提高可复用性(虽然微软也提供了User Control,但是它始终和ASPX有很多不一样,很难对2者共同对待)
-
等
微软也意识到了这些问题,于是出了个UIPB,但是UIPB也仅仅解决了一小部分的问题,如前面所说的页面跳转的问题。坦白地说,我认为UIPB的设计初衷实在是一个错误的方向。UIPB主要解决一个项目WEB表现层和WINFORM表现层如何能够最大范围的复用的问题。试问有多少项目会有这样的需求?诚然有些项目确实需求2种表现层,但是也是各自完成不同的任务居多。即使是有这种需求,我们也知道WINFORM和WEB有太多的不同,有些WEB中多个页面跳转完成的事情,在WINFORM中仅仅是一个窗口就可以完成。另外WEB中要尽量少用弹出窗口,而WINFORM没有这种限制。因此我认为UIPB还是没有解决WEB层的大多数问题,不能适应现在商业的WEB项目开发。
那么我真的开始苦恼了,.NET项目中应该用什么来实现表现层?看看J2EE阵营,他们确实也苦恼,但他们苦恼的是面对那么多的开源解决方案应该选择那个。有的时候真想改姓J2EE算了,呵呵。
终于前些天看到了MAVERICK.NET项目,实际上这个项目也是从J2EE的MAVERICK项目port过来的。我把Maverick.NET当了下来,研究了几天,总算心理稍稍平了点。从我现在对MAVERICK.NET的浅薄的了解中,我认为它至少解决了以下几个问题:
下面我从MAVERICK.NET中自带的一个简单例子来简单说明:
-
配置IIS,将.m的文件用ASP.NET引擎来解析
-
配置web.config,加入以下语句:
<configSections>
<sectionGroup name="Maverick">
<section name="Dispatcher" type="System.Configuration.NameValueSectionHandler,system, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, Custom=null" />
</sectionGroup>
</configSections>
<Maverick>
<Dispatcher>
<add key="configFile" value="maverick.config" />
<add key="currentConfigCommand" value="currentConfig" />
<add key="reloadCommand" value="reload" />
<add key="limitTransformsParam" value="maxTransforms" />
<add key="commandCaseSensitivity" value="insensitive" />
</Dispatcher>
</Maverick>
</system.web>
<httpHandlers>
<!-- 该工程的任何页面访问都会首先执行该httpHandler,实际上是一个dispatcher,
来根据maverick.config的配置来启动相关页面 -->
<add verb="*" path="*.m" type="Maverick.Dispatcher, Maverick" />
</httpHandlers>
</system.web>
-
配置maverick.config文件
<maverick version="2.0" default-view-type="document" default-transform-type="document">
<commands>
<command name="Default.aspx">
<view name="success" type="trivial">
<transform path="~/Wrapper.aspx"/> <!-- 这就是一个标题,也就是嵌入的公用页面模块 -->
</view>
<view name="april" path="~/April.aspx">
<transform path="~/Wrapper.aspx"/>
</view>
<view name="button" path="~/Button.aspx">
<transform path="~/Wrapper.aspx"/>
</view>
</command>
</commands>
</maverick>
-
建立Default.aspx文件,在code-behind文件中添加一个以下方法
protected string viewName = "success"; // 默认显示名为“success”的view
public override string Go(Maverick.Flow.IControllerContext cctx)
{
return this.viewName;
}
在一个button的时间代码中添加如下代码,当button按下时,就会根据maverick.config的配置来显示相应内容
private void Button1_Click(object sender, System.EventArgs e)
{
this.viewName = "button"; //按下button后就会显示名为button的view
}
在一个日历控健的Selection_change事件代码中,添加如下代码
private void Calendar1_SelectionChanged(object sender, System.EventArgs e)
{
if (this.Calendar1.SelectedDate.Month == 4)
this.viewName = "april"; // 当该代码执行时,就会转向名为april的view
}
-
建立April.aspx、Button.aspx、Wrapper.aspx页面
以上的例子至少体现了,Maverick的页面跳转配置实现、公用页面模块灵活配置的2个优点,因为这是一个最简单的实现,所以其他的特点没有完全展现。关于MAVERICK.NET的应用、以及under the hook,我将在后续的post中描述。