- 开始
- 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" %>
<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;"> 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" %>
<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"
从泛型类型继承的好处是:我可以在设计页面时,对于涉及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,则参考以下配置:
<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
{
///
/// 尝试根据当前请求,获取一个有效的Action,并返回ActionHandler
/// 此方法可以在HttpModule中使用,用于替代httpHandler的映射配置
///
///
///
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)
{
// 尝试根据请求路径获取Action
InvokeInfo 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();
}
///
/// 加载所有的Controller
///
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方法的实现代码如下:
///
/// 根据一个页面请求路径,返回内部表示的调用信息。
///
///
///
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的实现代码如下:
///
/// 用于验证用户身份的修饰属性
///
[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;
else
return (from s in value.Split(',')
let u = s.Trim()
where u.Length > 0
select u).ToArray();
}
///
/// 允许访问的用户列表,用逗号分隔。
///
public string Users
{
get { return _user; }
set { _user = value; _users = SplitString(value); }
}
///
/// 允许访问的角色列表,用逗号分隔。
///
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);
// 设置OutputCache
OutputCacheAttribute 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" %>
<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<>继承,它的实现代码如下:
///
/// 页面视图的基类
///
/// 传递给页面呈现时所需的数据实体对象类型
public class MyPageView<TModel> : MyBasePage
{
///
/// 用于页面呈现时所需的数据实体对象
///
public TModel Model { get; set; }
}
其实也就是一个简单的类型,包含了Model这个属性而已。 至于MyBasePage的实现代码,我们可以忽略它,它是直接从System.Web.UI.Page继承的。
再来一段用户控件的代码:
<%@ Control Language="C#" Inherits="MyUserControlView" %>
<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"
要让这种设置能够通过编译,需要在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的项目中,