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

上篇博客【写自己的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对应的代码如下:

<%@ Page Title="客户管理" Language="C#" MasterPageFile="MasterPage.master" Inherits="MyPageView<CustomersPageModel>" %><asp:Content ID="Content1" ContentPlaceHolderID="head" Runat="Server">
<%= HtmlExtension.RefJsFileHtml("/js/MyPage/Customers.js")%>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server"><p><a id="btnCreateItem" href="#" class="easyui-linkbutton" iconCls="icon-add">创建客户</a></p><table class="GridView" cellspacing="0" cellpadding="4" border="0" style="border-collapse:collapse;"><tr align="left"><th style="width:20px;">&nbsp;</th><th style="width:260px;">客户名称</th><th style="width:80px;">联系人</th><th>地址</th><th style="width:80px;">邮编</th><th style="width:160px;">电话</th></tr>
<% foreach( Customer customer in Model.List ) { %>
    <tr><td><a href="/AjaxCustomer/Delete.cspx?id=<%= customer.CustomerID %>&returnUrl=<%= RequestUrlEncodeRawUrl %>" title="删除" class="easyui-linkbutton" plain="true"><img src="/Images/delete.gif" alt="删除" /></a>                </td><td><a href="#" class="easyui-linkbutton" rowId="<%= customer.CustomerID %>" plain="true" iconCls="icon-open"><%= customer.CustomerName.HtmlEncode()%></a></td><td><span name="ContactName"><%= customer.ContactName.HtmlEncode() %></span></td><td><span name="Address"><%= customer.Address.HtmlEncode() %></span></td><td><span name="PostalCode"><%= customer.PostalCode.HtmlEncode() %></span></td><td><span name="Tel"><%= customer.Tel.HtmlEncode() %></span></td></tr>
<% } %><%= Model.PagingInfo.PaginationBar(6)%>
</table><div id="divCustomerInfo" title="客户" style="padding: 8px; display: none">
<%= UcExecutor.Render("/Controls/Style1/CustomerInfo.ascx", Model.Customer)%>
</div></asp:Content>

风格2

View对应的代码如下:

<%@ Page Title="客户管理" Language="C#" MasterPageFile="MasterPage.master" %><asp:Content ID="Content1" ContentPlaceHolderID="head" Runat="Server">
<%= HtmlExtension.RefJsFileHtml("/js/MyPage2/Customers.js")%>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server"><table id="grid1"></table><div id="divCustomerInfo" title="客户" style="padding: 8px; display: none">
<%= UcExecutor.Render("/Controls/Style2/CustomerInfo.ascx", null)%>
</div></asp:Content>

风格3

View对应的代码如下:

<%@ Page Title="客户管理" Language="C#" MasterPageFile="MasterPage.master" Inherits="MyPageView<CustomersPageModel>" %><asp:Content ID="Content1" ContentPlaceHolderID="head" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server"><ul class="itemList">
<% foreach( Customer customer in Model.List ) { %>
<li><table class="GridView" cellspacing="0" cellpadding="4" border="0" style="border-collapse:collapse;"><tr><td><%= customer.CustomerName.HtmlEncode()%></td></tr><tr><td><%= customer.ContactName.HtmlEncode() %></td></tr><tr><td><%= customer.Address.HtmlEncode() %></td></tr><tr><td><%= customer.PostalCode.HtmlEncode() %></td></tr><tr><td><%= customer.Tel.HtmlEncode() %></td></tr></table>
</li>
<% } %>
</ul><%= Model.PagingInfo.PaginationBar()%></asp:Content>

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

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

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

Inherits="MyPageView<CustomersPageModel>"

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

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

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

public class CustomerController
{[Action][PageUrl(Url = "/mvc/Customers")][PageUrl(Url = "/mvc/Customers.html")][PageUrl(Url = "/mvc/CustomerList.aspx")][PageUrl(Url = "/Pages/Customers.aspx")]public static object LoadModel(int? page){// 说明:参数page表示分页数,方法名LoadModel其实可以【随便取】。// 根据用户选择的界面风格,计算实现要呈现的页面路径。string papeUrl = StyleHelper.GetTargetPageUrl("Customers.aspx");if( StyleHelper.PageStyle == StyleHelper.StyleArray[1] )// Style2 风格下,页面不需要绑定数据。数据由JS通过AJAX方式获取return new PageResult(papeUrl, null);// 为Style1 风格获取数据。CustomerSearchInfo info = new CustomerSearchInfo();info.SearchWord = string.Empty;info.PageIndex = page.HasValue ? page.Value - 1 : 0;info.PageSize = AppHelper.DefaultPageSize;CustomersPageModel result = new CustomersPageModel();result.PagingInfo = info;result.List = BllFactory.GetCustomerBLL().GetList(info);return new PageResult(papeUrl, result);}
}

通过上面代码可以看到我用了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,则参考以下配置: <a class="nopadding" href="javascript:void(0);" codeid="pre-MvcPageHandlerFactory-iis7" "="">

<system.webServer><handlers><add name="MvcPageHandlerFactory" verb="*" path="*.aspx" type="MyMVC.MvcPageHandlerFactory, MyMVC" preCondition="integratedMode"/></handlers>
</system.webServer>

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

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

回到顶部

映射处理器(入口)

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

internal sealed class AspnetPageHandlerFactory : PageHandlerFactory { }public sealed class MvcPageHandlerFactory : IHttpHandlerFactory
{/// <summary>/// 尝试根据当前请求,获取一个有效的Action,并返回ActionHandler/// 此方法可以在HttpModule中使用,用于替代httpHandler的映射配置/// </summary>/// <param name="context"></param>/// <returns></returns>public static IHttpHandler TryGetHandler(HttpContext context){InvokeInfo vkInfo = ReflectionHelper.GetPageActionInvokeInfo(context.Request.FilePath);if( vkInfo == null ) return null;return ActionHandler.CreateHandler(vkInfo);}private AspnetPageHandlerFactory _msPageHandlerFactory;IHttpHandler IHttpHandlerFactory.GetHandler(HttpContext context, string requestType, string virtualPath, string physicalPath){// 尝试根据请求路径获取ActionInvokeInfo vkInfo = ReflectionHelper.GetPageActionInvokeInfo(virtualPath);// 如果没有找到合适的Action,并且请求的是一个ASPX页面,则按ASP.NET默认的方式来继续处理if( vkInfo == null && virtualPath.EndsWith(".aspx", StringComparison.OrdinalIgnoreCase) ) {if( _msPageHandlerFactory == null )_msPageHandlerFactory = new AspnetPageHandlerFactory();// 调用ASP.NET默认的Page处理器工厂来处理return _msPageHandlerFactory.GetHandler(context, requestType, virtualPath, physicalPath);}return ActionHandler.CreateHandler(vkInfo);}void IHttpHandlerFactory.ReleaseHandler(IHttpHandler handler){            }
}

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

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

回到顶部

内部初始化

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

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

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

internal static class ReflectionHelper
{// 保存PageAction的字典private static Dictionary<string, ActionDescription> s_PageActionDict;// 保存AjaxController的列表private static List<ControllerDescription> s_AjaxControllerList;// 保存AjaxAction的字典private static Hashtable s_AjaxActionTable = Hashtable.Synchronized(new Hashtable(4096, StringComparer.OrdinalIgnoreCase));// 用于从类型查找Action的反射标记private static readonly BindingFlags ActionBindingFlags =BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase;static ReflectionHelper(){InitControllers();}/// <summary>/// 加载所有的Controller/// </summary>private static void InitControllers(){s_AjaxControllerList = new List<ControllerDescription>(1024);var pageControllerList = new List<ControllerDescription>(1024);ICollection assemblies = BuildManager.GetReferencedAssemblies();foreach( Assembly assembly in assemblies ) {// 过滤以【System.】开头的程序集,加快速度if( assembly.FullName.StartsWith("System.", StringComparison.OrdinalIgnoreCase) )continue;try {foreach( Type t in assembly.GetExportedTypes() ) {if( t.Name.StartsWith("Ajax") )s_AjaxControllerList.Add(new ControllerDescription(t));else if( t.Name.EndsWith("Controller") )pageControllerList.Add(new ControllerDescription(t));}}catch { }}// 提前加载Page Controller中的所有Action方法s_PageActionDict = new Dictionary<string, ActionDescription>(4096, StringComparer.OrdinalIgnoreCase);foreach( ControllerDescription controller in pageControllerList ) {foreach( MethodInfo m in controller.ControllerType.GetMethods(ActionBindingFlags) ) {PageUrlAttribute[] pageUrlAttrs = m.GetMyAttributes<PageUrlAttribute>();ActionAttribute actionAttr = m.GetMyAttribute<ActionAttribute>();if( pageUrlAttrs.Length > 0 && actionAttr != null ) {ActionDescription actionDescription =new ActionDescription(m, actionAttr) { PageController = controller };foreach( PageUrlAttribute attr in pageUrlAttrs ) {if( string.IsNullOrEmpty(attr.Url) == false )s_PageActionDict.Add(attr.Url, actionDescription);}}}}// 用于Ajax调用的Action信息则采用延迟加载的方式。}

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

回到顶部

从URL到Action的映射过程

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

GetPageActionInvokeInfo方法的实现代码如下:

/// <summary>
/// 根据一个页面请求路径,返回内部表示的调用信息。
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static InvokeInfo GetPageActionInvokeInfo(string url)
{if( string.IsNullOrEmpty(url) )throw new ArgumentNullException("url");ActionDescription action = null;if( s_PageActionDict.TryGetValue(url, out action) == false )return null;InvokeInfo vkInfo = new InvokeInfo();vkInfo.Controller = action.PageController;vkInfo.Action = action;if( vkInfo.Action.MethodInfo.IsStatic == false )vkInfo.Instance = Activator.CreateInstance(vkInfo.Controller.ControllerType);return vkInfo;
}

在介绍这个映射过程之前,让我们再来回顾一下Action的声明代码:

public class CustomerController
{[Action][PageUrl(Url = "/mvc/Customers")][PageUrl(Url = "/mvc/Customers.html")][PageUrl(Url = "/mvc/CustomerList.aspx")][PageUrl(Url = "/Pages/Customers.aspx")]public static object LoadModel(int? page){

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

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

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

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

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

protected void Application_PostResolveRequestCache(object sender, EventArgs e)
{// 这里只是一个演示。// 主要是将诸如:/mvc/Customers 这类请求映射到MyMVC框架的处理器HttpApplication app = (HttpApplication)sender;if( app.Request.FilePath.StartsWith("/mvc/") ) {IHttpHandler myHandler = MyMVC.MvcPageHandlerFactory.TryGetHandler(app.Context);if( myHandler != null )app.Context.RemapHandler(myHandler);}
}

对于切换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会更符合常理:

public class CategoryController
{[Action][PageUrl(Url = "/Pages/Categories.aspx")]public object LoadModel(){

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

[Action]
[PageUrl(Url = "/Pages/AddOrder.aspx")]
[PageUrl(Url = "/Pages/CodeExplorer.aspx")]
[PageUrl(Url = "/Pages/Default.aspx")]
[PageUrl(Url = "/Pages/Orders.aspx")]
public object TransferRequest()
{// 这个Action要做的事较为简单,// 将请求 "/Pages/Orders.aspx" 用实际的页面 "/Pages/StyleX/Orders.aspx" 来响应。// 因为用户选择的风格不同,但URL地址是一样的,所以在这里切换。// 当然这样的处理也只适合页面不需要Model的情况下。string filePath = HttpContextHelper.RequestFilePath;int p = filePath.LastIndexOf('/');string pageName = filePath.Substring(p + 1);return new PageResult(StyleHelper.GetTargetPageUrl(pageName), null /*model*/);
}

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

理解上面那句话,可能还需要知道StyleHelper的实现代码:

public static class StyleHelper
{public static readonly string STR_PageStyle = "PageStyle";public static readonly string[] StyleArray = new string[] { "Style1", "Style2", "Style3" };public static string PageStyle{get { return CookieHelper.GetCookieValue(STR_PageStyle) ?? StyleArray[1]; }}public static string GetTargetPageUrl(string pageName){return string.Format("/Pages/{0}/" + pageName, PageStyle);}}

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

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

用户设置风格的JS代码如下,

$(function(){$("a.btnSetStyle").click(function(){var style = $(this).attr("ps");// 其实写cookie也可以直接使用JS去写的。$.ajax({url: "/AjaxStyle/SetStyle.cspx",data: {style: style},success: function(){window.location = window.location;}});return false;});
});

服务端的C#代码如下:

public class AjaxStyle
{[Action]public void SetStyle(string style){if( Array.IndexOf(StyleHelper.StyleArray, style) >= 0 ) {HttpCookie cookie = new HttpCookie(StyleHelper.STR_PageStyle, style);cookie.Expires = DateTime.Now.AddYears(1);CookieHelper.AddCookie(cookie);}}
}

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

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

回到顶部

解决老的URL兼容问题

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

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

在MyMVC中,可以简单地处理这个问题,就像下面的这个示例一样:

public class CustomerController
{[Action][PageUrl(Url = "/mvc/Customers")][PageUrl(Url = "/mvc/Customers.html")][PageUrl(Url = "/mvc/CustomerList.aspx")][PageUrl(Url = "/Pages/Customers.aspx")]public static object LoadModel(int? page){

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

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

回到顶部

对身份认证的支持

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

/// <summary>/// 用于验证用户身份的修饰属性/// </summary>[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]public class AuthorizeAttribute : Attribute{private string _user;private string[] _users;private string _role;private string[] _roles;private string[] SplitString(string value){if( string.IsNullOrEmpty(value) )return null;elsereturn (from s in value.Split(',')let u = s.Trim()where u.Length > 0select u).ToArray();}/// <summary>/// 允许访问的用户列表,用逗号分隔。/// </summary>public string Users{get { return _user; }set { _user = value; _users = SplitString(value); }}/// <summary>/// 允许访问的角色列表,用逗号分隔。/// </summary>public string Roles{get { return _role; }set { _role = value; _roles = SplitString(value); }}internal bool AuthenticateRequest(HttpContext context){if( context.Request.IsAuthenticated == false )return false;if( _users != null && _users.Contains(context.User.Identity.Name, StringComparer.OrdinalIgnoreCase) == false )return false;if( _roles != null && _roles.Any(context.User.IsInRole) == false )return false;return true;}}

认证检查发生在调用Action之前,代码如下:

internal static void ExecuteAction(HttpContext context, InvokeInfo vkInfo)
{if( context == null )throw new ArgumentNullException("context");if( vkInfo == null )throw new ArgumentNullException("vkInfo");// 验证请求是否允许访问(身份验证)AuthorizeAttribute authorize = vkInfo.GetAuthorize();if( authorize != null ) {if( authorize.AuthenticateRequest(context) == false )ExceptionHelper.Throw403Exception(context);}// 调用方法object result = ExecuteActionInternal(context, vkInfo);// 设置OutputCacheOutputCacheAttribute outputCache = vkInfo.GetOutputCacheSetting();if( outputCache != null )outputCache.SetResponseCache(context);// 处理方法的返回结果IActionResult executeResult = result as IActionResult;if( executeResult != null ) {executeResult.Ouput(context);}else {if( result != null ) {// 普通类型结果context.Response.ContentType = "text/plain";context.Response.Write(result.ToString());}}
}

下面的示例代码演示了它的用法:

[Action]
[Authorize(Users="fish")]
[PageUrl(Url = "/Pages/Demo/TestAuthorize/Fish.aspx")]
public object ShowFishPage()
{// 仅当当前用户是 fish 时,才允许访问这个PageAction// 注意:第一参数为null,表示使用当前地址。return new PageResult(null, null);
}[Action]
[Authorize]
[PageUrl(Url = "/Pages/Demo/TestAuthorize/LoginUser.aspx")]
public object ShowLoginUserPage()
{// 仅当当前用户是已登录用户时,才允许访问这个PageAction// 注意:第一参数为null,表示使用当前地址。return new PageResult(null, null);
}

注意:
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的代码:

<%@ Page Title="客户管理" Language="C#" MasterPageFile="MasterPage.master" Inherits="MyPageView<CustomersPageModel>" %><asp:Content ID="Content1" ContentPlaceHolderID="head" Runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server"><ul class="itemList">
<% foreach( Customer customer in Model.List ) { %>
<li><table class="GridView" cellspacing="0" cellpadding="4" border="0" style="border-collapse:collapse;"><tr><td><%= customer.CustomerName.HtmlEncode()%></td></tr><tr><td><%= customer.ContactName.HtmlEncode() %></td></tr><tr><td><%= customer.Address.HtmlEncode() %></td></tr><tr><td><%= customer.PostalCode.HtmlEncode() %></td></tr><tr><td><%= customer.Tel.HtmlEncode() %></td></tr></table>
</li>
<% } %>
</ul><%= Model.PagingInfo.PaginationBar()%></asp:Content>

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

/// <summary>
/// 页面视图的基类
/// </summary>
/// <typeparam name="TModel">传递给页面呈现时所需的数据实体对象类型</typeparam>
public class MyPageView<TModel> : MyBasePage
{/// <summary>/// 用于页面呈现时所需的数据实体对象/// </summary>public TModel Model { get; set; }}

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

再来一段用户控件的代码:

<%@ Control Language="C#" Inherits="MyUserControlView<Customer>" %><table cellpadding="4" border="0px">
<tr><td style="width: 80px">客户名称</td><td><input name="CustomerName" type="text" maxlength="50" id="txtCustomerName" class="myTextbox w400" value="<%= Model.CustomerName.HtmlEncode() %>" /></td></tr>
<tr><td>联系人</td><td><input name="ContactName" type="text" maxlength="50" id="txtContactName" class="myTextbox w400"    value="<%= Model.ContactName.HtmlEncode() %>" /></td></tr>
<tr><td>地址</td><td><input name="Address" type="text" maxlength="50" id="txtAddress" class="myTextbox w400"    value="<%= Model.Address.HtmlEncode() %>" /></td></tr><tr><td>邮编</td><td><input name="PostalCode" type="text" maxlength="10" id="txtPostalCode" class="myTextbox w400" value="<%= Model.PostalCode.HtmlEncode() %>" /></td></tr>
<tr><td>电话</td><td><input name="Tel" type="text" maxlength="50" id="txtTel" class="myTextbox w400"    value="<%= Model.Tel.HtmlEncode() %>" /></td></tr>
</table>

基本上,与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的实现代码如下:

public interface IActionResult
{void Ouput(HttpContext context);
}/// <summary>
/// 表示一个用户控件结果(用户控件将由框架执行)
/// </summary>
public sealed class UcResult : IActionResult
{public string VirtualPath { get; private set; }public object Model { get; private set; }public UcResult(string virtualPath, object model){if( string.IsNullOrEmpty(virtualPath) )throw new ArgumentNullException("virtualPath");this.VirtualPath = virtualPath;this.Model = model;}void IActionResult.Ouput(HttpContext context){context.Response.ContentType = "text/html";string html = UcExecutor.Render(VirtualPath, Model);context.Response.Write(html);}}/// <summary>
/// 表示一个页面结果(页面将由框架执行)
/// </summary>
public sealed class PageResult : IActionResult
{public string VirtualPath { get; private set; }public object Model { get; private set; }public PageResult(string virtualPath, object model){this.VirtualPath = virtualPath;this.Model = model;}void IActionResult.Ouput(HttpContext context){if( string.IsNullOrEmpty(this.VirtualPath) )this.VirtualPath = context.Request.FilePath;context.Response.ContentType = "text/html";string html = PageExecutor.Render(context, VirtualPath, Model);context.Response.Write(html);}
}

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

[Action]
public object ShowCustomerPicker(string searchWord, int? page)
{CustomerSearchInfo info = new CustomerSearchInfo();info.SearchWord = searchWord ?? string.Empty;info.PageIndex = page.HasValue ? page.Value - 1 : 0;info.PageSize = AppHelper.DefaultPageSize;CustomerPickerModel data = new CustomerPickerModel();data.SearchInfo = info;data.List = BllFactory.GetCustomerBLL().GetList(info);return new UcResult("/Controls/Style1/CustomerPicker.ascx", data);
}

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

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

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

[Action]
[Authorize(Users="fish")]
[PageUrl(Url = "/Pages/Demo/TestAuthorize/Fish.aspx")]
public object ShowFishPage()
{// 仅当当前用户是 fish 时,才允许访问这个PageAction// 注意:第一参数为null,表示使用当前地址。return new PageResult(null, null);
}

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

public static class PageExecutor
{internal static readonly Type MyPageViewOpenType = typeof(MyPageView<>);private static void SetPageModel(IHttpHandler handler, object model){if( handler == null )return;if( model != null ) {Type handlerType = handler.GetType().BaseType;Type viewType = MyPageViewOpenType.MakeGenericType(model.GetType());// 检查类型是否匹配if( handlerType == viewType || handlerType.IsSubclassOf(viewType) ) {DataMember member = ReflectionHelper.GetMemberByName(viewType, "Model", true);member.SetValue(handler, model);}elsethrow new ArgumentException("参数model的类型与页面的参数类型不一致。");}}/// <summary>/// 用指定的页面路径以及视图数据呈现结果,最后返回生成的HTML代码。/// 页面应从MyPageView&lt;T&gt;继承/// </summary>/// <param name="context">HttpContext对象</param>/// <param name="pageVirtualPath">Page的虚拟路径</param>/// <param name="model">视图数据</param>/// <returns>生成的HTML代码</returns>public static string Render(HttpContext context, string pageVirtualPath, object model){if( context == null )throw new ArgumentNullException("context");if( string.IsNullOrEmpty(pageVirtualPath) )throw new ArgumentNullException("pageVirtualPath");Page handler = BuildManager.CreateInstanceFromVirtualPath(pageVirtualPath, typeof(object)) as Page;if( handler == null )throw new InvalidOperationException(string.Format("指定的路径 {0} 不是一个有效的页面。", pageVirtualPath));SetPageModel(handler, model);StringWriter output = new StringWriter();context.Server.Execute(handler, output, false);return output.ToString();}

UcResult则会调用UcExecutor.Render()生成用户控件的输出代码,具体过程如下:

public static class UcExecutor
{internal static readonly Type MyUserControlViewOpenType = typeof(MyUserControlView<>);/// <summary>/// 用指定的用户控件以及视图数据呈现结果,最后返回生成的HTML代码。/// 用户控件应从MyUserControlView&lt;T&gt;继承/// </summary>/// <param name="ucVirtualPath">用户控件的虚拟路径</param>/// <param name="model">视图数据</param>/// <returns>生成的HTML代码</returns>public static string Render(string ucVirtualPath, object model){if( string.IsNullOrEmpty(ucVirtualPath) )throw new ArgumentNullException("ucVirtualPath");Page page = new Page();Control ctl = page.LoadControl(ucVirtualPath);if( ctl == null )throw new InvalidOperationException(string.Format("指定的用户控件 {0} 没有找到。", ucVirtualPath));MyBaseUserControl myctl = ctl as MyBaseUserControl;if( myctl != null ) {if( model != null ) {Type viewType = MyUserControlViewOpenType.MakeGenericType(model.GetType());Type ctrlType = myctl.GetType();// 检查用户控件的类型是否匹配if( ctrlType == viewType || ctrlType.IsSubclassOf(viewType) ) {// 给用户控件的视图数据赋值。DataMember member = ReflectionHelper.GetMemberByName(viewType, "Model", true);member.SetValue(myctl, model);}elsethrow new ArgumentException("参数model的类型与用户控件的参数类型不一致。");}}// 将用户控件放在Page容器中。page.Controls.Add(ctl);StringWriter output = new StringWriter();HtmlTextWriter write = new HtmlTextWriter(output, string.Empty);page.RenderControl(write);// 用下面的方法也可以的。//HttpContext.Current.Server.Execute(page, output, false);return output.ToString();}
回到顶部

HTML分块输出

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

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

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

在PageExecutor类型中的ResponseWrite方法:

/// <summary>
/// 用指定的Page以及视图数据呈现结果,
/// 然后将产生的HTML代码写入HttpContext.Current.Response
/// 用户控件应从MyPageView&lt;T&gt;继承
/// </summary>
/// <param name="pageVirtualPath">Page的虚拟路径</param>
/// <param name="model">视图数据</param>
/// <param name="flush">是否需要在输出html后调用Response.Flush()</param>
public static void ResponseWrite(string pageVirtualPath, object model, bool flush)
{HttpContext context = HttpContext.Current;if( context == null )return;if( string.IsNullOrEmpty(context.Response.ContentType) )context.Response.ContentType = "text/html";string html = Render(context, pageVirtualPath, model);context.Response.Write(html);if( flush )context.Response.Flush();
}

在UcExecutor类型中的ResponseWrite方法:

/// <summary>
/// 用指定的用户控件以及视图数据呈现结果,
/// 然后将产生的HTML代码写入HttpContext.Current.Response
/// 用户控件应从MyUserControlView&lt;T&gt;继承
/// </summary>
/// <param name="ucVirtualPath">用户控件的虚拟路径</param>
/// <param name="model">视图数据</param>
/// <param name="flush">是否需要在输出html后调用Response.Flush()</param>
public static void ResponseWrite(string ucVirtualPath, object model, bool flush)
{HttpContext context = HttpContext.Current;if( context == null )return;if( string.IsNullOrEmpty(context.Response.ContentType) )context.Response.ContentType = "text/html";string html = Render(ucVirtualPath, model);context.Response.Write(html);if( flush )context.Response.Flush();
}

注意:由于这二个方法在内部使用了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则提供一个辅助类来支持访问:

/// <summary>
/// 读写Cookie的辅助工具类。这个类对测试环境仍然有效。
/// </summary>
public static class CookieHelper
{[ThreadStatic]private static HttpCookieCollection s_cookies;private static HttpCookieCollection TestCookies{get{if( TestEnvironment.IsTestEnvironment == false )throw new InvalidOperationException();if( s_cookies == null )    // 测试环境不考虑线程安全问题。s_cookies = new HttpCookieCollection();return s_cookies;}}/// <summary>/// 从Request中获取一个Cookie对象/// </summary>/// <param name="key"></param>/// <returns></returns>public static HttpCookie GetCookie(string key){if( TestEnvironment.IsTestEnvironment ) return TestCookies[key];return HttpContext.Current.Request.Cookies[key];}/// <summary>/// 从Request中获取一个Cookie对象的值,如果不存在,则返回null/// </summary>/// <param name="key"></param>/// <returns></returns>public static string GetCookieValue(string key){HttpCookie cookie = GetCookie(key);if( cookie == null )return null;elsereturn cookie.Value;}/// <summary>/// 将一个Cookie对应写入到Response中/// </summary>/// <param name="cookie"></param>public static void SetCookie(HttpCookie cookie){if( TestEnvironment.IsTestEnvironment ) TestCookies.Set(cookie);            elseHttpContext.Current.Response.Cookies.Set(cookie);}/// <summary>/// 将一个Cookie对应写入到Response中/// </summary>/// <param name="cookie"></param>public static void AddCookie(HttpCookie cookie){if( TestEnvironment.IsTestEnvironment ) TestCookies.Add(cookie);elseHttpContext.Current.Response.Cookies.Add(cookie);}/// <summary>/// 清除所有写入的Cookie /// </summary>public static void ClearCookie(){if( TestEnvironment.IsTestEnvironment )TestCookies.Clear();elseHttpContext.Current.Response.Cookies.Clear();}/// <summary>/// 删除指定名称的Cookie/// </summary>/// <param name="key"></param>public static void RemoveCookie(string key){if( TestEnvironment.IsTestEnvironment ) TestCookies.Remove(key);elseHttpContext.Current.Response.Cookies.Remove(key);}}

重定向也是常见的需求。MyMVC则是通过提供RedirectResult来支持的:

/// <summary>
/// 表示一个重定向的结果
/// </summary>
public sealed class RedirectResult : IActionResult
{public string Url { get; private set; }public RedirectResult(string url){if( string.IsNullOrEmpty(url) )throw new ArgumentNullException("url");Url = url;}void IActionResult.Ouput(HttpContext context){context.Response.Redirect(Url, true);}
}

说明:Ouput方法由框架调用,不影响Action的单元测试。
示例代码:

[Action]
public object Delete(int id)
{BllFactory.GetCategoryBLL().Delete(id);return new RedirectResult("/Pages/Categories.aspx");
}

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

public static class TestEnvironment
{/// <summary>/// 当前运行环境是否为测试环境(非ASP.NET环境)/// </summary>internal static readonly bool IsTestEnvironment = (HttpRuntime.AppDomainAppId == null);[ThreadStatic]private static Hashtable s_ContextInfo;private static Hashtable ContextInfo{get{if( TestEnvironment.IsTestEnvironment == false )throw new InvalidOperationException("只有测试代码才允许执行这个调用。");if( s_ContextInfo == null )    // 测试环境不考虑线程安全问题。s_ContextInfo = new Hashtable(100, StringComparer.Ordinal);return s_ContextInfo;}}public static object GetValue(string key){object val = ContextInfo[key];if( val == null )throw new InvalidOperationException("您忘记了给测试环境赋值了。参数名:" + key);return val;}public static void SetValue(string key, object val){ContextInfo[key] = val;}public static void ClearTestEnvironmentInfo(){ContextInfo.Clear();CookieHelper.ClearCookie();}
}/// <summary>
/// 用于访问当前请求上下文的工具类。这个类对测试环境仍然有效。
/// </summary>
public static class HttpContextHelper
{// 为了能让代码支持测试环境// 会判断 TestEnvironment.IsTestEnvironment// 如果在ASP.NET环境中运行,则直接返回HttpRuntime或者HttpContext.Current中的参数值// 如果是在测试环境中运行,则使用另一个静态变量来维护所有需要访问的状态。// 在测试前,需要先给相应的参数赋值。// 测试完成后可调用TestEnvironment.ClearTestEnvironmentInfo()清除临时环境。/// <summary>/// return HttpRuntime.AppDomainAppPath;/// </summary>public static string AppRootPath{get{if( TestEnvironment.IsTestEnvironment )return TestEnvironment.GetValue("AppDomainAppPath") as string;return HttpRuntime.AppDomainAppPath;}set{TestEnvironment.SetValue("AppDomainAppPath", value);}}/// <summary>/// return HttpContext.Current.Request.FilePath;/// </summary>public static string RequestFilePath{get{if( TestEnvironment.IsTestEnvironment )return TestEnvironment.GetValue("RequestFilePath") as string;return HttpContext.Current.Request.FilePath;}set{TestEnvironment.SetValue("RequestFilePath", value);}}/// <summary>/// return HttpContext.Current.Request.RawUrl;/// </summary>public static string RequestRawUrl{get{if( TestEnvironment.IsTestEnvironment )return TestEnvironment.GetValue("RequestRawUrl") as string;return HttpContext.Current.Request.RawUrl;}set{TestEnvironment.SetValue("RequestRawUrl", value);}}/// <summary>/// return HttpContext.Current.User.Identity.Name;/// </summary>public static string UserIdentityName{get{if( TestEnvironment.IsTestEnvironment )return TestEnvironment.GetValue("UserIdentityName") as string;if( HttpContext.Current.Request.IsAuthenticated == false )return null;return HttpContext.Current.User.Identity.Name;}set{TestEnvironment.SetValue("UserIdentityName", value);}}// 如果还需要访问更多的HttpContext信息,也可以采用下面的方法。请自行完成。//public static HttpContextBase Current//{//    get { }//    set { }//}
}

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

class Program
{static void Main(string[] args){InitDB();Test_AjaxStyle_SetStyle();Test_AjaxCustomer_GetById();Test_TestEnvironment();Console.ReadLine();}static void InitDB(){string xmlPath = "..\\..\\DemoWebSite1\\App_Data\\MyNorthwindDataBase.xml";WebSiteDB.LoadDbFromXml(xmlPath);}static void Test_AjaxStyle_SetStyle(){AjaxStyle ajax = new AjaxStyle();ajax.SetStyle(StyleHelper.StyleArray[0]);if( CookieHelper.GetCookieValue(StyleHelper.STR_PageStyle) == StyleHelper.StyleArray[0] )Console.WriteLine("AjaxStyle.SetStyle(\"{0}\") OK", StyleHelper.StyleArray[0]);elseConsole.WriteLine("AjaxStyle.SetStyle(\"{0}\") faild.", StyleHelper.StyleArray[0]);CookieHelper.ClearCookie();ajax.SetStyle("abc");        // 一个无效的值。if( CookieHelper.GetCookieValue(StyleHelper.STR_PageStyle) == null  )Console.WriteLine("AjaxStyle.SetStyle(\"abc\") OK");elseConsole.WriteLine("AjaxStyle.SetStyle(\"abc\") faild.");}static void Test_AjaxCustomer_GetById(){AjaxCustomer ajax = new AjaxCustomer();object result = ajax.GetById(1);if( result is JsonResult )Console.WriteLine("AjaxCustomer.GetById(1) OK");elseConsole.WriteLine("AjaxCustomer.GetById(1) faild");}static void Test_TestEnvironment(){TestEnvironment.SetValue("key1", 123);TestEnvironment.SetValue("key2", "abc");if( (int)TestEnvironment.GetValue("key1") == 123 )Console.WriteLine("TestEnvironment.SetValue(123) OK.");if( (string)TestEnvironment.GetValue("key2") == "abc" )Console.WriteLine("TestEnvironment.SetValue(\"abc\") OK.");}

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

点击此处下载示例代码

注意:本文所介绍的框架还提供另外一个工具做为演示示例,
点击后面链接即可阅读。 【用ASP.NET写个SQLSERVER的小工具】

如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的【推荐】按钮。
如果,您希望更容易地发现我的新博客,不妨点击一下右下角的【关注 Fish Li】。
因为,我的写作热情也离不开您的肯定支持。

感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是Fish Li

写自己的ASP.NET MVC框架(下)相关推荐

  1. 【转】ASP.NET MVC框架下使用MVVM模式-KnockOutJS+JQ模板例子

    KnockOutJS学习系列----(一) 好几个月没去写博客了,最近也是因为项目紧张,不过这个不是借口,J. 很多时候可能是因为事情一多,然后没法静下来心来去写点东西,学点东西. 也很抱歉,突然看到 ...

  2. 写自己的ASP.NET MVC框架(上)

     开始 ASP.NET程序的几种开发方式 介绍我的MVC框架 我的MVC框架设计架构 回忆以往AJAX的实现方式 MyMVC中实现AJAX的方式 如何使用MyMVC框架中的AJAX功能 配置MyM ...

  3. 关于jQuery在Asp.Net Mvc 框架下Ajax文件上传的实现

    1. 实现传统的网络文件上传解决方案 首先,我先实现一个传统的网络文件上传方案,建立一个web页面,我还需要一个<form>和两个<input>元素就能解决问题,如在Index ...

  4. [转自scott]ASP.NET MVC框架 (第二部分): URL路径选择

    英文原文地址:http://weblogs.asp.net/scottgu/archive/2007/12/03/asp-net-mvc-framework-part-2-url-routing.as ...

  5. ASP.NET MVC 框架路线图更新 【转】

    [原文地址]ASP.NET MVC Framework Road-Map Update [原文发表日期] Tuesday, February 12, 2008 1:05 PM 去年的十二月份,作为AS ...

  6. 写在《ASP.NET MVC 4 Web 编程》即将出版之际!献给有节操的程序员!

    <Programming ASP.NET MVC 4>中文版即将上市了!非常荣幸我可以再次参与一本不错的技术书籍的翻译工作. 这也是在<WCF技术内幕>与<WCF服务编程 ...

  7. ASP.NET MVC框架(第一部分)

    [原文地址]ASP.NET MVC Framework (Part 1) [原文发表日期] Tuesday, November 13, 2007 3:45 AM 两个星期前, 我在博客里讨论了ASP. ...

  8. ASP.NET MVC 框架!

    [原文地址] ASP.NET MVC Framework  [原文发表日期] Sunday, October 14, 2007 10:41 PM 过去的几年里,很多人要求ASP.NET的一件事情就是对 ...

  9. ASP.NET MVC 框架

    ASP.NET MVC 框架 [原文地址]ASP.NET MVC Framework [原文发表日期] Sunday, October 14, 2007 10:41 PM 过去的几年里,很多人要求AS ...

最新文章

  1. 使用cacti对mysql监控的图像解释_Linux下的监控软件cacti的安装与配置
  2. 特征向量按照特征值大小进行排序
  3. quartz.net隔一天执行一次_你知多少年的轮回,中秋和国庆在同一天吗?喜相逢必备“蟹”逅礼...
  4. Hadoop迁移MaxCompute神器之DataX-On-Hadoop使用指南
  5. 残疾人软件开发_组织如何使残疾人更具包容性
  6. C语言经典贪心算法之加油站问题(详解)
  7. servlet 同版本对应的Tomcat版本 ,不同版本的web.xml写法
  8. Java Web提交任务到Spark Spark通过Java Web提交任务
  9. 网页授权前端 java_在开发网页授权获取用户基本信息时code是null
  10. 大数据分析技术与应用
  11. 安卓查看内存读写测试软件_办公电脑太慢?试试加条内存就好!内存晒单和内存问题科普...
  12. OV7725鹰眼摄像头
  13. Java:从1循环到150,在每行打印一个值,另外在每个3的倍数行上打印出foo“,在每个5的倍数行上打印“biz“,在每个7的倍数行上打印“baz“
  14. Kettle/Pentaho的Carte集群配置和使用
  15. 企业生产管理集成的核心工具—MES管理系统
  16. 利用AlphaFold进行蛋白质结构预测
  17. 【龙芯1c库】封装CAN接口和使用示例
  18. 1134:猴子与香蕉 dp
  19. SMETA验厂辅导,SEDEX(Smeta)验厂系统介绍及验厂结果查询
  20. 有关RO2模拟器与开私服的一点联想

热门文章

  1. 支持Esports,I love you!(记CCTV5《体育人间》栏目将聚焦《英雄联盟》)
  2. 想使用“本地项目” 的钥匙串
  3. Golang struct 指针引用用法(声明入门篇)
  4. C#语言实例源码系列-实现树形图列表
  5. 我来过,我很乖(催人泪下)
  6. Think Pad 前置摄像头不能打开及其解决
  7. 【读书笔记】计算广告学 Mooc
  8. C/C++实现仙侠类游戏的对战(附源码)
  9. 纪念我12月24日终于用妖姬拿首胜了
  10. filebeat output kafka配置参数