寫了幾篇細說之后,今天打算換換口味,還是來寫代碼吧。 所以,這次博客將以實際的代碼來展示在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,就像下面的代碼:
<%@ WebHandler Language="C#" Class="Handler1" %>
using System;
using System.Web;
public class Handler1 : IHttpHandler {
public void ProcssRequest (HttpContext context) {
// 【1】. 從context.Request中讀取輸入參數
string param1 = context.Request.QueryString["param1"];
string param2 = context.Request.QueryString["param2"];
// 【2】. 根據上面所獲取的參數,調用服務層或者BLL層獲取結果
// var result = CallXxxxMethod(param1, param2);
// 【3】. 將結果寫入context.Response
context.Response.ContentType = "text/plain";
context.Response.Write(" result ...... ");
}
public bool IsReusable { get { return false; } }
}
當然了,也有人會選擇創建一個空的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也可以支持復雜的數據類型。 例如,以下方法的簽名都是有效的:
[Action]
public void Insert(Customer customer)
[Action]
public object Delete(int id, string returnUrl)
[Action]
public object ShowCustomerPicker(string searchWord, int? page)
[Action]
public object Search(OrderSearchInfo info, int? page)
[Action]
public object ShowProductPicker(int? categoryId, string searchWord, int? page)
[Action]
public object LoadModel()
有了MyMVC,就幾乎上不需要再去訪問QueryString,Form這些對象了。 你需要什么參數,只要寫在方法的簽名中就可以了。參數可以是簡單的數據類型,也可以是自定義的數據類型,參數的個數也沒有限制。
不過,有一點我要提醒您:所有的數據來源只有二個地方:QueryString和Form,框架只讀取這二個地方,而且直接訪問它們的索引器。 由於QueryString,Form這二個類型都是NameValueCollection,而NameValueCollection的索引器在實現上有點獨特,因此請大家注意它們的返回值。 關於NameValueCollection的細節描述,可以參考我的博客【細說 Request[]與Request.Params[]】, 今天我就不再重談這個細節話題了。
在讀取參數時,萬一出現key重復了怎么辦?
框架還提供另一種解決方案,那就是您可以在C#的方法的簽名中,聲明NameValueCollection類型的變量,變量名可以從【Form,QueryString,Headers,ServerVariables】中選擇。 注意:對於后二者,框架本身也是不讀取的,如果需要讀取,只能使用這個方法來獲取。示例代碼如下:
[Action]
public string TestNameValueCollection(NameValueCollection queryString, NameValueCollection form,
NameValueCollection headers, NameValueCollection serverVariables)
{
StringBuilder sb = new StringBuilder();
foreach( string key in queryString.AllKeys )
sb.AppendFormat("queryString, {0} = {1}\r\n", key, queryString[key]);
foreach( string key in form.AllKeys )
sb.AppendFormat("form, {0} = {1}\r\n", key, form[key]);
foreach( string key in headers.AllKeys )
sb.AppendFormat("headers, {0} = {1}\r\n", key, headers[key]);
foreach( string key in serverVariables.AllKeys )
sb.AppendFormat("serverVariables, {0} = {1}\r\n", key, serverVariables[key]);
return sb.ToString();
}
代碼中,我同時要求框架給出這四個集合,事實上,您可以根據實際情況來決定需要多少個參數。
注意:
1. 參數名稱是大小寫【不敏感】的。
2. 類型一定要求是NameValueCollection 。
3. 框架會優先讀取QueryString,如果沒有則會查看Form
4. 千萬不要在Action中使用HttpContext.Current.Request.QueryString[]的方式讀取來自客戶端的參數。
關於參數,還有一種特殊的情況:我在博客【細說 Form (表單)】中曾提到過, 例如,我有這樣二個類型,它們的結構一樣:
public class Customer
{
public string Name;
public string Tel;
}
public class Salesman
{
public string Name { get; set; }
public string Tel { get; set; }
}
如果此時我有這樣一個C#方法,又該如何處理呢?
[Action]
public string TestCustomerType(Customer customer, Salesman salesman)
{
return "customer.Name = " + customer.Name + "\r\n" +
"customer.Tel = " + customer.Tel + "\r\n" +
"salesman.Name = " + salesman.Name + "\r\n" +
"salesman.Name = " + salesman.Tel;
}
上面的示例也可以理解成:一模一樣的參數類型,就是要出現多次,再或者,多個不同的自定義類型中,有些成員的名稱是相同的。
此時我的框架在設計時與微軟的MVC框架一樣,要求在HTML中對name做特殊的設置,示例代碼如下:
<form action="/AjaxDemo2/TestCustomerType.cspx" method="post">
<p>客戶名稱: <input type="text" name="customer.Name" style="width: 300px" /></p>
<p>客戶電話: <input type="text" name="customer.Tel" style="width: 300px" /></p>
<p>銷售員名稱: <input type="text" name="salesman.Name" style="width: 300px" /></p>
<p>銷售員電話: <input type="text" name="salesman.Tel" style="width: 300px" /></p>
<p><input type="submit" value="提交" /></p>
</form>
此時要求: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的環境中運行,還需要以下配置:
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<security>
<requestFiltering>
<fileExtensions>
<remove fileExtension=".cspx"/>
<add fileExtension=".cspx" allowed="true"/>
</fileExtensions>
</requestFiltering>
</security>
<handlers>
<add name="AjaxHandlerFactory" verb="*" path="*Ajax*/*.cspx"
type="MyMVC.AjaxHandlerFactory, MyMVC" preCondition="integratedMode"/>
</handlers>
</system.webServer>
在示例代碼中,我使用了【cspx】這個擴展名,如果您不喜歡,也可以選擇您所喜歡的擴展名,這個不是問題。
關於配置參數中的【path】屬性,請參考我的上篇博客【細說 HttpHandler 的映射過程】,這里也不再重新解釋。 如果沒有看過的,建議還是去看一下,下面將會用到那些知識,因為它非常重要。
MyMVC框架的實現原理 - 映射處理器(入口)
前面談到了MyMVC框架的配置,通過那個配置,相當於在ASP.NET中為MyMVC注冊了一個入口點。
根據上面的配置,符合條件的請求將會被映射給AjaxHandlerFactory。既然是這樣,我們來看一下這個入口點的實現代碼:
internal sealed class AjaxHandlerFactory : IHttpHandlerFactory
{
public IHttpHandler GetHandler(HttpContext context,
string requestType, string virtualPath, string physicalPath)
{
// 根據請求路徑,定位到要執行的Action
ControllerActionPair pair = UrlParser.ParseAjaxUrl(virtualPath);
if( pair == null )
ExceptionHelper.Throw404Exception(context);
// 獲取內部表示的調用信息
InvokeInfo vkInfo = ReflectionHelper.GetAjaxInvokeInfo(pair);
if( vkInfo == null )
ExceptionHelper.Throw404Exception(context);
// 創建能夠調用Action的HttpHandler
return ActionHandler.CreateHandler(vkInfo);
}
public void ReleaseHandler(IHttpHandler handler)
{
}
}
代碼中,每個步驟做了什么事情,注釋中有說明,不需要再重復說明。最后創建的ActionHandler的實現代碼如下:
internal class ActionHandler : IHttpHandler
{
internal InvokeInfo InvokeInfo;
public void ProcessRequest(HttpContext context)
{
// 調用核心的工具類,執行Action
ActionExecutor.ExecuteAction(context, this.InvokeInfo);
}
public bool IsReusable
{
get { return false; }
}
整個入口點就是這樣的。
有沒人想過:為什么不直接在web.config中映射到這個ActionHandler呢?
答案在后面,請繼續閱讀。
MyMVC框架的實現原理 - 對Session的支持
前面有一個方法的實現我故意沒有貼出,那么是ActionHandler.CreateHandler()這個靜態方法。現在是時候來貼它了:
public static ActionHandler CreateHandler(InvokeInfo vkInfo)
{
SessionMode mode = vkInfo.GetSessionMode();
if( mode == SessionMode.NotSupport )
return new ActionHandler { InvokeInfo = vkInfo };
else if( mode == SessionMode.ReadOnly )
return new ReadOnlySessionActionHandler { InvokeInfo = vkInfo };
else
return new RequiresSessionActionHandler { InvokeInfo = vkInfo };
}
這段代碼又涉及另外二個類型,它們的實現代碼如下:
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(SessionMode.Support)]
[Action]
public int TestSessionMode(int a)
{
// 一個累加的方法,檢驗是否可以訪問Session
// 警告:示例代碼的這樣做法會影響Action的單元測試。
if( System.Web.HttpContext.Current.Session == null )
throw new InvalidOperationException("Session沒有開啟。");
object obj = System.Web.HttpContext.Current.Session["counter"];
int counter = (obj == null ? 0 : (int)obj);
counter += a;
System.Web.HttpContext.Current.Session["counter"] = counter;
return counter;
}
在上面這段代碼中,我加了一個[SessionMode]的Attribute,用它可以指定Action的Session支持模式, SessionMode是個枚舉值,定義如下:
/// <summary>
/// Action所支持的Session模式
/// </summary>
public enum SessionMode
{
/// <summary>
/// 不支持
/// </summary>
NotSupport,
/// <summary>
/// 全支持
/// </summary>
Support,
/// <summary>
/// 僅支持讀取
/// </summary>
ReadOnly
}
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中的代碼:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class OutputCacheAttribute : Attribute
{
private OutputCacheParameters _cacheSettings = new OutputCacheParameters();
// 略過一些屬性。
internal void SetResponseCache(HttpContext context)
{
if( context == null )
throw new ArgumentNullException("context");
OutputCachedPage page = new OutputCachedPage(_cacheSettings);
page.ProcessRequest(context);
}
private sealed class OutputCachedPage : Page
{
private OutputCacheParameters _cacheSettings;
public OutputCachedPage(OutputCacheParameters cacheSettings)
{
this.ID = Guid.NewGuid().ToString();
_cacheSettings = cacheSettings;
}
protected override void FrameworkInitialize()
{
base.FrameworkInitialize();
InitOutputCache(_cacheSettings);
}
}
}
在OutputCacheAttribute類的用法中,清楚地指出適用於類型與方法,因此,這個Attribute可以用於Controller和Action 。
說明:OutputCacheAttribute與SessionModeAttribute類似,都可以用於Controller和Action,同時使用時,Action優先匹配,代碼如下:
internal sealed class InvokeInfo
{
public ControllerDescription Controller;
public ActionDescription Action;
public object Instance;
public OutputCacheAttribute GetOutputCacheSetting()
{
if( this.Action != null && this.Action.OutputCache != null )
return this.Action.OutputCache;
if( this.Controller != null && this.Controller.OutputCache != null )
return this.Controller.OutputCache;
return null;
}
public SessionMode GetSessionMode()
{
if( this.Action != null && this.Action.SessionMode != null )
return this.Action.SessionMode.SessionMode;
if( this.Controller != null && this.Controller.SessionMode != null )
return this.Controller.SessionMode.SessionMode;
return SessionMode.NotSupport;
}
}
因此,框架只要選擇一個時機調用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的:
internal static class UrlParser
{
// 用於匹配Ajax請求的正則表達式,
// 可以匹配的URL:/AjaxClass/method.cspx?id=2
// 注意:類名必須Ajax做為前綴
internal static readonly string AjaxUrlPattern
= @"/(?<name>(\w[\./\w]*)?(?=Ajax)\w+)[/\.](?<method>\w+)\.[a-zA-Z]+";
public static ControllerActionPair ParseAjaxUrl(string path)
{
if( string.IsNullOrEmpty(path) )
throw new ArgumentNullException("path");
Match match = Regex.Match(path, AjaxUrlPattern);
if( match.Success == false )
return null;
return new ControllerActionPair {
Controller = match.Groups["name"].Value.Replace("/", "."),
Action = match.Groups["method"].Value
};
}
}
代碼很簡單,核心其實就是那個正則表達式,從URL中提取Controller,Action的名字全靠它。
至於正則表達式的使用,我想這是個基本功,這里就略過了。
再來看AjaxHandlerFactory的第二個調用:
// 獲取內部表示的調用信息
InvokeInfo vkInfo = ReflectionHelper.GetAjaxInvokeInfo(pair);
ReflectionHelper類是一個內部使用的工具類,專門用於反射處理,AjaxAction查找過程的相關代碼如下(注意代碼中的注釋):
internal static class ReflectionHelper
{
// 保存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);
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));
}
}
catch { }
}
// 用於Ajax調用的Action信息則采用延遲加載的方式。
}
/// <summary>
/// 根據要調用的controller名返回對應的Controller (適用於Ajax調用)
/// </summary>
/// <param name="controller"></param>
/// <returns></returns>
private static ControllerDescription GetAjaxController(string controller)
{
if( string.IsNullOrEmpty(controller) )
throw new ArgumentNullException("controller");
// 查找類型的方式:如果有點號,則按全名來查找(包含命名空間),否則只看名字。
// 本框架對於多個匹配條件的類型,將返回第一個匹配項。
if( controller.IndexOf('.') > 0 )
return s_AjaxControllerList.FirstOrDefault(
t => string.Compare(t.ControllerType.FullName, controller, true) == 0);
else
return s_AjaxControllerList.FirstOrDefault(
t => string.Compare(t.ControllerType.Name, controller, true) == 0);
}
/// <summary>
/// 根據要調用的方法名返回對應的 Action (適用於Ajax調用)
/// </summary>
/// <param name="controller"></param>
/// <param name="action"></param>
/// <returns></returns>
private static ActionDescription GetAjaxAction(Type controller, string action)
{
if( controller == null )
throw new ArgumentNullException("controller");
if( string.IsNullOrEmpty(action) )
throw new ArgumentNullException("action");
// 首先嘗試從緩存中讀取
string key = action + "@" + controller.FullName;
ActionDescription mi = (ActionDescription)s_AjaxActionTable[key];
if( mi == null ) {
// 注意:這里不考慮方法的重載。
MethodInfo method = controller.GetMethod(action, ActionBindingFlags);
if( method == null )
return null;
var attrs = (ActionAttribute[])method.GetCustomAttributes(typeof(ActionAttribute), false);
if( attrs.Length != 1 )
return null;
mi = new ActionDescription(method, attrs[0]);
s_AjaxActionTable[key] = mi;
}
return mi;
}
/// <summary>
/// 根據一個AJAX的調用信息(類名與方法名),返回內部表示的調用信息。
/// </summary>
/// <param name="pair"></param>
/// <returns></returns>
public static InvokeInfo GetAjaxInvokeInfo(ControllerActionPair pair)
{
if( pair == null )
throw new ArgumentNullException("pair");
InvokeInfo vkInfo = new InvokeInfo();
vkInfo.Controller = GetAjaxController(pair.Controller);
if( vkInfo.Controller == null )
return null;
vkInfo.Action = GetAjaxAction(vkInfo.Controller.ControllerType, pair.Action);
if( vkInfo.Action == null )
return null;
if( vkInfo.Action.MethodInfo.IsStatic == false )
vkInfo.Instance = Activator.CreateInstance(vkInfo.Controller.ControllerType);
return vkInfo;
}
上面就是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中執行的:
internal class ActionHandler : IHttpHandler
{
internal InvokeInfo InvokeInfo;
public void ProcessRequest(HttpContext context)
{
// 調用核心的工具類,執行Action
ActionExecutor.ExecuteAction(context, this.InvokeInfo);
}
ExecuteAction的實現過程如下:
internal static void ExecuteAction(HttpContext context, InvokeInfo vkInfo)
{
if( context == null )
throw new ArgumentNullException("context");
if( vkInfo == null )
throw new ArgumentNullException("vkInfo");
// 調用方法
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());
}
}
}
internal static object ExecuteActionInternal(HttpContext context, InvokeInfo info)
{
// 准備要傳給調用方法的參數
object[] parameters = GetActionCallParameters(context, info.Action);
// 調用方法
if( info.Action.HasReturn )
return info.Action.MethodInfo.Invoke(info.Instance, parameters);
else {
info.Action.MethodInfo.Invoke(info.Instance, parameters);
return null;
}
}
前面我不是沒有說調用SetResponseCache()的時機嘛,這個時機就是在這里:執行完Action后。
設置過OutputCache后,就是處理返回值了。
前面那段代碼中,還有一句重要的調用:
// 准備要傳給調用方法的參數
object[] parameters = GetActionCallParameters(context, info.Action);
這個調用的意義在注釋中有解釋,關於這個過程的實現方式還請繼續閱讀。
MyMVC框架的實現原理 - 如何給方法賦值
用過反射的人都知道,調用一個方法很簡單,但如何給一個【不知簽名】的方法准備傳入參數呢?
下面就來回答這個問題,請接着看GetActionCallParameters的實現過程:
private static object[] GetActionCallParameters(HttpContext context, ActionDescription action)
{
if( action.Parameters == null || action.Parameters.Length == 0 )
return null;
object[] parameters = new object[action.Parameters.Length];
for( int i = 0; i < action.Parameters.Length; i++ ) {
ParameterInfo p = action.Parameters[i];
if( p.IsOut )
continue;
if( p.ParameterType == typeof(NameValueCollection) ) {
if( string.Compare(p.Name, "Form", StringComparison.OrdinalIgnoreCase) == 0 )
parameters[i] = context.Request.Form;
else if( string.Compare(p.Name, "QueryString", StringComparison.OrdinalIgnoreCase) == 0 )
parameters[i] = context.Request.QueryString;
else if( string.Compare(p.Name, "Headers", StringComparison.OrdinalIgnoreCase) == 0 )
parameters[i] = context.Request.Headers;
else if( string.Compare(p.Name, "ServerVariables", StringComparison.OrdinalIgnoreCase) == 0 )
parameters[i] = context.Request.ServerVariables;
}
else{
Type paramterType = p.ParameterType.GetRealType();
// 如果參數是簡單類型,則直接從HttpRequest中讀取並賦值
if( paramterType.IsSimpleType() ) {
object val = ModelHelper.GetValueByKeyAndTypeFrommRequest(
context.Request, p.Name, paramterType, null);
if( val != null )
parameters[i] = val;
}
else {
// 自定義的類型。首先創建實例,然后給所有成員賦值。
// 注意:這里不支持嵌套類型的自定義類型。
object item = Activator.CreateInstance(paramterType);
ModelHelper.FillModel(context.Request, item, p.Name);
parameters[i] = item;
}
}
}
return parameters;
}
要理解這段代碼還要從前面的【查找Action的過程】說起,在那個階段,可以獲取一個Action的描述,具體在框架內部表示為ActionDescription類型:
internal sealed class ActionDescription : BaseDescription
{
public ControllerDescription PageController; //為PageAction保留
public MethodInfo MethodInfo { get; private set; }
public ActionAttribute Attr { get; private set; }
public ParameterInfo[] Parameters { get; private set; }
public bool HasReturn { get; private set; }
public ActionDescription(MethodInfo m, ActionAttribute atrr) : base(m)
{
this.MethodInfo = m;
this.Attr = atrr;
this.Parameters = m.GetParameters();
this.HasReturn = m.ReturnType != ReflectionHelper.VoidType;
}
}
在構造函數的第三行代碼中,我就可以得到這個方法的所有參數情況。
然后,我在就可以在GetActionCallParameters方法中,循環每個參數的定義,為它們賦值。
這段代碼也解釋了前面所說的只支持4種NameValueCollection集合的原因。
注意了,我在獲取每個參數的類型時,是使用了下面的語句:
Type paramterType = p.ParameterType.GetRealType();
實際上,ParameterType就已經反映了參數的類型,為什么不直接使用它呢?
答:因為【可空泛型】的原因。這個類型我們需要特殊的處理。
例如:如果某個參數是這樣聲明的: int? id
那么,即使在QueryString中包含id這樣一個參數,我也不能直接轉成 int? 使用這種類型,必須得到它的【實際類型】。
GetRealType()是個擴展方法,它就專門完成這個功能:
/// <summary>
/// 得到一個實際的類型(排除Nullable類型的影響)。比如:int? 最后將得到int
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static Type GetRealType(this Type type)
{
if( type.IsGenericType )
return Nullable.GetUnderlyingType(type) ?? type;
else
return type;
}
如果某個參數的類型是一個自定義的類型,框架會先創建實例(調用無參的構造函數),然后給它的Property, Field賦值。
注意了:自定義的類型,一定要提供一個無參的構造函數。
為自定義類型的實例填充數據成員的代碼如下:
internal static class ModelHelper
{
public static readonly bool IsDebugMode;
static ModelHelper()
{
CompilationSection configSection =
ConfigurationManager.GetSection("system.web/compilation") as CompilationSection;
if( configSection != null )
IsDebugMode = configSection.Debug;
}
/// <summary>
/// 根據HttpRequest填充一個數據實體。
/// 這里不支持嵌套類型的數據實體,且要求各數據成員都是簡單的數據類型。
/// </summary>
/// <param name="request"></param>
/// <param name="model"></param>
public static void FillModel(HttpRequest request, object model, string paramName)
{
ModelDescripton descripton = ReflectionHelper.GetModelDescripton(model.GetType());
object val = null;
foreach( DataMember field in descripton.Fields ) {
// 這里的實現方式不支持嵌套類型的數據實體。
// 如果有這方面的需求,可以將這里改成遞歸的嵌套調用。
val = GetValueByKeyAndTypeFrommRequest(
request, field.Name, field.Type.GetRealType(), paramName);
if( val != null )
field.SetValue(model, val);
}
}
/// <summary>
/// 讀取一個HTTP參數值。這里只讀取QueryString以及Form
/// </summary>
/// <param name="request"></param>
/// <param name="key"></param>
/// <returns></returns>
public static string GetHttpValue(HttpRequest request, string key)
{
string val = request.QueryString[key];
if( val == null )
val = request.Form[key];
return val;
}
public static object GetValueByKeyAndTypeFrommRequest(
HttpRequest request, string key, Type type, string paramName)
{
// 不支持復雜類型
if( type.IsSimpleType() == false )
return null;
string val = GetHttpValue(request, key);
if( val == null ) {
// 再試一次。有可能是多個自定義類型,Form表單元素采用變量名做為前綴。
if( string.IsNullOrEmpty(paramName) == false ) {
val = GetHttpValue(request, paramName + "." + key);
}
if( val == null )
return null;
}
return SafeChangeType(val.Trim(), type);
}
public static object SafeChangeType(string value, Type conversionType)
{
if( conversionType == typeof(string) )
return value;
if( value == null || value.Length == 0 )
// 空字符串根本不能做任何轉換,所以直接返回null
return null;
try {
// 為了簡單,直接調用 .net framework中的方法。
// 如果轉換失敗,則會拋出異常。
return Convert.ChangeType(value, conversionType);
}
catch {
if( IsDebugMode )
throw; // Debug 模式下拋異常
else
return null; // Release模式下忽略異常(防止惡意用戶錯誤輸入)
}
}
}
在給自定義的數據類型實例加載數據前,需要先知道這個實例對象有哪些屬性以及字段,這個過程的代碼如下:
/// <summary>
/// 返回一個實體類型的描述信息(全部屬性及字段)。
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static ModelDescripton GetModelDescripton(Type type)
{
if( type == null )
throw new ArgumentNullException("type");
string key = type.FullName;
ModelDescripton mm = (ModelDescripton)s_modelTable[key];
if( mm == null ) {
List<DataMember> list = new List<DataMember>();
(from p in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
select new PropertyMember(p)).ToList().ForEach(x=>list.Add(x));
(from f in type.GetFields(BindingFlags.Instance | BindingFlags.Public)
select new FieldMember(f)).ToList().ForEach(x => list.Add(x));
mm = new ModelDescripton { Fields = list.ToArray() };
s_modelTable[key] = mm;
}
return mm;
}
在拿到一個類型的所有屬性以及字段的描述信息后,就可以通過循環的方式,根據這些數據成員的名字去QueryString,Form讀取所需的數據了。
參數准備好了,前面的調用就應該沒有問題了吧?
MyMVC框架的實現原理 - 處理返回值
MyMVC框架處理返回值的時機是在ExecuteAction方法中(前面有那段代碼)。
這里只做個簡單的補充說明。
我為Action的結果定義了一個接口:
public interface IActionResult
{
void Ouput(HttpContext context);
}
框架內實現了4種ActionResult:
/// <summary>
/// 表示一個用戶控件結果(用戶控件將由框架執行)
/// </summary>
public sealed class UcResult : IActionResult
/// <summary>
/// 表示一個重定向的結果
/// </summary>
public sealed class RedirectResult : IActionResult
/// <summary>
/// 一個Json對象結果
/// </summary>
public sealed class JsonResult : IActionResult
/// <summary>
/// 表示一個頁面結果(頁面將由框架執行)
/// </summary>
public sealed class PageResult : IActionResult
要輸出返回值的時候,不僅使用了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類型了。 它們的使用代碼如下:
[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);
}
由於我從來不用Page輸出一段HTML,因此沒有准備在Ajax中使用PageResult的示例。 但是,它們的使用方法是一樣,因為:PageResult和UcResult的構造函數有着一致的簽名方式。
再來說說創建UcResult對象那行代碼:傳入二個參數,第一個參數表示用戶控件的位置(View),第二個參數表示呈現用戶控件所需的數據(Model)。 至於這個地方為什么要設計二個參數,請關注我的后續博客,因為它涉及到MVC的核心思想,今天的博客不打算談這個話題。
MyMVC框架的實現原理 - 多命名空間的支持
前面的示例代碼都是演示了如何設計一個能供JS調用的Action,事實上,您也看到了,其實就是加了個[Action]的方法而已,沒有其它的特別之處了。 不過,在現實開發中,類型的名字可能會沖突。比如:.NET就引入了命名空間來處理這種沖突的類名。
MyMVC支持同名的Controller的名字嗎?
答案是肯定的:支持。
例如,我有下面二個類型。注意它們的名字是相同的。
namespace Fish.AA
{
public class AjaxTest
{
[Action]
public int Add(int a, int b)
{
return a + b;
}
}
}
namespace Fish.BB
{
public class AddInfo
{
public int A;
public int B;
}
public class AjaxTest
{
[Action]
public int Add(AddInfo info)
{
return info.A + info.B + 10; // 故意寫錯。
}
}
}
這二個類型不僅同名,而且還包含了同名的方法。(事實上,方法的簽名也可以完全一樣。)
那么,對於這種情況,JS如何去調用它們呢?
為了回答這個問題,我特意准備了一個示例,HTML代碼如下:
<fieldset>
<legend>Fish.AA.AjaxTest.Add</legend>
<input type="text" id="txtA1" style="width: 50px" value="3" /> +
<input type="text" id="txtB1" style="width: 50px" value="2" /> =
<span id="spanResult1"></span>
<input type="button" id="btnAdd1" value="Add" />
</fieldset>
<p></p>
<fieldset>
<legend>Fish.BB.AjaxTest.Add</legend>
<input type="text" id="txtA2" style="width: 50px" value="3" /> +
<input type="text" id="txtB2" style="width: 50px" value="2" /> =
<span id="spanResult2"></span>
<input type="button" id="btnAdd2" value="Add" />
</fieldset>
客戶端的JS代碼如下:
$(function(){
$("#btnAdd1").click(function(){
$.ajax({
url: "/Fish.AA.AjaxTest/Add.cspx",
data: {a: $("#txtA1").val(), b: $("#txtB1").val()},
success: function(responseText){
$("#spanResult1").text(responseText);
}
});
});
$("#btnAdd2").click(function(){
$.ajax({
// 以下二個URL地址都是有效的。
//url: "/Fish.BB.AjaxTest.Add.cspx",
url: "/Fish/BB/AjaxTest/Add.cspx",
data: {a: $("#txtA2").val(), b: $("#txtB2").val()},
success: function(responseText){
$("#spanResult2").text(responseText);
}
});
});
});
最終的調用結果如下:

注意:下方的調用結果雖然是錯誤的,但表示調用的方法是正確的。
讓我們再來回顧一下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框架源代碼及示例代碼請點擊此處進入下載頁面
如果,您認為閱讀這篇博客讓您有些收獲,不妨點擊一下右下角的【推薦】按鈕。
如果,您希望更容易地發現我的新博客,不妨點擊一下右下角的【關注 Fish Li】。
因為,我的寫作熱情也離不開您的肯定支持。
感謝您的閱讀,如果您對我的博客所講述的內容有興趣,請繼續關注我的后續博客,我是Fish Li 。