前言
Asp.net WebForm 和 Asp.net MVC(簡稱MVC) 都是基於Asp.net的web開發框架,兩者有很大的區別,其中一個就是MVC更加注重http本質,而WebForm試圖屏蔽http,為此提供了大量的服務器控件和ViewState機制,讓開發人員可以像開發Windows Form應用程序一樣,基於事件模型去編程。兩者各有優缺點和適用情景,但MVC現在是許多Asp.net開發者的首選。
WebForm是建立在Asp.net的基礎上的,Asp.net提供了足夠的擴展性,我們也可以利用這些在WebForm下編寫像MVC一樣的框架,這個有機會再寫。說到WebForm很多人就會聯想到服務器控件(拖控件!!!),其實不然,我們也可以完全不使用服務器控件,像MVC那樣關注html。WebForm要拋棄服務器控件,集中關注html,首先就要將<form runat="server"></form>標簽去掉,這個runat server 的form 是其PostBack機制的基礎。既然我們要回歸到html+css+js,那麼意味著許多東西都要自己實現,例如處理Ajax請求。不像MVC那樣,WebForm開始的設計就將服務器控件作為主要組成部分,如果不使用它,那麼只能利用它的擴展性去實現。
本系列就是實現一個基於WebForm平台的輕量級ajax組件,主要分為三個部分:
1. 介紹WebForm下各種實現方式。
2. 分析ajaxpro組件。
3. 編寫自己的ajax組件。
一、Ajax簡介
異步允許我們在不刷新整個頁面的情況下,像服務器請求或提交數據。對於復雜的頁面,為了請求一點數據而重載整個頁面顯然是很低效的,ajax就是為了解決這個問題的。ajax的核心是XmlHttpRequest對象,通過該對象,以文本的形式向服務器提交請求。XmlHttpRequest2.0後,還支持提交二進制數據。
ajax安全:出於安全考慮,ajax受同源策略限制;也就是只能訪問同一個域、同一個端口的請求,跨域請求會被拒絕。當然有時候需求需要跨域發送請求,常用的跨域處理方法有CORS(跨域資源共享)和JSONP(參數式JSON)。
ajax數據交互格式:雖然Ajax核心對象XmlHttpRequest有"XML"字眼,但客戶端與服務器數據交換格式不局限於xml,例如現在更多是使用json格式。
ajax 也是有缺點的。例如對搜索引擎的支持不太好;有時候也會違背url資源定位的初衷。
二、Asp.net MVC 平台下使用ajax
在MVC裡,ajax調用後台方法非常方便,只需要指定Action的名稱即可。
前台代碼:
<body> <h1>index</h1> <input type="button" value="GetData" onclick="getData()" /> <span id="result"></span> </body> <script type="text/javascript"> function getData() { $.get("GetData", function (data) { $("#result").text(data); }); } </script>
後台代碼:
public class AjaxController : Controller { public ActionResult GetData() { if(Request.IsAjaxRequest()) { return Content("data"); } return View(); } }
三、WebForm 平台下使用ajax
3.1 基於服務器控件包或者第三方組件
這是基於服務器控件的,例如ajax toolkit工具包,或者像FineUI這樣的組件。web前端始終是由html+css+js組成的,只不過如何去生成的問題。原生的我們可以自己編寫,或者用一些前端插件;基於服務器控件的,都是在後台生成的,通常效率也低一點。服務器組件會在前台生成一系列代理,本質還是一樣的,只不過控件封裝了這個過程,不需要我們自己編寫。基於控件或者第三方組件的模式,在一些管理系統還是挺有用的,訪問量不是很大,可以快速開發。
3.2 基於ICallbackEventHandler接口
.net 提供了ICallbackEventHandler接口,用於處理回調請求。該接口需要用ClientScriptManager在前台生成代理腳本,用於發送和接收請求,所以需要<form runat="server">標簽。
前台代碼:
<body> <form id="form1" runat="server"> <div> <input type="button" value="獲取回調結果" onclick="callServer()" /> <span id="result"> public partial class Test1 : System.Web.UI.Page, ICallbackEventHandler { protected void Page_Load(object sender, EventArgs e) { //客戶端腳本Manager ClientScriptManager scriptMgr = this.ClientScript; //獲取回調函數,getCallbackResult就是回調函數 string functionName = scriptMgr.GetCallbackEventReference(this, "", "getCallbackResult", ""); //發起請求的腳本,callServer就是點擊按鈕事件的執行函數 string scriptExecutor = "function callServer(){" + functionName + ";}"; //注冊腳本 scriptMgr.RegisterClientScriptBlock(this.GetType(), "callServer", scriptExecutor, true); } //接口方法 public string GetCallbackResult() { return "callback result"; } //接口方法 public void RaiseCallbackEvent(string eventArgument) { } }
這種方式有以下缺點:
1. 實現起來較復雜,每個頁面Load事件都要去注冊相應的腳本。
2. 前台會生成一個用於代理的腳本文件。
3. 對於頁面交互復雜的,實現起來非常麻煩。
4. 雖然是回調,但是此時頁面對象還是生成了。
3.3 使用一般處理程序
一般處理程序其實是一個實現了IHttpHandler接口類,與頁面類一樣,它也可以用於處理請求。一般處理程序通常不用於生成html,也沒有復雜的事件機制,只有一個ProcessRequest入口用於處理請求。我們可以將ajax請求地址寫成.ashx文件的路徑,這樣就可以處理了,而且效率比較高。
要輸出文本內容只需要Response.Write(data)即可,例如,從數據庫獲取數據後,序列化為json格式字符串,然後輸出。前面說到,一般處理程序不像頁面一樣原來生成html,如果要生成html,可以通過加載用戶控件生成。如:
public void ProcessRequest(HttpContext context) { Page page = new Page(); Control control = page.LoadControl("~/PageOrAshx/UserInfo.ascx"); if (control != null) { StringWriter sw = new StringWriter(); HtmlTextWriter writer = new HtmlTextWriter(sw); control.RenderControl(writer); string html = sw.ToString(); context.Response.Write(html); } }
這種方式的優點是輕量、高效;缺點是對於交互多的需要定義許多ashx文件,加大了管理和維護成本。
3.4 頁面基類
將處理ajax請求的方法定義在頁面對象內,這樣每個頁面就可以專注處理本頁面相關的請求了。這裡有點需要注意。
1.如何知道這個請求是ajax請求?
通過請求X-Requested-With:XMLHttlRequest 可以判斷,大部份浏覽器的異步請求都會包含這個請求頭;也可以通過自定義請求頭實現,例如:AjaxFlag:XHR。
2.在哪裡統一處理?
如果在每個頁面類裡判斷和調用是很麻煩的,所以將這個處理過程轉到一個頁面基類裡處理。
3.如何知道調用的是哪個方法?
通過傳參或者定義在請求頭都可以,例如:MethodName:GetData。
4.知道方法名稱了,如何動態調用?
反射。
5.如何知道該方法可以被外部調用?
可以認為public類型的就可以被外部調用,也可以通過標記屬性標記。
通過上面的分析,簡單實現如下
頁面基類:
public class PageBase : Page { public override void ProcessRequest(HttpContext context) { HttpRequest request = context.Request; if (string.Compare(request.Headers["AjaxFlag"],"AjaxFlag",0) == 0) { string methodName = request.Headers["MethodName"]; if (string.IsNullOrEmpty(methodName)) { EndRequest("MethodName標記不能為空!"); } Type type = this.GetType().BaseType; MethodInfo info = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static); if (info == null) { EndRequest("找不到合適的方法調用!"); } string data = info.Invoke(this, null) as string; EndRequest(data); } base.ProcessRequest(context); } private void EndRequest(string msg) { HttpResponse response = this.Context.Response; response.Write(msg); response.End(); } }
頁面類:
public partial class Test1 : PageBase { protected void Page_Load(object sender, EventArgs e) { } public string GetData() { return "213"; } }
前台代碼:
function getData(){ $.ajax({ headers:{"AjaxFlag":"XHR","MethodName":"GetData"}, success:function(data){ $("#result").text(data); } }); }
四、優化版頁面基類
上面的頁面基類功能很少,而且通過反射這樣調用的效率很低。這裡優化一下:
1.可以支持簡單類型的參數。
例如上面的GetData可以是:GetData(string name),通過函數元數據可以獲取相關的參數,再根據請求的參數,就可以設置參數了。
2.加入標記屬性。
只有被AjaxMethodAttribute標記的屬性才能被外部調用。
3.優化反射。
利用緩存,避免每次都根據函數名稱去搜索函數信息。
標記屬性:
public class AjaxMethodAttribute : Attribute { }
緩存對象:
public class CacheMethodInfo { public string MethodName { get; set; } public MethodInfo MethodInfo { get; set; } public ParameterInfo[] Parameters { get; set; } }
基類代碼:
public class PageBase : Page { private static Hashtable _ajaxTable = Hashtable.Synchronized(new Hashtable()); public override void ProcessRequest(HttpContext context) { HttpRequest request = context.Request; if (string.Compare(request.Headers["AjaxFlag"],"XHR",true) == 0) { InvokeMethod(request.Headers["MethodName"]); } base.ProcessRequest(context); } /// <summary> /// 反射執行函數 /// </summary> /// <param name="methodName"></param> private void InvokeMethod(string methodName) { if (string.IsNullOrEmpty(methodName)) { EndRequest("MethodName標記不能為空!"); } CacheMethodInfo targetInfo = TryGetMethodInfo(methodName); if (targetInfo == null) { EndRequest("找不到合適的方法調用!"); } try { object[] parameters = GetParameters(targetInfo.Parameters); string data = targetInfo.MethodInfo.Invoke(this, parameters) as string; EndRequest(data); } catch (FormatException) { EndRequest("參數類型匹配發生錯誤!"); } catch (InvalidCastException) { EndRequest("參數類型轉換發生錯誤!"); } catch (ThreadAbortException) { } catch (Exception e) { EndRequest(e.Message); } } /// <summary> /// 獲取函數元數據並緩存 /// </summary> /// <param name="methodName"></param> /// <returns></returns> private CacheMethodInfo TryGetMethodInfo(string methodName) { Type type = this.GetType().BaseType; string cacheKey = type.AssemblyQualifiedName; Dictionary<string, CacheMethodInfo> dic = _ajaxTable[cacheKey] as Dictionary<string, CacheMethodInfo>; if (dic == null) { dic = new Dictionary<string, CacheMethodInfo>(); MethodInfo[] methodInfos = (from m in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) let ma = m.GetCustomAttributes(typeof(AjaxMethodAttribute), false) where ma.Length > 0 select m).ToArray(); foreach (var mi in methodInfos) { CacheMethodInfo cacheInfo = new CacheMethodInfo(); cacheInfo.MethodName = mi.Name; cacheInfo.MethodInfo = mi; cacheInfo.Parameters = mi.GetParameters(); dic.Add(mi.Name, cacheInfo); } _ajaxTable.Add(cacheKey, dic); } CacheMethodInfo targetInfo = null; dic.TryGetValue(methodName, out targetInfo); return targetInfo; } /// <summary> /// 獲取函數參數 /// </summary> /// <param name="parameterInfos"></param> /// <returns></returns> private object[] GetParameters(ParameterInfo[] parameterInfos) { if (parameterInfos == null || parameterInfos.Length <= 0) { return null; } HttpRequest request = this.Context.Request; NameValueCollection nvc = null; string requestType = request.RequestType; if (string.Compare("GET", requestType, true) == 0) { nvc = request.QueryString; } else { nvc = request.Form; } int length = parameterInfos.Length; object[] parameters = new object[length]; if (nvc == null || nvc.Count <= 0) { return parameters; } for (int i = 0; i < length; i++) { ParameterInfo pi = parameterInfos[i]; string[] values = nvc.GetValues(pi.Name); object value = null; if (values != null) { if (values.Length > 1) { value = String.Join(",", values); } else { value = values[0]; } } if (value == null) { continue; } parameters[i] = Convert.ChangeType(value, pi.ParameterType); } return parameters; } private void EndRequest(string msg) { HttpResponse response = this.Context.Response; response.Write(msg); response.End(); } }
頁面類:
[AjaxMethod] public string GetData3(int i, double d, string str) { string[] datas = new string[] { i.ToString(), d.ToString(), str }; return "參數分別是:" + String.Join(",", datas); }
前台代碼:
function getData3(){ $.ajax({ headers:{"AjaxFlag":"XHR","MethodName":"GetData3"}, data:{"i":1,"d":"10.1a","str":"hehe"}, success:function(data){ $("#result").text(data); } }); }
五、總結
上面的頁面基類已經具備可以完成基本的功能,但它還不夠好。主要有:
1. 依附在頁面基類。對於本來有頁面基類的,無疑會變得更加復雜。我們希望把它獨立開來,變成一個單獨的組件。
2. 效率問題。反射的效率是很低的,尤其在web這類應用程序上,更應該慎用。以動態執行函數為例,效率主要低在:a.根據字符串動態查找函數的過程。b.執行函數時,反射內部需要將參數打包成一個數組,再將參數解析到線程棧上;在調用前CLR還要檢測參數的正確性,再判斷有沒有權限執行。上面的優化其實只優化了一半,也就是優化了查找的過程,而Invoke同樣會有性能損失。當然,隨著.net版本越高,反射的效率也會有所提升,但這種動態的東西,始終是用效率換取靈活性的。
3.不能支持復雜參數。有時候參數比較多,函數參數一般會封裝成一個對象類型。
4. AjaxMethodAttribute只是一個空的標記屬性。我們可以為它加入一些功能,例如,標記函數的名稱、是否使用Session、緩存設置等都可以再這裡完成。
用過WebForm的朋友可能會提到AjaxPro組件,這是一個開源的組件,下一篇就通過源碼了解這個組件,借鑒它的處理過程,並且分析它的優缺點。