写自己的ASP.NET MVC框架(下) MyMVC的特点 介绍示例项目 关于URL路由 配置MyMVC框架 映射处理器(入口) 内部初始化 从URL到Action的映射过程 PageUrl的设计思想 多URL的匹配功能 解决老的URL兼容问题 对身份认证的支持 View的设计方式 Controller,Action的设计方式 输出HTML的方式 HTML分块输出 关于单元测试的支持 关于框架代码与示例代码

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

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

在开发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中,一个页面请求的执行过程:

写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

介绍示例项目

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

风格1

写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

View对应的代码如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

风格2

写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

View对应的代码如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

风格3

写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

View对应的代码如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

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

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

Inherits="MyPageView<CustomersPageModel>"

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

写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

虽然前面我们可以从图片中看到访问【同一个URL地址】出现【三个不同的页面】,但它们背后的Controller却是同一个: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

通过上面代码可以看到我用了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,则参考以下配置: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

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

映射处理器(入口)

在web.config中注册MvcPageHandlerFactory后,所有符合条件的请求将会进入MvcPageHandlerFactory。
我们来看一下MvcPageHandlerFactory的实现代码: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

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

内部初始化

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

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

ReflectionHelper有个静态构造函数,虽然上次我已贴出它的代码,但那只是部分代码,以下才是完整的初始化代码: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

从URL到Action的映射过程

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

GetPageActionInvokeInfo方法的实现代码如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

在介绍这个映射过程之前,让我们再来回顾一下Action的声明代码: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

虽然这种URL虽然没有扩展名,但我仍然可以通过配置httpHandler的方式来解决,下面的配置就是我们需要的: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

在介绍MvcPageHandlerFactory时,MyMVC提供了另一个方法TryGetHandler供外部使用。 因此,在示例网站中,我还可以在Global.asax中调用这个方法来解决前面的那个问题: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

对于切换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会更符合常理: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

是的,通常情况下,一个Action处理一个URL也是较为常见。
但仍然有二种情况需要这个功能。首先来看下面的示例: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

理解上面那句话,可能还需要知道StyleHelper的实现代码: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

用户设置风格的JS代码如下, 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

服务端的C#代码如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

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

解决老的URL兼容问题

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

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

在MyMVC中,可以简单地处理这个问题,就像下面的这个示例一样: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

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

对身份认证的支持

MyMVC也支持一些基本的身份认证,可以通过在Action方法中添加[Authorize]修饰属性来指示。
AuthorizeAttribute的实现代码如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

认证检查发生在调用Action之前,代码如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

下面的示例代码演示了它的用法: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

注意:
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的代码: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

此时,对于呈现所需的数据可以直接从Model对象中获取,但要求在Page指令中指出Model的类型,这样还可以有智能提示的优点。 如果页面需要显示数据,请务必从MyPageView<>继承,它的实现代码如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

再来一段用户控件的代码: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

基本上,与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时,完全可以把它们独立到【类库项目】中, 
就像示例项目这样:

写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

输出HTML的方式

MyMVC提供二种方式在Action中返回HTML,分别是返回PageResult或者UcResult,表示需要呈现一个页面或者一个用户控件。 当在Action返回这二种结果时,Action的部分就执行完毕了。 剩下的处理是在MyMVC框架中进行的,MyMVC框架会对这二种结果,以IActionResult接口的方式调用Ouput方法输出结果给客户端。
PageResult和UcResult的实现代码如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

这二个类型的使用方式是一样的,都需要提供二个参数,第一个参数表示页面或者用户控件的存放路径,第二个参数表示给页面或者用户控件所需的显示数据。 比如下面这个示例: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

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

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

PageResult多用于PageAction,而PageAction又有[PageUrl]来指示可以处理哪些URL,虽然一个PageAction可以处理多个URL, 但通常情况下,还是以一个PageAction处理一个URL的情况居多。此时,MyMVC允许在返回PageResult时, 第一个参数可以设置为null,表示使用当前请求地址。 如果此时当前请求地址有一个aspx页面与之对应,自然就会方便很多。 可以参考下面的示例: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

在MyMVC框架中,PageResult最终会调用PageExecutor.Render()来获取页面的生成代码,具体过程如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

UcResult则会调用UcExecutor.Render()生成用户控件的输出代码,具体过程如下: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

HTML分块输出

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

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

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

在PageExecutor类型中的ResponseWrite方法: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

在UcExecutor类型中的ResponseWrite方法: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

注意:由于这二个方法在内部使用了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则提供一个辅助类来支持访问: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

重定向也是常见的需求。MyMVC则是通过提供RedirectResult来支持的: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

说明:Ouput方法由框架调用,不影响Action的单元测试。
示例代码: 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

在ASP.NET项目开发过程中,还有一类需求较为常见,那就是:访问一些当前环境变量。
MyMVC则是通过以下二个类型来处理的。 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

注意HttpContextHelper这个类,我将平时访问的一些与请求或者与ASP.NET运行环境相关的属性全部封装在这里了。 如果不够,还可以继续添加。有了这些代码,我就可以简单在控制台程序中调用它们(您也可以移到单元测试项目中): 写自己的ASP.NET MVC框架(下)
MyMVC的特点
介绍示例项目
关于URL路由
配置MyMVC框架
映射处理器(入口)
内部初始化
从URL到Action的映射过程
PageUrl的设计思想
多URL的匹配功能
解决老的URL兼容问题
对身份认证的支持
View的设计方式
Controller,Action的设计方式
输出HTML的方式
HTML分块输出
关于单元测试的支持
关于框架代码与示例代码

用过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中必须知道的一些设置。

转自:http://www.cnblogs.com/fish-li/archive/2012/02/21/2361982.html