Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 572|回复: 0

[默认分类] 写自己的ASP.NET MVC框架(上)

[复制链接]
  • TA的每日心情
    开心
    2021-12-13 21:45
  • 签到天数: 15 天

    [LV.4]偶尔看看III

    发表于 2018-7-8 18:04:35 | 显示全部楼层 |阅读模式
    写了几篇细说之后,今天打算换换口味,还是来写代码吧。所以,这次博客将以实际的代码来展示在ASP.NET平台上开发自己的框架,我希望也能让您发现这并不是件难事。
    我在前面的博客【用Asp.net写自己的服务框架】中,发布了一个用ASP.NET写的服务框架,那个框架我目前仍在使用中。近来,由于时常也会有人问我一些关于ASP.NET MVC的话题,因此,就想再写个自己的MVC框架出来,一方面可以留给自己使用,另外也可以谈谈MVC,尤其可以展示一下在ASP.NET下写框架的乐趣。
    我之所以将写框架看成是件有乐趣的事,是因为:在写框架的过程中会接触许多的技术细节。
    比如:
    1. 为了支持Session需要了解管线过程以及支持Session工作的原理。
    2. 在HttpHandler的映射过程中,HttpHandlerFactory的使用会让设计很灵活。
    3. 反射可以让我们轻松地根据一个URL去寻找匹配的Action以及为Action准备传入参数。
    4. 为了便于测试Action,如何有效的封装框架的功能(这里有许多ASP.NET的技术细节)。
    5. 如何设计让框架更灵活更强大。
    在开始今天的博客之前,我想有必要说说我的框架的规模:
    如果说ASP.NET WebForm是个复杂框架,ASP.NET MVC是个轻量级框架的话,那么,我的MVC框架将只是个微量级的框架。
    但这个微量级的框架却可以提供许多实用的功能(因为我没有引入一些与MVC无关的东西),而且完全遵守MVC的思想而设计。
    由于我的框架规模实在太小,因此,有些地方的确是不够完善,但我认为在大多数情况下是够用的。
    ASP.NET程序的几种开发方式
    时常见到有人在问:到底选择WebForm还是MVC ?
    其实我认为最好是二个都选吧。然后,你去发现它们的优点与缺点,最后,当你觉得它们都不爽时,还是写自己的框架吧。
    我来说说我这样选择的理由:任何东西都有它们的优点,这很正常,所以二个都选就能发现更多的优点,发现优点的过程其实也是自己进步的过程。当然它们也有缺点,发现那些缺点,你自然会想办法去避开,其实这也是个进步的过程。因此,在你吸收优点以及避开缺点的过程中,会感觉它们不再完美(因为自己在进步),再到后来,你会怎么选择,我就不知道了,那就是你自己的事了。而我选择了另一条路:写自己的ASP.NET MVC框架。
    在比较各类框架之前,我想有必要先来总结一下:现在能用ASP.NET开发哪些类型的网站?由于ASP.NET与WCF这类纯服务性质的框架不同,我们主要还是用它来开发一些可与用户交互的界面程序。因此,今天的分类将重要讨论这些界面(UI)的开发方式。
    我认为目前的ASP.NET能支持开发三种类型的网站:
    1. 服务端为中心的网站:所有页面的生成以及交互的逻辑全部服务端来完成,服务端甚至能生成必要的JS代码。
    2. 门户类网站:服务端只负责页面的第一次呈现,用户的交互以及表单的提交全部采用AJAX的方式完成。
    3. 纯AJAX网站:服务端基本上不参与UI的处理,只负责处理数据,UI在客户端由javaScript来生成并控制提交。
    【以服务端为中心的网站】,这类网站有个非常明显的特点,至少在开发上表现地非常明显:服务端要做的事情很多,HTML的生成, 简单的JS交互,客户端验证,等等,全由服务端来处理。在开发这类网站时,由于工作全在服务端,因此如果我们使用ASP.NET开发,自然地,所有的任务都将由aspx, C#这类技术来实现,采用这种方式开发出来的网站,页面与用户的交互性通常不会很友好,比如:提交数据时经常需要整页刷新。
    【门户类网站】,这类网站与之前的【以服务端为中心的网站】有个重要的差别:页面只是呈现数据,表单的提交全采用AJAX方式了。这样做的好处能将显示逻辑与数据更新逻辑有效的分离,不必纠缠在一起(可认为是二个通道),在这种开发模式下,由于页面只负责数据的显示,因此,只要能将业务逻辑尽可能地与UI分离,项目在维护上会容易很多,采用这种方式开发的网站,页面与用户交互的友好性会好很多,而且也不会影响SEO,因此被较多的门户网站采用。
    【纯AJAX网站】,在这类网站中,服务端由于不参与UI处理,网站可以只是些静态的HTML文件,而且在设计页面时,只要留下一些占位符就可以了,UI元素全部由JS来生成。这类网站的客户端通常会选择一个JS的UI框架来支持。这类界面相对于前二种来说,会更漂亮,用户的操作体验也会更友好。但由于页面主要由JS来生成,对SEO的支持较差,因此,特别适合一些后台类的网站项目。
    在前面所列出的三种开发方式中,前二种由于界面部分由服务端来实现,因此选择一个合适的框架,会对开发工作有着非常重要的影响(尤其是第一种)。但是,如果选择第三种方式,那么选择 WebForm 还是 MVC 真的都是浮云了,甚至还可以使用其它的服务框架来支持AJAX的调用。
    喜欢微软的MVC框架的一些人,通常会列举一些WebForm中较为低级的缺点,从而把ASP.NET MVC框架说的很完美,而且是非它不可。这里,我不想谈论它们的优点与缺点,因为我前面已经说过了,在我看来,它们都有优点也同时有各自的缺点。今天,我只想先暂且忘记它们,来实现自己的框架。
    开始吧,看看我的作品。
    介绍我的MVC框架
    我比较喜欢ASP.NET这个平台,因为它们扩展性实在太好了,在它上面,我可以容易地实现自己所需的东西,包括开发自己所需要的WEB框架。通过微软的ASP.NET MVC框架,也让我认识到MVC思想的优点,因此,我的WEB框架也将采用MVC思想来开发,因此,我把自己的这个框架称为【我的MVC框架:MyMVC】。今天的博客也将向您展示这个框架,同时我也会与大家一起分享我在开发框架过程中所使用到的一些技术(或者称为实现方式)。
    为了让大家对我的MVC框架有个感性的认识,我准备了一个示例网站,网站提供二种完全不同的风格,分别采用【门户类网站】和【纯AJAX网站】的方式来开发。在示例网站的首页,程序会让您选择喜欢的界面风格来继续后面的操作,当然,您也可以随时在右上角切换风格。

    我的MVC框架设计架构
    在我的框架中,【页面请求】与【AJAX请求】是分开来实现的。
    因为我前面以对开发方式做过分类,在开发【纯AJAX网站】时,那么就几乎没有页面请求了(或许有,但可以忽略),此时在服务端全是AJAX服务(我喜欢将AJAX的服务端实现称为服务)。
    我将AJAX请求分开来处理是因为:我做的网站中,AJAX的使用非常多,数量都会超过页面请求,而且有时甚至没有ASPX页面,全是AJAX调用,所以我更看重AJAX。
    二种请求(我称为通道)大致是这样的处理过程:

    说明:示意图中并没有直观地反映出【页面请求】与【AJAX请求】在处理过程中的差别,但这个差别是存在的,差别主要在于从URL到Action的映射过程,后面会有详细地介绍。
    以下示意图表示了【我的MVC框架】在处理一个请求时的具体过程:


    今天的博客内容将主要介绍这个框架如何实现AJAX请求处理,页面请求的实现过程将在后续的博客中介绍。

    回忆以往AJAX的实现方式
    我的MVC框架对AJAX的支持来源于我对代码不断重构的过程,为了更好地了解我的MVC框架,我认为有必要先来回忆一下以往是如何(在服务端)实现AJAX的。
    在ASP.NET中有一种比较原始的实现Ajax的方式,那就是创建一个ashx,就像下面的代码:
    1. <%@ WebHandler Language="C#" Class="Handler1" %>
    2. using System;
    3. using System.Web;
    4. public class Handler1 : IHttpHandler {
    5.    
    6.     public void ProcssRequest (HttpContext context) {
    7.         // 【1】. 从context.Request中读取输入参数
    8.         string param1 = context.Request.QueryString["param1"];
    9.         string param2 = context.Request.QueryString["param2"];
    10.         
    11.         // 【2】. 根据上面所获取的参数,调用服务层或者BLL层获取结果
    12.         // var result = CallXxxxMethod(param1, param2);
    13.         // 【3】. 将结果写入context.Response
    14.         context.Response.ContentType = "text/plain";
    15.         context.Response.Write(" result ...... ");
    16.     }
    17.     public bool IsReusable { get { return false; } }
    18. }
    复制代码
    当然了,也有人会选择创建一个空的aspx去代替ashx,而且使用aspx还可以只输出一个HTML片段。
    在这种原始的方式下,整个处理过程可以大致分为注释中所标注的三个阶段。如果使用这种方式去做服务端的AJAX开发,当AJAX的数量到达一定规模后,可以发现:大量的代码是类似。我之所以称为【类似】,是因为它们却实有差别,差别在于:参数的名字不同,参数的类型不同,参数的个数不同,要调用的方法以及返回值不同。
    说实话,这种机械代码我也写过。
    不过,当我发现时这个现象时,我就开始想办法去解决这个问题,因为我非常不喜欢写这类重复性质的代码。
    在重构过程中,也逐渐形成了我自己的AJAX服务端框架。
    后来我把它写到我的第一篇博客中了:【晒晒我的Ajax服务端框架】
    在AJAX的发展过程中,微软曾经推出过ASP.NET AJAX框架,它可以在服务端生成一些JS的代理类,让客户端的JS方便地调用服务端的方法。虽然那个框架设计地很巧妙,并且与WebForm配合地很完美,只可惜那个框架不够流行。后来的WCF通过一些配置也可以让JS去调用,不过,喜欢的人也不多,可能还是因为配置麻烦的缘故吧。当后来微软推出了ASP.NET MVC框架时,一些人开始惊呼:AJAX非ASP.NET MVC框架不可。因为ASP.NET MVC框架可以很容易让JS去调用一个C#方法,从此以后,再也不用去【读参数,调用方法,写输出】这些繁琐的事情了,而且没有WCF那么复杂的配置。的确,他们没有解决的问题,ASP.NET MVC框架很好地解决了。
    今天的博客,我将向大家介绍我的AJAX解决方案,它同样可以很好的解决上面的那些繁琐的过程。
    MyMVC中实现AJAX的方式
    在我的框架中,服务端可以很容易地将一个C#方法公开给客户端的JavaScript来访问,比如下面这个C#方法:
    1. public class AjaxOrder
    2. {
    3.     [Action]
    4.     public void AddOrder(OrderSubmitForm form)
    5.     {
    6.         Order order = form.ConvertToOrderItem();
    7.         BllFactory.GetOrderBLL().AddOrder(order);
    8.     }
    复制代码
    那么客户端就可以通过这个URL地址来调用那个方法:"/AjaxOrder/AddOrder.cspx" ,
    URL中的一些名称与C#类名以及方法的名称的对应关系,请参考下图。
    至于C#方法所需的参数,你就不用担心了,框架会替您准备好,你只要访问就可以了。
    说明:这个Action太简单了,连返回值也没有。后面会有返回值的示例代码,请继续阅读。
    前面的示例可以用下面的图形来表示C#代码与URL的映射关系:

    补充说明一下:按照MVC的标准术语,下文将这类用于处理请求的方法将称为【Action】,Action所在的类型称为Controller。不过,在我的MVC框架中,Action又分【PageAction】和【AjaxAction】。而且,在我的MVC框架中,对Controller限制极少,不会要求您继承什么类型或者实现什么接口,Controller甚至可以是个静态类。
    唯独只要求:1. 包含AjaxAction的Controller必须以Ajax开头包含PageAction的Controller必须以Controller结尾(照顾喜欢微软MVC框架的用户)。加这个限制仅仅是为了快速定位Action,并没有其它原因。2. 类型与方法的可见性为 public (同样仅仅只是为了快速定位) 。
    所以,在我的框架中,Controller的意义将只是一个Action的容器。
    如何使用MyMVC框架中的AJAX功能
    在我的MVC框架中,JS几乎可以透明地直接调用C#方法。比如我有这样一个C#方法:
    1. public class AjaxDemo
    2. {
    3.     [Action]
    4.     public string GetMd5(string input)
    5.     {
    6.         if( input == null )
    7.             input = string.Empty;
    8.         byte[] bb = (new MD5CryptoServiceProvider()).ComputeHash(Encoding.Default.GetBytes(input));
    9.         return BitConverter.ToString(bb).Replace("-", "").ToLower();
    10.     }
    11. }
    复制代码
    方法很简单,可以计算一个字符串的MD5值。下面再来看一下如何在JS中调用:
    1. $("#btnGetMd5").click(function(){
    2.     $.ajax({
    3.         // 以下二个URL地址都是有效的。
    4.         //url: "/AjaxDemo/GetMd5.cspx",
    5.         url: "/AjaxDemo.GetMd5.cspx",
    6.         data: {input: $("#txtInput").val()},
    7.         success: function(responseText){
    8.             $("#spanReslt").text(responseText);
    9.         }
    10.     });
    11. });
    复制代码
    说明一下:这里我使用JQuery这个JavaScript类库来完成客户端的部分。
    在JS代码中,我通过一个URL地址就可以直接访问到前面所定义的C#方法,C#方法所面的参数由$.ajax()的data参数指定。由于实在过于简单,我感觉不需要再对这个示例做更多的解释。
    唯独我要提醒的是:为了安全,JS并不能调用任何一个C#方法(虽然在技术上没有任何难度)。所以,如果您允许一个C#方法公开给JS调用,那么方法必须加[Action]这个Attribute 。
    在前面的示例中,方法的传入参数以及返回值的类型都比较简单,事实上,MyMVC也可以支持复杂的数据类型。例如,以下方法的签名都是有效的:
    1. [Action]
    2. public void Insert(Customer customer)
    3. [Action]
    4. public object Delete(int id, string returnUrl)
    5. [Action]
    6. public object ShowCustomerPicker(string searchWord, int? page)
    7. [Action]
    8. public object Search(OrderSearchInfo info, int? page)
    9. [Action]
    10. public object ShowProductPicker(int? categoryId, string searchWord, int? page)
    11. [Action]
    12. public object LoadModel()
    复制代码
    有了MyMVC,就几乎上不需要再去访问QueryString,Form这些对象了。你需要什么参数,只要写在方法的签名中就可以了。参数可以是简单的数据类型,也可以是自定义的数据类型,参数的个数也没有限制。
    不过,有一点我要提醒您:所有的数据来源只有二个地方:QueryString和Form,框架只读取这二个地方,而且直接访问它们的索引器。由于QueryString,Form这二个类型都是NameValueCollection,而NameValueCollection的索引器在实现上有点独特,因此请大家注意它们的返回值。关于NameValueCollection的细节描述,可以参考我的博客【细说 Request[]与Request.Params[]】,今天我就不再重谈这个细节话题了。
    在读取参数时,万一出现key重复了怎么办?
    框架还提供另一种解决方案,那就是您可以在C#的方法的签名中,声明NameValueCollection类型的变量,变量名可以从【Form,QueryString,Headers,ServerVariables】中选择。注意:对于后二者,框架本身也是不读取的,如果需要读取,只能使用这个方法来获取。示例代码如下:
    1. [Action]
    2. public string TestNameValueCollection(NameValueCollection queryString, NameValueCollection form,
    3.     NameValueCollection headers, NameValueCollection serverVariables)
    4. {
    5.     StringBuilder sb = new StringBuilder();
    6.     foreach( string key in queryString.AllKeys )
    7.         sb.AppendFormat("queryString, {0} = {1}\r\n", key, queryString[key]);
    8.     foreach( string key in form.AllKeys )
    9.         sb.AppendFormat("form, {0} = {1}\r\n", key, form[key]);
    10.     foreach( string key in headers.AllKeys )
    11.         sb.AppendFormat("headers, {0} = {1}\r\n", key, headers[key]);
    12.     foreach( string key in serverVariables.AllKeys )
    13.         sb.AppendFormat("serverVariables, {0} = {1}\r\n", key, serverVariables[key]);
    14.     return sb.ToString();
    15. }
    复制代码
    代码中,我同时要求框架给出这四个集合,事实上,您可以根据实际情况来决定需要多少个参数。
    注意:
    1. 参数名称是大小写【不敏感】的。
    2. 类型一定要求是NameValueCollection 。
    3. 框架会优先读取QueryString,如果没有则会查看Form
    4. 千万不要在Action中使用HttpContext.Current.Request.QueryString[]的方式读取来自客户端的参数。
    关于参数,还有一种特殊的情况:我在博客【细说 Form (表单)】中曾提到过,例如,我有这样二个类型,它们的结构一样:
    1. public class Customer
    2. {
    3.     public string Name;
    4.     public string Tel;
    5. }
    6. public class Salesman
    7. {
    8.     public string Name { get; set; }
    9.     public string Tel { get; set; }
    10. }
    复制代码
    如果此时我有这样一个C#方法,又该如何处理呢?
    1. [Action]
    2. public string TestCustomerType(Customer customer, Salesman salesman)
    3. {
    4.     return "customer.Name = " + customer.Name + "\r\n" +
    5.         "customer.Tel = " + customer.Tel + "\r\n" +
    6.         "salesman.Name = " + salesman.Name + "\r\n" +
    7.         "salesman.Name = " + salesman.Tel;
    8. }
    复制代码
    上面的示例也可以理解成:一模一样的参数类型,就是要出现多次,再或者,多个不同的自定义类型中,有些成员的名称是相同的。
    此时我的框架在设计时与微软的MVC框架一样,要求在HTML中对name做特殊的设置,示例代码如下:
    1. <form action="/AjaxDemo2/TestCustomerType.cspx" method="post">
    2. <p>客户名称: <input type="text" name="customer.Name" style="width: 300px" /></p>
    3. <p>客户电话: <input type="text" name="customer.Tel" style="width: 300px" /></p>
    4. <p>销售员名称: <input type="text" name="salesman.Name" style="width: 300px" /></p>
    5. <p>销售员电话: <input type="text" name="salesman.Tel" style="width: 300px" /></p>
    6. <p><input type="submit" value="提交" /></p>
    7. </form>
    复制代码
    此时要求:input标签中的name必须能够反映C#方法的参数名以及类型中所包含的数据成员名称。
    注意:在MyMVC框架中,自定义的数据类型所包含的数据成员不要求是属性,字段(Field)也是完全受支持的。
    配置MyMVC框架
    MyMVC框架在使用前,必须配置。
    在前面的示例中,"/AjaxDemo2/TestCustomerType.cspx" 这样的URL地址,按照ASP.NET的默认设置,它是不能被映射到一个有效的处理器的,那时,将出现一个404异常。因此,为了使用MyMVC中对AJAX的支持,必须做以下配置:
    1. <httpHandlers>
    2.     <add path="*Ajax*/*.cspx,*Ajax*.*.cspx" verb="*"
    3.                                 type="MyMVC.AjaxHandlerFactory, MyMVC" validate="true"/>
    4. </httpHandlers>
    复制代码
    如果在IIS7的环境中运行,还需要以下配置:
    1. <system.webServer>
    2.     <validation validateIntegratedModeConfiguration="false"/>
    3.     <security>
    4.         <requestFiltering>
    5.             <fileExtensions>
    6.                 <remove fileExtension=".cspx"/>
    7.                 <add fileExtension=".cspx" allowed="true"/>
    8.             </fileExtensions>
    9.         </requestFiltering>
    10.     </security>
    11.     <handlers>
    12.         <add name="AjaxHandlerFactory" verb="*" path="*Ajax*/*.cspx"
    13.                         type="MyMVC.AjaxHandlerFactory, MyMVC" preCondition="integratedMode"/>
    14.     </handlers>
    15. </system.webServer>
    复制代码
    在示例代码中,我使用了【cspx】这个扩展名,如果您不喜欢,也可以选择您所喜欢的扩展名,这个不是问题。
    关于配置参数中的【path】属性,请参考我的上篇博客【细说 HttpHandler 的映射过程】,这里也不再重新解释。如果没有看过的,建议还是去看一下,下面将会用到那些知识,因为它非常重要。
    MyMVC框架的实现原理 - 映射处理器(入口)
    前面谈到了MyMVC框架的配置,通过那个配置,相当于在ASP.NET中为MyMVC注册了一个入口点。
    根据上面的配置,符合条件的请求将会被映射给AjaxHandlerFactory。既然是这样,我们来看一下这个入口点的实现代码:
    1. internal sealed class AjaxHandlerFactory : IHttpHandlerFactory
    2. {
    3.     public IHttpHandler GetHandler(HttpContext context,
    4.                         string requestType, string virtualPath, string physicalPath)
    5.     {
    6.         // 根据请求路径,定位到要执行的Action
    7.         ControllerActionPair pair = UrlParser.ParseAjaxUrl(virtualPath);
    8.         if( pair == null )
    9.             ExceptionHelper.Throw404Exception(context);
    10.         // 获取内部表示的调用信息
    11.         InvokeInfo vkInfo = ReflectionHelper.GetAjaxInvokeInfo(pair);
    12.         if( vkInfo == null )
    13.             ExceptionHelper.Throw404Exception(context);
    14.         // 创建能够调用Action的HttpHandler
    15.         return ActionHandler.CreateHandler(vkInfo);
    16.     }
    17.    
    18.     public void ReleaseHandler(IHttpHandler handler)
    19.     {
    20.     }
    21. }
    复制代码
    代码中,每个步骤做了什么事情,注释中有说明,不需要再重复说明。最后创建的ActionHandler的实现代码如下:
    1. internal class ActionHandler : IHttpHandler
    2. {
    3.     internal InvokeInfo InvokeInfo;
    4.     public void ProcessRequest(HttpContext context)
    5.     {
    6.         // 调用核心的工具类,执行Action
    7.         ActionExecutor.ExecuteAction(context, this.InvokeInfo);
    8.     }
    9.     public bool IsReusable
    10.     {
    11.         get { return false; }
    12.     }
    复制代码
    整个入口点就是这样的。
    有没人想过:为什么不直接在web.config中映射到这个ActionHandler呢?
    答案在后面,请继续阅读。
    MyMVC框架的实现原理 - 对Session的支持
    前面有一个方法的实现我故意没有贴出,那么是ActionHandler.CreateHandler()这个静态方法。现在是时候来贴它了:
    1. public static ActionHandler CreateHandler(InvokeInfo vkInfo)
    2. {
    3.     SessionMode mode = vkInfo.GetSessionMode();
    4.     if( mode == SessionMode.NotSupport )
    5.         return new ActionHandler { InvokeInfo = vkInfo };
    6.     else if( mode == SessionMode.ReadOnly )
    7.         return new ReadOnlySessionActionHandler { InvokeInfo = vkInfo };
    8.     else
    9.         return new RequiresSessionActionHandler { InvokeInfo = vkInfo };
    10. }
    复制代码
    这段代码又涉及另外二个类型,它们的实现代码如下:
    1. internal class RequiresSessionActionHandler : ActionHandler, IRequiresSessionState
    2. {
    3. }
    4. internal class ReadOnlySessionActionHandler :
    5.                         ActionHandler, IRequiresSessionState, IReadOnlySessionState
    6. {
    7. }
    复制代码
    不要感到奇怪,这二个类型的确没有任何代码。
    它们除了从ActionHandler继承而来,还实现了另外二个接口,那二个接口我在博客【Session,有没有必要使用它?】中已有详细的解释,不明白的朋友,可以去阅读那篇博客。
    再来回答前面那个问题:为什么不直接在web.config中映射到这个ActionHandler呢?
    答:如果这样配置,那么对Session的支持将只有一种模式!
    在这个框架中,我采用HttpHandlerFactory就可以轻松地实现对多种Session模式的支持。
    说到这里,我真的感觉上篇博客【细说 HttpHandler 的映射过程】的研究成果太有意义了,是它给【MyMVC对Session完美的支持】提供了灵感。
    老实说:我是不使用Session的。
    但看到以前的博客中有些人还是坚持使用Session,所以就决定在MyMVC中支持这个功能,毕竟支持Session不是件难事。
    下面再来说说如何支持Session 。
    1. [SessionMode(SessionMode.Support)]
    2. [Action]
    3. public int TestSessionMode(int a)
    4. {
    5.     // 一个累加的方法,检验是否可以访问Session
    6.     // 警告:示例代码的这样做法会影响Action的单元测试。
    7.     if( System.Web.HttpContext.Current.Session == null )
    8.         throw new InvalidOperationException("Session没有开启。");
    9.     object obj = System.Web.HttpContext.Current.Session["counter"];
    10.     int counter = (obj == null ? 0 : (int)obj);
    11.     counter += a;
    12.     System.Web.HttpContext.Current.Session["counter"] = counter;
    13.     return counter;
    14. }
    复制代码
    在上面这段代码中,我加了一个[SessionMode]的Attribute,用它可以指定Action的Session支持模式,SessionMode是个枚举值,定义如下:
    1. /// <summary>
    2. /// Action所支持的Session模式
    3. /// </summary>
    4. public enum SessionMode
    5. {
    6.     /// <summary>
    7.     /// 不支持
    8.     /// </summary>
    9.     NotSupport,
    10.     /// <summary>
    11.     /// 全支持
    12.     /// </summary>
    13.     Support,
    14.     /// <summary>
    15.     /// 仅支持读取
    16.     /// </summary>
    17.     ReadOnly
    18. }
    复制代码
    MyMVC框架支持以上三种不同的Session模式,默认是关闭的,如果需要使用,请显式指定。[SessionMode]既可以用于Controller类型,也可以用于Action 。
    注意:Session的使用将会给Action的单元测试带来麻烦。
    MyMVC框架的实现原理 - 对OutputCache的支持
    MyMVC框架对OutputCache也有着很好的支持。下面的代码演示了如何使用OutputCache:
    1. [OutputCache(Duration=10, VaryByParam="none")]
    2. [Action]
    3. public string TestOutputCache()
    4. {
    5.     return DateTime.Now.ToString();
    6. }
    复制代码
    如果在浏览器中访问这个地址:http://localhost:34743/AjaxDemo/TestOutputCache.cspx
    会发现结果在10秒钟内不会有改变(F5刷新),如果打开Fiddler2,会看到304的响应。

    [OutputCache]所支持的属性较多,这里就不一一列出了,下面再来说说它的实现原理。
    我在博客【细说 ASP.NET控制HTTP缓存】曾分析过ASP.NET Page的缓存页实现原理,其中有个小节【缓存页的服务端编程】专门分析了Page对OutputCache的实现过程,在MyMVC中,就是使用的这种方法,具体过程可以参考那篇博客。补充一句:微软的ASP.NET MVC也是这样做的,它也是借助了Page的强大功能。
    MyMVC中的代码:
    1. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
    2. public class OutputCacheAttribute : Attribute
    3. {
    4.     private OutputCacheParameters _cacheSettings = new OutputCacheParameters();
    5.     // 略过一些属性。
    6.     internal void SetResponseCache(HttpContext context)
    7.     {
    8.         if( context == null )
    9.             throw new ArgumentNullException("context");
    10.         OutputCachedPage page = new OutputCachedPage(_cacheSettings);
    11.         page.ProcessRequest(context);
    12.     }
    13.     private sealed class OutputCachedPage : Page
    14.     {
    15.         private OutputCacheParameters _cacheSettings;
    16.         public OutputCachedPage(OutputCacheParameters cacheSettings)
    17.         {
    18.             this.ID = Guid.NewGuid().ToString();
    19.             _cacheSettings = cacheSettings;
    20.         }
    21.         protected override void FrameworkInitialize()
    22.         {
    23.             base.FrameworkInitialize();
    24.             InitOutputCache(_cacheSettings);
    25.         }
    26.     }
    27. }
    复制代码
    在OutputCacheAttribute类的用法中,清楚地指出适用于类型与方法,因此,这个Attribute可以用于Controller和Action 。
    说明:OutputCacheAttribute与SessionModeAttribute类似,都可以用于Controller和Action,同时使用时,Action优先匹配,代码如下:
    1. internal sealed class InvokeInfo
    2. {
    3.     public ControllerDescription Controller;
    4.     public ActionDescription Action;
    5.     public object Instance;
    6.     public OutputCacheAttribute GetOutputCacheSetting()
    7.     {
    8.         if( this.Action != null && this.Action.OutputCache != null )
    9.             return this.Action.OutputCache;
    10.         if( this.Controller != null && this.Controller.OutputCache != null )
    11.             return this.Controller.OutputCache;            
    12.         return null;
    13.     }
    14.     public SessionMode GetSessionMode()
    15.     {
    16.         if( this.Action != null && this.Action.SessionMode != null )
    17.             return this.Action.SessionMode.SessionMode;
    18.         if( this.Controller != null && this.Controller.SessionMode != null )
    19.             return this.Controller.SessionMode.SessionMode;            
    20.         return SessionMode.NotSupport;
    21.     }
    22. }
    复制代码
    因此,框架只要选择一个时机调用SetResponseCache()方法就可以了,至于这个调用时机出现在哪里,请继续阅读。
    MyMVC框架的实现原理 - 查找Action的过程
    前面有张图片反映了从URL地址到Action的映射过程:

    下面再来谈谈这个过程的实现。
    首先,我们要先在web.config中注册MyMVC的HttpHandlerFactory,它是整个框架的入口。
    在ASP.NET的管线过程中,会调用GetHandler()方法,终于我的代码有机会运行了!
    框架执行的第一行代码是:
    1. // 根据请求路径,定位到要执行的Action
    2. ControllerActionPair pair = UrlParser.ParseAjaxUrl(virtualPath);
    复制代码
    ControllerActionPair是我定义的一个表示Controller以及Action名字的值对类型:
    1. public sealed class ControllerActionPair
    2. {
    3.     public string Controller;
    4.     public string Action;
    5. }
    复制代码
    静态方法UrlParser.ParseAjaxUrl()就是专门用来解析URL并返回ControllerActionPair的:
    1. internal static class UrlParser
    2. {
    3.     // 用于匹配Ajax请求的正则表达式,
    4.     // 可以匹配的URL:/AjaxClass/method.cspx?id=2
    5.     // 注意:类名必须Ajax做为前缀
    6.     internal static readonly string AjaxUrlPattern
    7.         = @"/(?<name>(\w[\./\w]*)?(?=Ajax)\w+)[/\.](?<method>\w+)\.[a-zA-Z]+";
    8.     public static ControllerActionPair ParseAjaxUrl(string path)
    9.     {
    10.         if( string.IsNullOrEmpty(path) )
    11.             throw new ArgumentNullException("path");
    12.         Match match = Regex.Match(path, AjaxUrlPattern);
    13.         if( match.Success == false )
    14.             return null;
    15.         return new ControllerActionPair {
    16.             Controller = match.Groups["name"].Value.Replace("/", "."),
    17.             Action = match.Groups["method"].Value
    18.         };
    19.     }
    20. }
    复制代码
    代码很简单,核心其实就是那个正则表达式,从URL中提取Controller,Action的名字全靠它。
    至于正则表达式的使用,我想这是个基本功,这里就略过了。
    再来看AjaxHandlerFactory的第二个调用:
    1. // 获取内部表示的调用信息
    2. InvokeInfo vkInfo = ReflectionHelper.GetAjaxInvokeInfo(pair);
    复制代码
    ReflectionHelper类是一个内部使用的工具类,专门用于反射处理,AjaxAction查找过程的相关代码如下(注意代码中的注释):
    1. internal static class ReflectionHelper
    2. {
    3.     // 保存AjaxController的列表
    4.     private static List<ControllerDescription> s_AjaxControllerList;
    5.     // 保存AjaxAction的字典
    6.     private static Hashtable s_AjaxActionTable = Hashtable.Synchronized(
    7.                                         new Hashtable(4096, StringComparer.OrdinalIgnoreCase));
    8.     // 用于从类型查找Action的反射标记
    9.     private static readonly BindingFlags ActionBindingFlags =
    10.         BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase;
    11.     static ReflectionHelper()
    12.     {
    13.         InitControllers();
    14.     }
    15.     /// <summary>
    16.     /// 加载所有的Controller
    17.     /// </summary>
    18.     private static void InitControllers()
    19.     {
    20.         s_AjaxControllerList = new List<ControllerDescription>(1024);
    21.         ICollection assemblies = BuildManager.GetReferencedAssemblies();
    22.         foreach( Assembly assembly in assemblies ) {
    23.             // 过滤以【System.】开头的程序集,加快速度
    24.             if( assembly.FullName.StartsWith("System.", StringComparison.OrdinalIgnoreCase) )
    25.                 continue;
    26.             try {
    27.                 foreach( Type t in assembly.GetExportedTypes() ) {
    28.                     if( t.Name.StartsWith("Ajax") )
    29.                         s_AjaxControllerList.Add(new ControllerDescription(t));
    30.                 }
    31.             }
    32.             catch { }
    33.         }
    34.         // 用于Ajax调用的Action信息则采用延迟加载的方式。
    35.     }
    36.    
    37.     /// <summary>
    38.     /// 根据要调用的controller名返回对应的Controller (适用于Ajax调用)
    39.     /// </summary>
    40.     /// <param name="controller"></param>
    41.     /// <returns></returns>
    42.     private static ControllerDescription GetAjaxController(string controller)
    43.     {
    44.         if( string.IsNullOrEmpty(controller) )
    45.             throw new ArgumentNullException("controller");
    46.         // 查找类型的方式:如果有点号,则按全名来查找(包含命名空间),否则只看名字。
    47.         // 本框架对于多个匹配条件的类型,将返回第一个匹配项。
    48.         if( controller.IndexOf(".") > 0 )
    49.             return s_AjaxControllerList.FirstOrDefault(
    50.                 t => string.Compare(t.ControllerType.FullName, controller, true) == 0);
    51.         else
    52.             return s_AjaxControllerList.FirstOrDefault(
    53.                 t => string.Compare(t.ControllerType.Name, controller, true) == 0);
    54.     }
    55.     /// <summary>
    56.     /// 根据要调用的方法名返回对应的 Action (适用于Ajax调用)
    57.     /// </summary>
    58.     /// <param name="controller"></param>
    59.     /// <param name="action"></param>
    60.     /// <returns></returns>
    61.     private static ActionDescription GetAjaxAction(Type controller, string action)
    62.     {
    63.         if( controller == null )
    64.             throw new ArgumentNullException("controller");
    65.         if( string.IsNullOrEmpty(action) )
    66.             throw new ArgumentNullException("action");
    67.         // 首先尝试从缓存中读取
    68.         string key = action + "@" + controller.FullName;
    69.         ActionDescription mi = (ActionDescription)s_AjaxActionTable[key];
    70.         if( mi == null ) {
    71.             // 注意:这里不考虑方法的重载。
    72.             MethodInfo method = controller.GetMethod(action, ActionBindingFlags);
    73.             if( method == null )
    74.                 return null;
    75.             var attrs = (ActionAttribute[])method.GetCustomAttributes(typeof(ActionAttribute), false);
    76.             if( attrs.Length != 1 )
    77.                 return null;
    78.             
    79.             mi = new ActionDescription(method, attrs[0]);
    80.             s_AjaxActionTable[key] = mi;
    81.         }
    82.         return mi;
    83.     }
    84.     /// <summary>
    85.     /// 根据一个AJAX的调用信息(类名与方法名),返回内部表示的调用信息。
    86.     /// </summary>
    87.     /// <param name="pair"></param>
    88.     /// <returns></returns>
    89.     public static InvokeInfo GetAjaxInvokeInfo(ControllerActionPair pair)
    90.     {
    91.         if( pair == null )
    92.             throw new ArgumentNullException("pair");
    93.         InvokeInfo vkInfo = new InvokeInfo();
    94.         vkInfo.Controller = GetAjaxController(pair.Controller);
    95.         if( vkInfo.Controller == null )
    96.             return null;
    97.         vkInfo.Action = GetAjaxAction(vkInfo.Controller.ControllerType, pair.Action);
    98.         if( vkInfo.Action == null )
    99.             return null;
    100.         
    101.         if( vkInfo.Action.MethodInfo.IsStatic == false )
    102.             vkInfo.Instance = Activator.CreateInstance(vkInfo.Controller.ControllerType);
    103.         
    104.         return vkInfo;
    105.     }
    复制代码
    上面就是AjaxAction查找相关的4段代码:
    1. 在ReflectionHelper的静态构造函数中,我加载了所有AjaxController。
    2. GetAjaxController方法用于根据一个Controller的名字返回Controller的类型描述。
    3. GetAjaxAction方法用于根据Controller的类型以及要调用的Action的名字返回Action的描述信息。
    4. GetAjaxInvokeInfo方法用于根据从AjaxHandlerFactory得到的ControllerActionPair描述转成更具体的描述信息。
    代码中,Action的查找过程采用了延迟的加载模式,保存Action描述信息的集合我采用了线程安全的Hashtable
    好了,上面那段代码我想说的就这些,剩下的就只些反射的使用,这也算是个基本功,而且也不是三言二语能说清楚的。因此,我打算继续谈其它的内容了。
    MyMVC框架的实现原理 - 执行Action的过程
    在AjaxHandlerFactory的GetHandler方法中,最后将创建一个ActionHandler,这是一个HttpHandler,它将在管线的第15个步骤中被调用(引用博客【用Asp.net写自己的服务框架】中的顺序)。
    注意:AjaxHandlerFactory的GetHandler方法是在第10步中调用的,第12步就是在准备Session(非进程内模式),因此,必须在第12步前决定Session的使用方式。
    所有的Action代码都是在ActionHandler中执行的:
    1. internal class ActionHandler : IHttpHandler
    2. {
    3.     internal InvokeInfo InvokeInfo;
    4.     public void ProcessRequest(HttpContext context)
    5.     {
    6.         // 调用核心的工具类,执行Action
    7.         ActionExecutor.ExecuteAction(context, this.InvokeInfo);
    8.     }
    复制代码
    ExecuteAction的实现过程如下:
    1. internal static void ExecuteAction(HttpContext context, InvokeInfo vkInfo)
    2. {
    3.     if( context == null )
    4.         throw new ArgumentNullException("context");
    5.     if( vkInfo == null )
    6.         throw new ArgumentNullException("vkInfo");
    7.     // 调用方法
    8.     object result = ExecuteActionInternal(context, vkInfo);
    9.     // 设置OutputCache
    10.     OutputCacheAttribute outputCache = vkInfo.GetOutputCacheSetting();
    11.     if( outputCache != null )
    12.         outputCache.SetResponseCache(context);
    13.     // 处理方法的返回结果
    14.     IActionResult executeResult = result as IActionResult;
    15.     if( executeResult != null ) {
    16.         executeResult.Ouput(context);
    17.     }
    18.     else {
    19.         if( result != null ) {
    20.             // 普通类型结果
    21.             context.Response.ContentType = "text/plain";
    22.             context.Response.Write(result.ToString());
    23.         }
    24.     }
    25. }
    26. internal static object ExecuteActionInternal(HttpContext context, InvokeInfo info)
    27. {
    28.     // 准备要传给调用方法的参数
    29.     object[] parameters = GetActionCallParameters(context, info.Action);
    30.     // 调用方法
    31.     if( info.Action.HasReturn )
    32.         return info.Action.MethodInfo.Invoke(info.Instance, parameters);
    33.     else {
    34.         info.Action.MethodInfo.Invoke(info.Instance, parameters);
    35.         return null;
    36.     }
    37. }
    复制代码
    前面我不是没有说调用SetResponseCache()的时机嘛,这个时机就是在这里:执行完Action后。
    设置过OutputCache后,就是处理返回值了。
    前面那段代码中,还有一句重要的调用:
    1. // 准备要传给调用方法的参数
    2. object[] parameters = GetActionCallParameters(context, info.Action);
    复制代码
    这个调用的意义在注释中有解释,关于这个过程的实现方式还请继续阅读。
    MyMVC框架的实现原理 - 如何给方法赋值
    用过反射的人都知道,调用一个方法很简单,但如何给一个【不知签名】的方法准备传入参数呢?
    下面就来回答这个问题,请接着看GetActionCallParameters的实现过程:
    1. private static object[] GetActionCallParameters(HttpContext context, ActionDescription action)
    2. {
    3.     if( action.Parameters == null || action.Parameters.Length == 0 )
    4.         return null;
    5.     object[] parameters = new object[action.Parameters.Length];
    6.     for( int i = 0; i < action.Parameters.Length; i++ ) {
    7.         ParameterInfo p = action.Parameters[i];
    8.         if( p.IsOut )
    9.             continue;
    10.         if( p.ParameterType == typeof(NameValueCollection) ) {
    11.             if( string.Compare(p.Name, "Form", StringComparison.OrdinalIgnoreCase) == 0 )
    12.                 parameters[i] = context.Request.Form;
    13.             else if( string.Compare(p.Name, "QueryString", StringComparison.OrdinalIgnoreCase) == 0 )
    14.                 parameters[i] = context.Request.QueryString;
    15.             else if( string.Compare(p.Name, "Headers", StringComparison.OrdinalIgnoreCase) == 0 )
    16.                 parameters[i] = context.Request.Headers;
    17.             else if( string.Compare(p.Name, "ServerVariables", StringComparison.OrdinalIgnoreCase) == 0 )
    18.                 parameters[i] = context.Request.ServerVariables;
    19.         }
    20.         else{
    21.             Type paramterType = p.ParameterType.GetRealType();
    22.             // 如果参数是简单类型,则直接从HttpRequest中读取并赋值
    23.             if( paramterType.IsSimpleType() ) {
    24.                 object val = ModelHelper.GetValueByKeyAndTypeFrommRequest(
    25.                                                 context.Request, p.Name, paramterType, null);
    26.                 if( val != null )
    27.                     parameters[i] = val;
    28.             }
    29.             else {
    30.                 // 自定义的类型。首先创建实例,然后给所有成员赋值。
    31.                 // 注意:这里不支持嵌套类型的自定义类型。
    32.                 object item = Activator.CreateInstance(paramterType);
    33.                 ModelHelper.FillModel(context.Request, item, p.Name);
    34.                 parameters[i] = item;
    35.             }
    36.         }
    37.     }
    38.     return parameters;
    39. }
    复制代码
    要理解这段代码还要从前面的【查找Action的过程】说起,在那个阶段,可以获取一个Action的描述,具体在框架内部表示为ActionDescription类型:
    1. internal sealed class ActionDescription : BaseDescription
    2. {
    3.     public ControllerDescription PageController; //为PageAction保留
    4.     public MethodInfo MethodInfo { get; private set; }
    5.     public ActionAttribute Attr { get; private set; }
    6.     public ParameterInfo[] Parameters { get; private set; }
    7.     public bool HasReturn { get; private set; }
    8.     public ActionDescription(MethodInfo m, ActionAttribute atrr) : base(m)
    9.     {
    10.         this.MethodInfo = m;
    11.         this.Attr = atrr;
    12.         this.Parameters = m.GetParameters();
    13.         this.HasReturn = m.ReturnType != ReflectionHelper.VoidType;
    14.     }
    15. }
    复制代码
    在构造函数的第三行代码中,我就可以得到这个方法的所有参数情况。
    然后,我在就可以在GetActionCallParameters方法中,循环每个参数的定义,为它们赋值。
    这段代码也解释了前面所说的只支持4种NameValueCollection集合的原因。
    注意了,我在获取每个参数的类型时,是使用了下面的语句:
    1. Type paramterType = p.ParameterType.GetRealType();
    复制代码
    实际上,ParameterType就已经反映了参数的类型,为什么不直接使用它呢?
    答:因为【可空泛型】的原因。这个类型我们需要特殊的处理。
    例如:如果某个参数是这样声明的: int? id
    那么,即使在QueryString中包含id这样一个参数,我也不能直接转成 int? 使用这种类型,必须得到它的【实际类型】。
    GetRealType()是个扩展方法,它就专门完成这个功能:
    1. /// <summary>
    2. /// 得到一个实际的类型(排除Nullable类型的影响)。比如:int? 最后将得到int
    3. /// </summary>
    4. /// <param name="type"></param>
    5. /// <returns></returns>
    6. public static Type GetRealType(this Type type)
    7. {
    8.     if( type.IsGenericType )
    9.         return Nullable.GetUnderlyingType(type) ?? type;
    10.     else
    11.         return type;
    12. }
    复制代码
    如果某个参数的类型是一个自定义的类型,框架会先创建实例(调用无参的构造函数),然后给它的Property, Field赋值。
    注意了:自定义的类型,一定要提供一个无参的构造函数。
    为自定义类型的实例填充数据成员的代码如下:
    1. internal static class ModelHelper
    2. {
    3.     public static readonly bool IsDebugMode;
    4.     static ModelHelper()
    5.     {
    6.         CompilationSection configSection =
    7.                     ConfigurationManager.GetSection("system.web/compilation") as CompilationSection;
    8.         if( configSection != null )
    9.             IsDebugMode = configSection.Debug;
    10.     }
    11.     /// <summary>
    12.     /// 根据HttpRequest填充一个数据实体。
    13.     /// 这里不支持嵌套类型的数据实体,且要求各数据成员都是简单的数据类型。
    14.     /// </summary>
    15.     /// <param name="request"></param>
    16.     /// <param name="model"></param>
    17.     public static void FillModel(HttpRequest request, object model, string paramName)
    18.     {
    19.         ModelDescripton descripton = ReflectionHelper.GetModelDescripton(model.GetType());
    20.         object val = null;
    21.         foreach( DataMember field in descripton.Fields ) {
    22.             // 这里的实现方式不支持嵌套类型的数据实体。
    23.             // 如果有这方面的需求,可以将这里改成递归的嵌套调用。
    24.             val = GetValueByKeyAndTypeFrommRequest(
    25.                                 request, field.Name, field.Type.GetRealType(), paramName);
    26.             if( val != null )
    27.                 field.SetValue(model, val);
    28.         }
    29.     }
    30.     /// <summary>
    31.     /// 读取一个HTTP参数值。这里只读取QueryString以及Form
    32.     /// </summary>
    33.     /// <param name="request"></param>
    34.     /// <param name="key"></param>
    35.     /// <returns></returns>
    36.     public static string GetHttpValue(HttpRequest request, string key)
    37.     {
    38.         string val = request.QueryString[key];
    39.         if( val == null )
    40.             val = request.Form[key];
    41.         return val;
    42.     }
    43.    
    44.     public static object GetValueByKeyAndTypeFrommRequest(
    45.                         HttpRequest request, string key, Type type, string paramName)
    46.     {
    47.         // 不支持复杂类型
    48.         if( type.IsSimpleType() == false )
    49.             return null;
    50.         string val = GetHttpValue(request, key);
    51.         if( val == null ) {
    52.             // 再试一次。有可能是多个自定义类型,Form表单元素采用变量名做为前缀。
    53.             if( string.IsNullOrEmpty(paramName) == false ) {
    54.                 val = GetHttpValue(request, paramName + "." + key);
    55.             }
    56.             if( val == null )
    57.                 return null;
    58.         }
    59.         return SafeChangeType(val.Trim(), type);
    60.     }
    61.     public static object SafeChangeType(string value, Type conversionType)
    62.     {
    63.         if( conversionType == typeof(string) )
    64.             return value;
    65.         if( value == null || value.Length == 0 )
    66.             // 空字符串根本不能做任何转换,所以直接返回null
    67.             return null;
    68.         try {
    69.             // 为了简单,直接调用 .net framework中的方法。
    70.             // 如果转换失败,则会抛出异常。
    71.             return Convert.ChangeType(value, conversionType);
    72.         }
    73.         catch {
    74.             if( IsDebugMode )
    75.                 throw;            // Debug 模式下抛异常
    76.             else
    77.                 return null;    // Release模式下忽略异常(防止恶意用户错误输入)
    78.         }
    79.     }
    80. }
    复制代码
    在给自定义的数据类型实例加载数据前,需要先知道这个实例对象有哪些属性以及字段,这个过程的代码如下:
    1. /// <summary>
    2. /// 返回一个实体类型的描述信息(全部属性及字段)。
    3. /// </summary>
    4. /// <param name="type"></param>
    5. /// <returns></returns>
    6. public static ModelDescripton GetModelDescripton(Type type)
    7. {
    8.     if( type == null )
    9.         throw new ArgumentNullException("type");
    10.    
    11.     string key = type.FullName;
    12.     ModelDescripton mm = (ModelDescripton)s_modelTable[key];
    13.     if( mm == null ) {
    14.         List<DataMember> list = new List<DataMember>();
    15.         (from p in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
    16.          select new PropertyMember(p)).ToList().ForEach(x=>list.Add(x));
    17.         (from f in type.GetFields(BindingFlags.Instance | BindingFlags.Public)
    18.          select new FieldMember(f)).ToList().ForEach(x => list.Add(x));
    19.         mm = new ModelDescripton { Fields = list.ToArray() };
    20.         s_modelTable[key] = mm;
    21.     }
    22.     return mm;
    23. }
    复制代码
    在拿到一个类型的所有属性以及字段的描述信息后,就可以通过循环的方式,根据这些数据成员的名字去QueryString,Form读取所需的数据了。
    参数准备好了,前面的调用就应该没有问题了吧?
    MyMVC框架的实现原理 - 处理返回值
    MyMVC框架处理返回值的时机是在ExecuteAction方法中(前面有那段代码)。
    这里只做个简单的补充说明。
    我为Action的结果定义了一个接口:
    1. public interface IActionResult
    2. {
    3.     void Ouput(HttpContext context);
    4. }
    复制代码
    框架内实现了4种ActionResult:
    1. /// <summary>
    2. /// 表示一个用户控件结果(用户控件将由框架执行)
    3. /// </summary>
    4. public sealed class UcResult : IActionResult
    5. /// <summary>
    6. /// 表示一个重定向的结果
    7. /// </summary>
    8. public sealed class RedirectResult : IActionResult
    9. /// <summary>
    10. /// 一个Json对象结果
    11. /// </summary>
    12. public sealed class JsonResult : IActionResult
    13. /// <summary>
    14. /// 表示一个页面结果(页面将由框架执行)
    15. /// </summary>
    16. public sealed class PageResult : IActionResult
    复制代码
    要输出返回值的时候,不仅使用了IActionResult接口,我还使用下面这个调用:
    1. context.Response.Write(result.ToString());
    复制代码
    不要小看了ToString()的调用。
    对于自定义的数据类型来说,可以用它来控制最终输出给客户端的是JSON或者是XML,或者是您自己定义的文本序列化格式(比如:特殊分隔符拼接而成),因此,它有足够的能力可以取代JsonResult类型,而且同样不影响Action的单元测试。
    ToString()的强大原因在于它是个虚方法,可以被派生类重写。
    所以,如果您只打算返回一个数据实体对象给客户端,那么既可以实现IActionResult接口,还可以重写ToString方法。
    MyMVC框架的实现原理 - 如何返回HTML片段
    AJAX调用中,虽然以返回数据居多,但有时也会要求返回一段HTML,毕竟拼HTML代码在服务端会容易些。
    MyMVC提供UcResult类型,用来将一个用户控件的呈现结果做为HTML输出。当然了,您也可以创建一个Page,采用Page来输出HTML,那么就要用到PageResult类型了。它们的使用代码如下:
    1. [Action]
    2. public object ShowCustomerPicker(string searchWord, int? page)
    3. {
    4.     CustomerSearchInfo info = new CustomerSearchInfo();
    5.     info.SearchWord = searchWord ?? string.Empty;
    6.     info.PageIndex = page.HasValue ? page.Value - 1 : 0;
    7.     info.PageSize = AppHelper.DefaultPageSize;
    8.     CustomerPickerModel data = new CustomerPickerModel();
    9.     data.SearchInfo = info;
    10.     data.List = BllFactory.GetCustomerBLL().GetList(info);
    11.     return new UcResult("/Controls/Style1/CustomerPicker.ascx", data);
    12. }
    复制代码
    由于我从来不用Page输出一段HTML,因此没有准备在Ajax中使用PageResult的示例。但是,它们的使用方法是一样,因为:PageResult和UcResult的构造函数有着一致的签名方式。
    再来说说创建UcResult对象那行代码:传入二个参数,第一个参数表示用户控件的位置(View),第二个参数表示呈现用户控件所需的数据(Model)。至于这个地方为什么要设计二个参数,请关注我的后续博客,因为它涉及到MVC的核心思想,今天的博客不打算谈这个话题。
    MyMVC框架的实现原理 - 多命名空间的支持
    前面的示例代码都是演示了如何设计一个能供JS调用的Action,事实上,您也看到了,其实就是加了个[Action]的方法而已,没有其它的特别之处了。不过,在现实开发中,类型的名字可能会冲突。比如:.NET就引入了命名空间来处理这种冲突的类名。
    MyMVC支持同名的Controller的名字吗?
    答案是肯定的:支持。
    例如,我有下面二个类型。注意它们的名字是相同的。
    1. namespace Fish.AA
    2. {
    3.     public class AjaxTest
    4.     {
    5.         [Action]
    6.         public int Add(int a, int b)
    7.         {
    8.             return a + b;
    9.         }
    10.     }
    11. }
    12. namespace Fish.BB
    13. {
    14.     public class AddInfo
    15.     {
    16.         public int A;
    17.         public int B;
    18.     }
    19.     public class AjaxTest
    20.     {
    21.         [Action]
    22.         public int Add(AddInfo info)
    23.         {
    24.             return info.A + info.B + 10;    // 故意写错。
    25.         }
    26.     }
    27. }
    复制代码
    这二个类型不仅同名,而且还包含了同名的方法。(事实上,方法的签名也可以完全一样。)
    那么,对于这种情况,JS如何去调用它们呢?
    为了回答这个问题,我特意准备了一个示例,HTML代码如下:
    1. <fieldset>
    2.     <legend>Fish.AA.AjaxTest.Add</legend>
    3.     <input type="text" id="txtA1" style="width: 50px" value="3" /> +
    4.     <input type="text" id="txtB1" style="width: 50px" value="2" /> =
    5.     <span id="spanResult1"></span>
    6.     <input type="button" id="btnAdd1" value="Add" />
    7. </fieldset>
    8. <p></p>
    9. <fieldset>
    10.     <legend>Fish.BB.AjaxTest.Add</legend>
    11.     <input type="text" id="txtA2" style="width: 50px" value="3" /> +
    12.     <input type="text" id="txtB2" style="width: 50px" value="2" /> =
    13.     <span id="spanResult2"></span>
    14.     <input type="button" id="btnAdd2" value="Add" />
    15. </fieldset>
    复制代码
    客户端的JS代码如下:
    1. $(function(){
    2.     $("#btnAdd1").click(function(){
    3.         $.ajax({
    4.             url: "/Fish.AA.AjaxTest/Add.cspx",
    5.             data: {a: $("#txtA1").val(), b: $("#txtB1").val()},
    6.             success: function(responseText){
    7.                 $("#spanResult1").text(responseText);
    8.             }
    9.         });
    10.     });
    11.    
    12.     $("#btnAdd2").click(function(){
    13.         $.ajax({
    14.             // 以下二个URL地址都是有效的。
    15.             //url: "/Fish.BB.AjaxTest.Add.cspx",
    16.             url: "/Fish/BB/AjaxTest/Add.cspx",
    17.             data: {a: $("#txtA2").val(), b: $("#txtB2").val()},
    18.             success: function(responseText){
    19.                 $("#spanResult2").text(responseText);
    20.             }
    21.         });
    22.     });
    23. });
    复制代码
    最终的调用结果如下:

    注意:下方的调用结果虽然是错误的,但表示调用的方法是正确的。
    让我们再来回顾一下UrlParser类中定义的那个正则表达式吧:
    1. internal static readonly string AjaxUrlPattern
    2.     = @"/(?<name>(\w[\./\w]*)?(?=Ajax)\w+)[/\.](?<method>\w+)\.[a-zA-Z]+";
    复制代码
    它可以解析这些格式的URL:
    1. /*
    2.     可以解析以下格式的URL:(前三个表示包含命名空间的)
    3.     /Fish.AA.AjaxTest/Add.cspx
    4.     /Fish.BB.AjaxTest.Add.cspx
    5.     /Fish/BB/AjaxTest/Add.cspx
    6.     /AjaxDemo/GetMd5.cspx
    7.     /AjaxDemo.GetMd5.cspx
    8. */
    复制代码
    值得说明的是:这个正则表达式并没有限定用什么样的扩展名,而且也不限制URL中的查询字符串参数。
    但是,就算它再强大,还需要在web.config中注册时,要保证匹配的URL能被传入,否则代码根本没有机会运行。
    重温httpHandlers的注册:
    1. <httpHandlers>
    2.     <add path="*Ajax*/*.cspx,*Ajax*.*.cspx" verb="*"
    3.             type="MyMVC.AjaxHandlerFactory, MyMVC" validate="true"/>
    4. </httpHandlers>
    复制代码
    感谢微软的天才设计,让我可以用通配符的方式写正则表达式。
    关于反射的使用
    反射。
    我想有些人听到这个名字,首先想到的会是低性能,并积极地拒绝使用。
    在那些人的心目中,反射就是低性能的代名词。
    有趣的是,那些人可能在乐滋滋地用着ASP.NET MVC, WCF, EntryFramewok这类框架。
    这里我要说明的是,我并没有说那些框架比较差,而是想说:
    那些框架其实也在大量地使用反射,只是微软没有直接说出来而已。
    我不知道那些不喜欢的反射的人,知道这些框架在大量使用反射时,会有什么样的想法。
    其实想知道一个框架有没有在使用反射,有个简单的识别方法:
    1. 它有没有序列化和反序列化。
    2. 有没有把类名与方法写在字符串中。
    3. 它是不是可以神奇地知道你的任何对象拥有哪些成员?
    4. 有没有使用[Attribute]。您不会以为这个标记是给编译器看的吧?
    WCF简直是把这些全用上了,而且是在大量使用,ASP.NET MVC,EntryFramewok也没少用!
    在实现MyMVC的过程,我大量地使用了反射。
    没办法,不用反射,我真的写不出来什么东西。
    我认为:没有哪个框架可以不使用反射的。
    不使用反射,就意味着:在事先就需要知道将调用哪些类型的哪些方法,这样哪来的灵活性?
    反射还有另一个好处就是简化代码,许多类似的代码,就像前面【回忆以往AJAX的实现方式】中总结的那样。那些类似的代码差别在于:参数的名字不同,参数的类型不同,参数的个数不同,要调用的方法以及返回值不同。那些惊呼【非ASP.NET MVC框架不可】的人或许也是厌倦了这些重复劳动,然而,ASP.NET MVC解决这个问题的办法还是反射。
    所以,不必害怕反射,它的确会影响性能。
    但是,你可以保证你的其它代码都是性能很好吗?
    我见过的低性能代码实在是太多了。
    反射是会影响性能,但好消息是,它对性能的影响是可以优化的,因此,不同的写法,所表现出来的影响也是不一样的。不过,反射的优化也是个复杂的话题,我打算以后有机会再谈。
    结束语
    今天的博客演示了我的MVC框架对于AJAX的支持,也展示了在ASP.NET上开发一个框架的具体过程,虽然还未全部说完,但核心部分已经实现了,那就是:根据URL动态调用一个方法。先说AJAX的实现是因为,它是【无界面】的,无界面的东西通常会比较简单。
    说到【无界面】又让我想到一些人把微软的ASP.NET MVC用于【无界面】的项目,还在信誓旦旦地说:此类型的项目非微软的ASP.NET MVC不可!
    如何评价这些人呢?我只想说:你们还是小点声吧,小心遭人鄙视!
    说到写框架,我想还是有必要再说说我写框架的原因:(引用我在博客【用Asp.net写自己的服务框架】中的原话)
    自己写框架的好处不在于能将它做得多强大,多完美,而是从写框架的过程中,可以学到很多东西。
    一个框架写完了,不在乎要给多少人使用,而是自己感觉有没有进步,这才是关键。

    不管你信不信,那些喜欢说【非什么什么不可】的人,通常是从来不会写框架的。
    MyMVC的介绍还未结束,下篇博客将会继续,下篇博客的重点在于UI部分的支持和实现,这也正是MVC思想存在的必要性,当然也可以反映出MVC框架的核心价值。
    说到这里,我打算给下篇博客做个预告:
    MyMVC框架的后半部分在设计上主要体现了MVC这三者的关系,在设计时主要遵循了Martin Fowler大叔的总结:从模型中分离表现和从视图中分离控制器。
    最终MyMVC对于UI部分支持的结果是:多个URL可以映射到一个Action,一个Action可以将结果指定给多个View来输出。也就是说:请求与View是一种多对多的关系,而中间的Controller只是一个。至于Model的返回,可以由Controller根据运行的上下文条件给出不同的结果,同一个Model可以交给不同的View来显示,也可以返回不同的Model,分别交给不同的View来显示。

    写博客真不容易,为了写这篇博客,我先要写MyMVC框架以及准备示例代码,再准备一些Visio图,最后是文字部分,总共花了整整二个星期。这还不包括前面二篇做为铺垫的博客:【细说 ASP.NET控制HTTP缓存】【细说 HttpHandler 的映射过程】。但是,每当看到自己写的博客在博客园上拥有较高的【推荐数量】时,感觉宽慰了许多。但愿今天的博客能受欢迎。
    感谢 Amy(黄敏)同学为本文所做的校对工作,她已帮我找了好多处文字上的错误。

    获取MyMVC框架源代码及示例代码请点击此处进入下载页面

    如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的推荐按钮。
    如果,您希望更容易地发现我的新博客,不妨点击一下右下角的关注 Fish Li
    因为,我的写作热情也离不开您的肯定支持。
    感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是Fish Li 。
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-23 20:40 , Processed in 0.431189 second(s), 52 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表