写自己的ASP.NET MVC框架

写了几篇细说之后,今天打算换换口味,还是来写代码吧。 所以,这次博客将以实际的代码来展示在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,就像下面的代码:

当然了,也有人会选择创建一个空的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#方法:

public class AjaxOrder
{
    [Action]
    public void AddOrder(OrderSubmitForm form)
    {
        Order order = form.ConvertToOrderItem();
        BllFactory.GetOrderBLL().AddOrder(order);
    }

那么客户端就可以通过这个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#方法:

public class AjaxDemo
{
    [Action]
    public string GetMd5(string input)
    {
        if( input == null )
            input = string.Empty;

        byte[] bb = (new MD5CryptoServiceProvider()).ComputeHash(Encoding.Default.GetBytes(input));
        return BitConverter.ToString(bb).Replace("-", "").ToLower();
    }
}

方法很简单,可以计算一个字符串的MD5值。下面再来看一下如何在JS中调用:

$("#btnGetMd5").click(function(){
    $.ajax({
        // 以下二个URL地址都是有效的。
        //url: "/AjaxDemo/GetMd5.cspx",
        url: "/AjaxDemo.GetMd5.cspx",
        data: {input: $("#txtInput").val()},
        success: function(responseText){
            $("#spanReslt").text(responseText);
        }
    });
});

说明一下:这里我使用JQuery这个JavaScript类库来完成客户端的部分。
在JS代码中,我通过一个URL地址就可以直接访问到前面所定义的C#方法,C#方法所面的参数由$.ajax()的data参数指定。 由于实在过于简单,我感觉不需要再对这个示例做更多的解释。

唯独我要提醒的是:为了安全,JS并不能调用任何一个C#方法(虽然在技术上没有任何难度)。 所以,如果您允许一个C#方法公开给JS调用,那么方法必须加[Action]这个Attribute 。

在前面的示例中,方法的传入参数以及返回值的类型都比较简单,事实上,MyMVC也可以支持复杂的数据类型。 例如,以下方法的签名都是有效的:

有了MyMVC,就几乎上不需要再去访问QueryString,Form这些对象了。 你需要什么参数,只要写在方法的签名中就可以了。参数可以是简单的数据类型,也可以是自定义的数据类型,参数的个数也没有限制。

不过,有一点我要提醒您:所有的数据来源只有二个地方:QueryString和Form,框架只读取这二个地方,而且直接访问它们的索引器。 由于QueryString,Form这二个类型都是NameValueCollection,而NameValueCollection的索引器在实现上有点独特,因此请大家注意它们的返回值。 关于NameValueCollection的细节描述,可以参考我的博客【细说 Request[]与Request.Params[]】, 今天我就不再重谈这个细节话题了。

在读取参数时,万一出现key重复了怎么办?
框架还提供另一种解决方案,那就是您可以在C#的方法的签名中,声明NameValueCollection类型的变量,变量名可以从【Form,QueryString,Headers,ServerVariables】中选择。 注意:对于后二者,框架本身也是不读取的,如果需要读取,只能使用这个方法来获取。示例代码如下:

代码中,我同时要求框架给出这四个集合,事实上,您可以根据实际情况来决定需要多少个参数。

注意:
1. 参数名称是大小写【不敏感】的。
2. 类型一定要求是NameValueCollection 。
3. 框架会优先读取QueryString,如果没有则会查看Form
4. 千万不要在Action中使用HttpContext.Current.Request.QueryString[]的方式读取来自客户端的参数。

关于参数,还有一种特殊的情况:我在博客【细说 Form (表单)】中曾提到过, 例如,我有这样二个类型,它们的结构一样:

如果此时我有这样一个C#方法,又该如何处理呢?

上面的示例也可以理解成:一模一样的参数类型,就是要出现多次,再或者,多个不同的自定义类型中,有些成员的名称是相同的。
此时我的框架在设计时与微软的MVC框架一样,要求在HTML中对name做特殊的设置,示例代码如下:

此时要求:input标签中的name必须能够反映C#方法的参数名以及类型中所包含的数据成员名称。

注意:在MyMVC框架中,自定义的数据类型所包含的数据成员不要求是属性,字段(Field)也是完全受支持的。

配置MyMVC框架

MyMVC框架在使用前,必须配置。
在前面的示例中,"/AjaxDemo2/TestCustomerType.cspx" 这样的URL地址,按照ASP.NET的默认设置,它是不能被映射到一个有效的处理器的, 那时,将出现一个404异常。因此,为了使用MyMVC中对AJAX的支持,必须做以下配置:

<httpHandlers>
    <add path="*Ajax*/*.cspx,*Ajax*.*.cspx" verb="*" 
                                type="MyMVC.AjaxHandlerFactory, MyMVC" validate="true"/>
</httpHandlers>

如果在IIS7的环境中运行,还需要以下配置:

在示例代码中,我使用了【cspx】这个扩展名,如果您不喜欢,也可以选择您所喜欢的扩展名,这个不是问题。

关于配置参数中的【path】属性,请参考我的上篇博客【细说 HttpHandler 的映射过程】,这里也不再重新解释。 如果没有看过的,建议还是去看一下,下面将会用到那些知识,因为它非常重要。

MyMVC框架的实现原理 - 映射处理器(入口)

前面谈到了MyMVC框架的配置,通过那个配置,相当于在ASP.NET中为MyMVC注册了一个入口点。

根据上面的配置,符合条件的请求将会被映射给AjaxHandlerFactory。既然是这样,我们来看一下这个入口点的实现代码:

代码中,每个步骤做了什么事情,注释中有说明,不需要再重复说明。最后创建的ActionHandler的实现代码如下:

整个入口点就是这样的。
有没人想过:为什么不直接在web.config中映射到这个ActionHandler呢?
答案在后面,请继续阅读。

MyMVC框架的实现原理 - 对Session的支持

前面有一个方法的实现我故意没有贴出,那么是ActionHandler.CreateHandler()这个静态方法。现在是时候来贴它了:

这段代码又涉及另外二个类型,它们的实现代码如下:

internal class RequiresSessionActionHandler : ActionHandler, IRequiresSessionState
{
}

internal class ReadOnlySessionActionHandler : 
                        ActionHandler, IRequiresSessionState, IReadOnlySessionState
{
}

不要感到奇怪,这二个类型的确没有任何代码。

它们除了从ActionHandler继承而来,还实现了另外二个接口, 那二个接口我在博客【Session,有没有必要使用它?】中已有详细的解释, 不明白的朋友,可以去阅读那篇博客。

再来回答前面那个问题:为什么不直接在web.config中映射到这个ActionHandler呢?
答:如果这样配置,那么对Session的支持将只有一种模式!
在这个框架中,我采用HttpHandlerFactory就可以轻松地实现对多种Session模式的支持。
说到这里,我真的感觉上篇博客【细说 HttpHandler 的映射过程】的研究成果太有意义了, 是它给【MyMVC对Session完美的支持】提供了灵感。

老实说:我是不使用Session的。
但看到以前的博客中有些人还是坚持使用Session,所以就决定在MyMVC中支持这个功能, 毕竟支持Session不是件难事。

下面再来说说如何支持Session 。

在上面这段代码中,我加了一个[SessionMode]的Attribute,用它可以指定Action的Session支持模式, SessionMode是个枚举值,定义如下:

MyMVC框架支持以上三种不同的Session模式,默认是关闭的,如果需要使用,请显式指定。 [SessionMode]既可以用于Controller类型,也可以用于Action 。

注意:Session的使用将会给Action的单元测试带来麻烦。

MyMVC框架的实现原理 - 对OutputCache的支持

MyMVC框架对OutputCache也有着很好的支持。下面的代码演示了如何使用OutputCache:

[OutputCache(Duration=10, VaryByParam="none")]
[Action]
public string TestOutputCache()
{
    return DateTime.Now.ToString();
}

如果在浏览器中访问这个地址: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中的代码:

在OutputCacheAttribute类的用法中,清楚地指出适用于类型与方法,因此,这个Attribute可以用于Controller和Action 。

说明:OutputCacheAttribute与SessionModeAttribute类似,都可以用于Controller和Action,同时使用时,Action优先匹配,代码如下:

因此,框架只要选择一个时机调用SetResponseCache()方法就可以了,至于这个调用时机出现在哪里,请继续阅读。

MyMVC框架的实现原理 - 查找Action的过程

前面有张图片反映了从URL地址到Action的映射过程:

下面再来谈谈这个过程的实现。
首先,我们要先在web.config中注册MyMVC的HttpHandlerFactory,它是整个框架的入口。
在ASP.NET的管线过程中,会调用GetHandler()方法,终于我的代码有机会运行了!
框架执行的第一行代码是:

// 根据请求路径,定位到要执行的Action
ControllerActionPair pair = UrlParser.ParseAjaxUrl(virtualPath);

ControllerActionPair是我定义的一个表示Controller以及Action名字的值对类型:

public sealed class ControllerActionPair
{
    public string Controller;
    public string Action;
}

静态方法UrlParser.ParseAjaxUrl()就是专门用来解析URL并返回ControllerActionPair的:

代码很简单,核心其实就是那个正则表达式,从URL中提取Controller,Action的名字全靠它。
至于正则表达式的使用,我想这是个基本功,这里就略过了。

再来看AjaxHandlerFactory的第二个调用:

// 获取内部表示的调用信息
InvokeInfo vkInfo = ReflectionHelper.GetAjaxInvokeInfo(pair);

ReflectionHelper类是一个内部使用的工具类,专门用于反射处理,AjaxAction查找过程的相关代码如下(注意代码中的注释):

上面就是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中执行的:

ExecuteAction的实现过程如下:

前面我不是没有说调用SetResponseCache()的时机嘛,这个时机就是在这里:执行完Action后。
设置过OutputCache后,就是处理返回值了。

前面那段代码中,还有一句重要的调用:

// 准备要传给调用方法的参数
object[] parameters = GetActionCallParameters(context, info.Action);

这个调用的意义在注释中有解释,关于这个过程的实现方式还请继续阅读。

MyMVC框架的实现原理 - 如何给方法赋值

用过反射的人都知道,调用一个方法很简单,但如何给一个【不知签名】的方法准备传入参数呢?
下面就来回答这个问题,请接着看GetActionCallParameters的实现过程:

要理解这段代码还要从前面的【查找Action的过程】说起,在那个阶段,可以获取一个Action的描述,具体在框架内部表示为ActionDescription类型:

在构造函数的第三行代码中,我就可以得到这个方法的所有参数情况。
然后,我在就可以在GetActionCallParameters方法中,循环每个参数的定义,为它们赋值。
这段代码也解释了前面所说的只支持4种NameValueCollection集合的原因。

注意了,我在获取每个参数的类型时,是使用了下面的语句:

Type paramterType = p.ParameterType.GetRealType();

实际上,ParameterType就已经反映了参数的类型,为什么不直接使用它呢?
答:因为【可空泛型】的原因。这个类型我们需要特殊的处理。
例如:如果某个参数是这样声明的: int? id 
那么,即使在QueryString中包含id这样一个参数,我也不能直接转成 int? 使用这种类型,必须得到它的【实际类型】。
GetRealType()是个扩展方法,它就专门完成这个功能:

如果某个参数的类型是一个自定义的类型,框架会先创建实例(调用无参的构造函数),然后给它的Property, Field赋值。

注意了:自定义的类型,一定要提供一个无参的构造函数。

为自定义类型的实例填充数据成员的代码如下:

在给自定义的数据类型实例加载数据前,需要先知道这个实例对象有哪些属性以及字段,这个过程的代码如下:

在拿到一个类型的所有属性以及字段的描述信息后,就可以通过循环的方式,根据这些数据成员的名字去QueryString,Form读取所需的数据了。

参数准备好了,前面的调用就应该没有问题了吧?

MyMVC框架的实现原理 - 处理返回值

MyMVC框架处理返回值的时机是在ExecuteAction方法中(前面有那段代码)。
这里只做个简单的补充说明。

我为Action的结果定义了一个接口:

public interface IActionResult
{
    void Ouput(HttpContext context);
}

框架内实现了4种ActionResult:

要输出返回值的时候,不仅使用了IActionResult接口,我还使用下面这个调用:

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类型了。 它们的使用代码如下:

由于我从来不用Page输出一段HTML,因此没有准备在Ajax中使用PageResult的示例。 但是,它们的使用方法是一样,因为:PageResult和UcResult的构造函数有着一致的签名方式。

再来说说创建UcResult对象那行代码:传入二个参数,第一个参数表示用户控件的位置(View),第二个参数表示呈现用户控件所需的数据(Model)。 至于这个地方为什么要设计二个参数,请关注我的后续博客,因为它涉及到MVC的核心思想,今天的博客不打算谈这个话题。

MyMVC框架的实现原理 - 多命名空间的支持

前面的示例代码都是演示了如何设计一个能供JS调用的Action,事实上,您也看到了,其实就是加了个[Action]的方法而已,没有其它的特别之处了。 不过,在现实开发中,类型的名字可能会冲突。比如:.NET就引入了命名空间来处理这种冲突的类名。

MyMVC支持同名的Controller的名字吗?
答案是肯定的:支持。

例如,我有下面二个类型。注意它们的名字是相同的。

这二个类型不仅同名,而且还包含了同名的方法。(事实上,方法的签名也可以完全一样。)

那么,对于这种情况,JS如何去调用它们呢?

为了回答这个问题,我特意准备了一个示例,HTML代码如下:

客户端的JS代码如下:

最终的调用结果如下:

注意:下方的调用结果虽然是错误的,但表示调用的方法是正确的。

让我们再来回顾一下UrlParser类中定义的那个正则表达式吧:

internal static readonly string AjaxUrlPattern
    = @"/(?<name>(\w[\./\w]*)?(?=Ajax)\w+)[/\.](?<method>\w+)\.[a-zA-Z]+";

它可以解析这些格式的URL:

/*
    可以解析以下格式的URL:(前三个表示包含命名空间的)

    /Fish.AA.AjaxTest/Add.cspx
    /Fish.BB.AjaxTest.Add.cspx
    /Fish/BB/AjaxTest/Add.cspx
    /AjaxDemo/GetMd5.cspx
    /AjaxDemo.GetMd5.cspx
*/

值得说明的是:这个正则表达式并没有限定用什么样的扩展名,而且也不限制URL中的查询字符串参数。

但是,就算它再强大,还需要在web.config中注册时,要保证匹配的URL能被传入,否则代码根本没有机会运行。

重温httpHandlers的注册:

<httpHandlers>
    <add path="*Ajax*/*.cspx,*Ajax*.*.cspx" verb="*" 
            type="MyMVC.AjaxHandlerFactory, MyMVC" validate="true"/>
</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框架源代码及示例代码请点击此处进入下载页面

上篇博客【写自己的ASP.NET MVC框架(上)】 我给大家介绍我的MVC框架对于Ajax的支持与实现原理。今天的博客将介绍我的MVC框架对UI部分的支持。

注意:由于这篇博客是基于前篇博客的,因此有些已说过的内容将会直接跳过,也不会给出提示。
所以,如果要想理解这篇博客,那么阅读上篇博客【写自己的ASP.NET MVC框架(上)】则是必要的。

MyMVC的特点

在开发MyMVC的过程中,我吸取了一些ASP.NET WebForm的使用经验,也参考了ASP.NET MVC,也接受了Martin Fowler对于MVC思想的总结。 在设计过程中,我只实现了一些必要的功能,而且没有引入其它的类库与组件,因此,它非常简单,且容易使用。

我们可以这样理解MyMVC:它是一个简单,容易使用,且符合MVC思想的框架。

在MyMVC框架中,View仍然采用了WebForm中的Page,毕竟Page已经使用了十年,能经得起时间的检验,它仍然是我们可信赖的技术。 另一方面,Page也是ASP.NET中默认的HTML输出技术,使用它会比较方便。

MyMVC与微软的ASP.NET MVC不同的是:
1. 不依赖于URL路由组件。
2. 不提供任何HtmlHelper
3. Controller只是一个Action的容器,没有基类的要求。
4. Action处理的请求不区分POST, GET
5. URL可以直接对应一个网站目录中的aspx页面(View)。
6. View的使用是使用路径来指定,与Controller,Action的名字无关。

说明:URL虽然可以与网站中的页面对应,但这种对应并不是必须的,也可以不对应。
而且本质上与WebFrom中的页面执行过程并不相同。
下图反映了在MyMVC中,一个页面请求的执行过程:

介绍示例项目

为了让大家对MyMVC有兴趣,也为了检验MyMVC的设计,我在开发MyMVC的过程,还专门开发一个基于MyMVC的ASP.NET网站示例项目。 网站提供了三种显示风格(也就是三种View),下面以“客户管理”页面为例来展示三种View的不同:

风格1

View对应的代码如下: 『点击此处展开』

风格2

View对应的代码如下: 『点击此处展开』

风格3

View对应的代码如下: 『点击此处展开』

这是三种截然不同的风格,在服务端的代码也是完全不同的。

其中第二种风格,是采用了我上篇博客中总结的【纯AJAX网站】的风格来开发,因此在服务端页面的开发过程中,最为简单,它需要输出的HTML最少,UI部分由客户端的JS来实现。

对于第一种和第三种风格,它们的HTML结构是不同的,页面所能完成的功能也是不同的, 除此之外,它们应该是比较类似的,都是从下面这个泛型类型继承而来:

Inherits="MyPageView<CustomersPageModel>"

从泛型类型继承的好处是:我可以在设计页面时,对于涉及Model的访问,都会有智能提示。比如:

由于有智能提示的支持,可以提高开发效率,并可以避免一些低级的拼写错误。

虽然前面我们可以从图片中看到访问【同一个URL地址】出现【三个不同的页面】,但它们背后的Controller却是同一个: 『点击此处展开』

通过上面代码可以看到我用了4个[PageUrl],这意味着其实我可以使用4种不同的URL都能访问到这三个页面, 而且每一个URL都会根据当前用户所选择的风格,呈现对应的页面。

事实上,我还可以为这个Action指定更多的[PageUrl],让它可以处理更多的URL。关于[PageUrl]的使用与设计目的,请继续往下阅读。

关于URL路由

随着 .net framewrok 3.5 的问世,微软发布了一个【ASP.NET 路由】组件,它的出现给当时的URL优化方法提供了另外一种选择, 不仅如此,它还提供了一些URL重写组件没有的功能:生成URL 。

随着AP.NET MVC的出现,【ASP.NET 路由】成为此框架的直接依赖组件,我们很难有其它的选择, 而且,想不用都不行。

有趣的是:【ASP.NET 路由】这个后生小子的出现,并没有很好地遵守ASP.NET制定的一些规则, 其中最为明显的是:它跳过了【处理器的映射】阶段,导致ASP.NET MVC在支持Session时,很为难。 直到最后ASP.NET 4.0,微软修改了Session的部分实现方式,这样ASP.NET MVC才能最终借此机会解决Session的完整支持问题。

ASP.NET 路由虽然可以生成URL,但它引入了RouteData的概念,要想支持它,需要在框架层面上做许多基础工作。

而且,我认为:
1. 并不是每个网站都需要这种技术,对于不需要URL优化的网站来说,URL路由的使用只是白白地浪费性能。
2. 另一方面,即使需要URL优化,我们还有众多的URL重写组件可供选择,这样可以不用改变现在构架。

因此,MyMVC虽然不支持URL路由,但并不表示不能实现URL优化。

在MVC思想中,Controller应该是处理请求的地方,也是最先运行的部分。 然而在传统的WebForm编程模型中,aspx页面负责处理请求。 因此,必须采取一种方式让最先处理请求的地方从aspx页面中转移,并能提前执行。

而且,将代码从页面移出还有另外二个好处:
1. 被移出的代码肯定是与UI部分无关的,因此,会比较容易测试。
2. 代码与UI的分享也意味着:可以根据运行条件,有选择地将结果交给不同的View来呈现。

考虑到Action可以选择将结果交给不同的View来呈现,而Session也需要支持的问题, 最终我决定,在框架内部使用一个专门的HttpHandler来执行用户的Action,根据Action所要求的Session支持模式, HttpHandlerFactory创建不同的HttpHandler来支持。由于需要使用HttpHandlerFactory,所以必须在web.config中注册。

配置MyMVC框架

MyMVC在使用时,需要在web.config中简单的配置:

<httpHandlers>
    <add path="*.aspx" verb="*" type="MyMVC.MvcPageHandlerFactory, MyMVC" validate="true"/>
</httpHandlers>

如果使用IIS7,则参考以下配置: 『点击此处展开』

我们可以把MvcPageHandlerFactory理解成MyMVC在ASP.NET管线的入口。

注意:
1. 上面的配置代码中,选择aspx这个扩展名并不是必须的,您也可以选择喜欢的扩展名。
2. 如果不喜欢扩展名的映射,可以使用HttpModule,MyMVC中提供的方法也能替代这个过程。

映射处理器(入口)

在web.config中注册MvcPageHandlerFactory后,所有符合条件的请求将会进入MvcPageHandlerFactory。
我们来看一下MvcPageHandlerFactory的实现代码: 『点击此处展开』

从代码中可以看到,MyMVC首先会根据当前的请求地址查找有没有一个Action可以处理它,如果没有,则采用ASP.NET默认的方式来处理。 因此,把【*.aspx】交给MvcPageHandlerFactory是不会有问题的。

说明:创建一个空壳类型AspnetPageHandlerFactory的原因是:不能直接调用PageHandlerFactory的构造函数。

内部初始化

MyMVC在第一次处理请求时,要做一个初始化的过程,这个过程是由MvcPageHandlerFactory中的一个调用引发的:

// 尝试根据请求路径获取Action
InvokeInfo vkInfo = ReflectionHelper.GetPageActionInvokeInfo(virtualPath);

ReflectionHelper有个静态构造函数,虽然上次我已贴出它的代码,但那只是部分代码,以下才是完整的初始化代码: 『点击此处展开』

从以上代码可以看出,在初始化时,MyMVC加载了全部的PageAction ,而AjaxAction却没有采用这种方式来实现,为什么呢? 请继续阅读。

从URL到Action的映射过程

前面我们看到了MyMVC的初始化过程,其实是在ReflectionHelper的构造函数中完成的。 在这个初始化之后,MvcPageHandlerFactory调用ReflectionHelper.GetPageActionInvokeInfo(virtualPath)便可以得到要调用的Action的具体描述。 我称这个过程为:从URL到Action的映射

GetPageActionInvokeInfo方法的实现代码如下: 『点击此处展开』

在介绍这个映射过程之前,让我们再来回顾一下Action的声明代码: 『点击此处展开』

通过ReflectionHelper构造函数中所完成的初始化过程,每个Action的描述会根据[PageUrl]的数量而生成多个字典条目, 因此,在GetPageActionInvokeInfo的实现过程中,也只是简单的查找了这个字典而已,就可以得到所需要的调用信息,从面完成映射的过程。 整个过程可以用以下图形来表示:

在上面的示例中,我使用了"/mvc/Customers"这种URL,显然它并不符合我在web.config中为MvcPageHandlerFactory注册时所指定的URL模式要求。 那么,又该如何处理呢?

虽然这种URL虽然没有扩展名,但我仍然可以通过配置httpHandler的方式来解决,下面的配置就是我们需要的: 『点击此处展开』

在介绍MvcPageHandlerFactory时,MyMVC提供了另一个方法TryGetHandler供外部使用。 因此,在示例网站中,我还可以在Global.asax中调用这个方法来解决前面的那个问题: 『点击此处展开』

对于切换HttpHandler的操作,我有以下建议:
1. 尽量放在HttpModule中去实现。因为可以通过修改配置来切换规则(启用或者禁止),所以会比较灵活。
2. 如果可以通过HttpHandler映射能实现的,尽量首选HttpHandler映射方式。原因:更快,更标准。

PageUrl的设计思想

在前面的示例代码中,我为一个Action添加多个[PageUrl],来标记这个Action可以处理多个URL, 因此,一个Action能处理哪些URL是通过指定[PageUrl]来实现的。

为什么要叫【PageUrl】?
我想或许有些人会有这个疑问。
下面我就来回答这个问题,也可以让大家了解我设计PageUrl的原因:
1. 我们请求一个URL通常是为了得到一个页面显示,因此可以认为一个URL最终可以表示成一个页面。
2. 我也想过使用[Url]这种名称,但感觉太短了,而且Ajax请求也有URL,那么必须显式地加以区分。
所以,我最终决定使用[PageUrl]这个名字。

在Ajax部分,我认为通常只需要完成获取数据以及处理提交数据的功能就可以了。 因此,绝大多数情况下是不要需View的,而且,一个功能与一个URL对应,这样还可以简化问题。 所以,在Ajax部分,我提倡在URL中直接指出要调用哪个Controller中的哪个Action。

在Page部分,事实上也需要一个Action,本来也是可以继续使用这种做法的, 不过,我并没有这种做,理由如下:
1. 我们创建View其实也是创建Page,使用Page的路径不是更好吗?而且WebForm的粉丝或许会更喜欢。
2. 多URL的匹配功能。后面会有详细说明。

由于以上种种原因,我将[PageUrl]设计成与[Action]是独立关系,并且[PageUrl]可以多次指定的。

注意:
1. Url参数中指定的字符串,可以对应一个aspx页面。也可以不对应aspx页面。
2. Url参数中,不要包含QueryString,否则根本不能匹配。
3. 如果您使用URL重写组件,那么此处应该是重写后的路径。

由于我在MvcPageHandlerFactory中使用ASP.NET框架传入的virtualPath并不包含查询参数, 因此,把它理解成页面路径也是非常合适的。

多URL的匹配功能

或许有些人认为多URL匹配一个Action是没有意义的,比如下面的这个Action会更符合常理: 『点击此处展开』

是的,通常情况下,一个Action处理一个URL也是较为常见。
但仍然有二种情况需要这个功能。首先来看下面的示例: 『点击此处展开』

代码所涉及的4个页面在呈现时,由于并不需要数据,但为了能够实现多样式的支持,它们可以共用一个Action,因此这里只是切换一个View的路径而已。

理解上面那句话,可能还需要知道StyleHelper的实现代码: 『点击此处展开』

示例网站的目录结构如下图:

在示例网站中,由于三种风格的截然不同,尤其是在功能与HTML结构上就完全不同,因此根本不可能通过CSS或者SKIN的方式来解决, 所以我为三种风格创建了三个目录,分别存放相应的页面文件。 最终根据用户的选择(Cookie)来决定使用哪个目录下的页面来呈现。

用户设置风格的JS代码如下, 『点击此处展开』

服务端的C#代码如下: 『点击此处展开』

说明:CookieHelper是设计成支持单元测试的,所以不要怀疑这里的代码不符合MVC,后面会专门谈它。

所以,在这种情况下,多个URL映射到一个Action是有意义的。这是【多URL的匹配功能】的第一个用途。

解决老的URL兼容问题

在一个网站的成长过程中,一般会有重构的过程。在重构过程中,或许会删除以前的某些页面,或许调整URL格式。 然而,用户也可能会收藏这个网站的链接,但由于页面重构了,老的链接可能会因此而失效,造成404错误。 此时就要解决URL的兼容问题。

在ASP.NET中,我们可以在web.config配置urlMappings节点来做这样的映射转换。 还有另一种方法是,创建一个HttpModule专门判断是否在请求一些老的URL,如果是,则重定向到新的页面。 总之,不管使用哪种方法,都需要为每个传入请求检查URL是否是老格式的URL, 这个过程会根据一个列表来逐一检查,不过,可惜的是:绝大部分请求可能都是新的URL格式, 而那些兼容方案无疑会浪费很多的CPU资源。

在MyMVC中,可以简单地处理这个问题,就像下面的这个示例一样: 『点击此处展开』

这个“客户管理”页面可能经过了多次重构,没关系,只要把各个版本的地址用[PageUrl]标识出来就可以了,完全不用前面所说的兼容方案, 因此,在URL的兼容处理上没有任何负担,也不会影响性能。

说明:[PageUrl]的顺序并不重要,可以随意调整。

对身份认证的支持

MyMVC也支持一些基本的身份认证,可以通过在Action方法中添加[Authorize]修饰属性来指示。
AuthorizeAttribute的实现代码如下: 『点击此处展开』

认证检查发生在调用Action之前,代码如下: 『点击此处展开』

下面的示例代码演示了它的用法: 『点击此处展开』

注意:
1. 如果一个Action没有使用[Authorize],则表示允许任意用户访问(包括未登录用户)。
2. [Authorize]对于AjaxAction仍然有效。

View的设计方式

在MyMVC中,View采用了ASP.NET Page,不过,我并不建议使用CodeFile文件。 不使用CodeFile文件,我想这是很多喜欢WebForm的人不能接受的。 他们更愿意在CodeFile文件中获取数据,绑定数据,响应事件,处理用户的提交数据。 也正是由于这个原因,才会让其它人认为WebForm是一种对单元测试极差的编程模型。

这里我要表达一下我的观点:代码是否可支持单元测试,这其中最主要的原因还是开发人员自身造成的, 框架的选择只是起到促进或是部分限制的作用。 就算让一些人使用ASP.NET MVC,他们所编写的代码未必就能支持单元测试, 有些人实在太依赖于HttpContext.Current,甚至在ASP.NET MVC中还在写这种代码。

好吧,还是回到Page的设计这个话题上来。MyMVC所提倡的做法与ASP.NET MVC的做法类似, 那就是直接在Page中采用内联的方式显示数据,而不是在CodeFile中绑定数据。 许多人一看到ASP.NET MVC的这种内联写法,感觉又回到了ASP时代,认为是在倒退,其实这只是表面现象。 表面的背后是:代码远离了UI。,也可以理解成:逻辑远离了UI。 这也是正是ASP.NET MVC一直所提倡的:分离关注点。 在新的开发理念中,原来的Page分解成View和Controller,在实现它们时,只关注自身那一部分就可以了, 因此,如果单看Page时,可能是会有前面所说的那种感觉。 另一方面,由于代码远离了UI,或许可以有更多的机会重构它们,使它们的重用性更高。

下面还是来回顾一下MyMVC中Page的代码: 『点击此处展开』

此时,对于呈现所需的数据可以直接从Model对象中获取,但要求在Page指令中指出Model的类型,这样还可以有智能提示的优点。 如果页面需要显示数据,请务必从MyPageView<>继承,它的实现代码如下: 『点击此处展开』

其实也就是一个简单的类型,包含了Model这个属性而已。 至于MyBasePage的实现代码,我们可以忽略它,它是直接从System.Web.UI.Page继承的。

再来一段用户控件的代码: 『点击此处展开』

基本上,与Page的开发方式差不多,只是基类换成了MyUserControlView<>而已。

在这里我认为要补充一点的是:
与ASP.NET MVC不同,MyMVC不提供任何HtmlHelper。
我认为HtmlHelper与MVC思想完全没有关系,因此不提供这些方法。
另一方面,很多人希望更好地控制HTML代码,因此就更没必要提供这些方法了。
如果您认为需要一些必要的HtmlHelper方法,那么可以实现自己喜欢的HtmlHelper类库。

最后我想说的是:页面继承泛型类,还需要一些额外的处理。比如下面的代码:

Inherits="MyPageView<CustomersPageModel>"

要让这种设置能够通过编译,需要在web.config中做如下配置:

<pages  pageParserFilterType="MyMVC.ViewTypeParserFilter, MyMVC"  >

ViewTypeParserFilter的实现代码较长,我就不在此贴出了,可以从本文结尾处下载。

Controller,Action的设计方式

在MyMVC中,Action分为二种:AjaxAction和PageAction。
PageAction与AjaxActioin在方法的定义上并没有什么差异,只要是个public方法就可以了。
不过,PageAction与AjaxAction不同点在于:
1. Controller的容器名称不同,PageAction要求Controller的名字必须以Controller结尾。
2. 必须有一个有效的[PageUrl]的修饰属性指出可以处理的URL
3. Action的名字与URL无关,可以随意取名。

在MyMVC中,2种Action还有另一特点是:不区分GET,POST 。
原因是:我喜欢用JQuery,用它实现客户端的Ajax时,GET, POST,只是一个参数的差别而已。 另一方面,对于HTML表单来说,GET, POST也只是一个参数的差别,大部分表单也可以通过GET方式来提交,只要您愿意。 所以,我想,既然客户端可以这样灵活地切换,服务端也就没有必要再去做那样限制。 或许有些人认为区分二者会更安全,但我认为它们对安全性基本上不构成影响。 反而,如果服务端忽略它们,只会让客户端更容易调用。

还有一种情况下可能需要区分二者:请求与提交是同一个地址。
这应该可以算得上是我在上篇总结的【以服务端为中心的网站】的开发方式。
事实上,在使用MyMVC的项目中,<form>标签应该需要手写,可能更多的时候会提交到另一个地址,
因为,我更建议使用Ajax方式提交数据。
所以,最终我决定:MyMVC的Action不区分GET, POST.

在设计MyMVC时,我一直没有忘记将View和Controller的分离,而且对于Controller,只有名字上的约束, Action的约束也较少,因此,我们在实现Action时,完全可以把它们独立到【类库项目】中, 
就像示例项目这样:

这样做的好处是:测试Actioin会更容易。
此时网站可能只是一堆aspx,js, css文件。我一直期待能将aspx也交给美工去维护,这样设计但愿能让可能性更大一些。

输出HTML的方式

MyMVC提供二种方式在Action中返回HTML,分别是返回PageResult或者UcResult,表示需要呈现一个页面或者一个用户控件。 当在Action返回这二种结果时,Action的部分就执行完毕了。 剩下的处理是在MyMVC框架中进行的,MyMVC框架会对这二种结果,以IActionResult接口的方式调用Ouput方法输出结果给客户端。
PageResult和UcResult的实现代码如下: 『点击此处展开』

这二个类型的使用方式是一样的,都需要提供二个参数,第一个参数表示页面或者用户控件的存放路径,第二个参数表示给页面或者用户控件所需的显示数据。 比如下面这个示例: 『点击此处展开』

设计这二类结果,我的本意是:
1. UcResult给Ajax请求使用,因为有可能会要求服务端输出一段HTML
2. PageResult用于整页面的响应。

在MyMVC中,执行页面或者用户控件,需要指出页面或者用户控件的路径,而不是采用什么约定关系。
我认为约定会造成名字耦合,约定也会影响限制灵活,因此,必须明确指定(允许为null)。

PageResult多用于PageAction,而PageAction又有[PageUrl]来指示可以处理哪些URL,虽然一个PageAction可以处理多个URL, 但通常情况下,还是以一个PageAction处理一个URL的情况居多。此时,MyMVC允许在返回PageResult时, 第一个参数可以设置为null,表示使用当前请求地址。 如果此时当前请求地址有一个aspx页面与之对应,自然就会方便很多。 可以参考下面的示例: 『点击此处展开』

在MyMVC框架中,PageResult最终会调用PageExecutor.Render()来获取页面的生成代码,具体过程如下: 『点击此处展开』

UcResult则会调用UcExecutor.Render()生成用户控件的输出代码,具体过程如下: 『点击此处展开』

HTML分块输出

注意哦,前面介绍的2个Render方法的可见性都是public,这样设计的想法是让框架提供对外生成HTML的能力,或许有些用户有这样的需求。 另一方面,或许还有些用户打算在Action的执行过程中,将原来较大的HTML页面分块输出给客户端。 BigPipe就使用了这种想法: 整个请求不用等到全部数据获取成功后一次性输出,而是将页面按业务逻辑拆分,并在获取到相应的数据后,立即向客户端输出部分片段。

其实HTML分块输出在ASP.NET中并不是什么新的技术,而是在ASP.NET一出现时就已经存在了, 那就是在输出的过程中不断调用Response.Flush();

由于MyMVC将生成HTML做为一种基础功能,因此在MyMVC中,只要您调用Response.Flush();便可以方便地实现分块输出。 不过,为了让调用更简单,我提供了二个辅助方法来简化这个过程。

在PageExecutor类型中的ResponseWrite方法: 『点击此处展开』

在UcExecutor类型中的ResponseWrite方法: 『点击此处展开』

注意:由于这二个方法在内部使用了HttpContext.Current,因此如果在Action中调用它们,会造成Action不能支持单元测试。

关于单元测试的支持

提到MVC思想,我想就不得不谈单元测试了。
因为MVC的主要思想还是想把这三个字目对应的事物分开,以方便开发与测试。 这里面,我认为尤其是View与Controller的分离最为重要,因为有UI的地方比较难测试, 反过来,如果没有UI的东西就比较容易测试了。

不过,在ASP.NET中,影响单元测试的不仅仅只是UI元素,还有HttpContxt, HttpRequest, HttpResponse这之类的核心对象。 比如:即使我们将Controller放在类库项目中实现,但在Action中还在访问QueryString,Form,甚至发起重定向的请求,你说这样的代码如何测试。

我认为判断一个方法是否可支持单元测试有一个简单的办法:写个控制台的程序去调用它,看它能否正常运行。

通常,用户的输入数据主要有三个来源:QueryString, Form, Cookie。而且前二者居多,Cookie则多用于保存用户偏好设置。 因此,在MyMVC中,可以让Action不再去直接访问QueryString, Form,替代的方式是:将要读取的名字做为C#方法的参数名明确指出。 这样,Actioin中的代码就远离了QueryString, Form。至于Cookie的访问,MyMVC则提供一个辅助类来支持访问: 『点击此处展开』

重定向也是常见的需求。MyMVC则是通过提供RedirectResult来支持的: 『点击此处展开』

说明:Ouput方法由框架调用,不影响Action的单元测试。
示例代码: 『点击此处展开』

在ASP.NET项目开发过程中,还有一类需求较为常见,那就是:访问一些当前环境变量。
MyMVC则是通过以下二个类型来处理的。 『点击此处展开』

注意HttpContextHelper这个类,我将平时访问的一些与请求或者与ASP.NET运行环境相关的属性全部封装在这里了。 如果不够,还可以继续添加。有了这些代码,我就可以简单在控制台程序中调用它们(您也可以移到单元测试项目中): 『点击此处展开』

用过ASP.NET MVC的人可能会问我:
为什么不使用System.Web.Abstractions定义的那些类型,那样不是更容易支持单元测试吗?
是啊,我也知道那种做法的好处。但那样做的工作量也会更大。
根据目前的MyMVC设计方式,如果要引入HttpContextBase, HttpRequestBase, HttpResponseBase这类对象, 就需要设计Controller基类,并且创建Controller的过程也会比目前复杂, 或者要把ASP.NET MVC那套创建Controller的过程搬过来,否则仍然会不完整。 然而,我还是打算自己再设计另一种简单的方法 尽可能 地去解决这个问题。 上面的方法就是我的设计,虽然不够完整,却是简单有效的,而且测试代码也会简单很多。 另一方面,我不提供Controller基类,但可以设计诸如HttpContextHelper.Current这样的访问方式, 最终的结果仍然是可以支持单元测试的,况且HttpContextHelper.Current这种用法也不会让人难以适应。 不提供还有另外的好处:允许设计自己的基类。

还有一点要补充的是:MyMVC框架内部也没有使用System.Web.Abstractions,是的,我知道。
这也只能说:框架的代码不能进行单元测试而已。 但不影响用户的Action代码的单元测试。
再说框架中的代码有些也很难做单元测试,毕竟太依赖于ASP.NET,而且我没那么多的空闲时间以及驱动力。

MyMVC还有一个没有支持的是文件的上传与下载。
这里我来说说对于这块功能访如何去实现:
1. 可以直接访问HttpContext.Current ,并忽略这些代码的单元测试能力。
2. 自行实现我前面没有实现的HttpContextHelper.Current 。
是的,我的确没有完成这个功能,而把它留给了用户,抱歉。

关于框架代码与示例代码

在本文的未尾,我提供了MyMVC框架的代码,以及全部示例代码。

以前我也提供过我的老版本框架的演示示例, 我认为我已经考虑地相当周到了:
1. 没有IIS,没有VS,一样可以运行我的DEMO,因为我把FishAspnetLoader放进去了,调用的BAT文件也准备好了。
2. SQL SERVER如果支持【用户实例】模式,部署会容易。
3. 在数据方面,我不但提供了mdf文件,还提供了sql脚本。
4. 还准备了一些说明文件。 

然而,事实却没有我想像那么好,还是有很多人给我发邮件,问我示例为什么不能运行。
不能运行的环境也是让我完全没有想到的:
1. 有人把它部署到了IIS6,扩展名的映射遇到问题。
2. 有人把它部署到了IIS7,可我没有提供对IIS7的配置!
3. 有人没有安装SQL SERVER。这个只能是没有办法了!
4. 有人不能完成示例程序所需的SQL SERVER配置。
5. 有人用VS2010打开项目并升级了.net版本,遇到一些说不清楚的问题。

吸取前面的教训后,这次我的示例采用XML文件做为数据源,而且增加了IIS7的配置。
不过,有一点我不能替您设置的是XML文件的写入权限。
如果数据不能保存,请检查目录的写入权限,此时程序没有任何提示。

再补充二点:
1. 如果您使用VS2010打开示例项目,请不要选择升级.net版本,不要盲目点击确定。
2. 如果在IIS中部署示例网站遇到问题,那么建议使用VS运行示例网站。

如果您还有配置ASP.NET应用程序的问题,那么请关注我的后续博客。
下篇博客我打算谈一下在部署ASP.NET网站时,IIS6/7 以及SQL SERVER中必须知道的一些设置。

点击此处下载示例代码

原文地址:https://www.cnblogs.com/Leo_wl/p/2363219.html