web form中自定義HttpHandler仿mvc

前言

  在mvc大行其道的今天,仍然有不少公司的專案還是使用web form來實現的(其實mvc也是基於web form的),如果要在專案中引入mvc,不得不新建一個mvc的專案,然後將當前專案的功能一點點的轉移過去,實在是很麻煩的一件事情,而且專案的改造週期也會加長,更別說一邊改造一邊新增新功能了,那麼如果中間出現那麼一點點的小差錯,那麼開發人員和測試人員估計想死的心都有了。

  基於以上的情景,我們可以通過自定義HttpHandler來仿造mvc的模式,大概的實現思路如下:

  1. 給頁面提供一個PageBase<TModel>的類來繼承,中間類似於mvc中存放Model的容器
  2. 通過類似/mvc/controller/action方式的url對於Controller內Action的呼叫(之前《C#實現簡易ajax呼叫後臺方法》這篇文章有簡單介紹過)
  3. 不同的action返回不同的ActionResult(如文字、Json等)
  4. 將自定義的MvcHandler在web.config中進行配置並引用相關的庫即可

實現

  首先我們需要自定義一個IHttpHandler來處理我們定義的mvc規則,並對其進行解析,其實原理就是上面提到的文章,只是Controller的Action會跟mvc的相似,返回ActionResult,程式碼大致如下:

publicabstractclassActionResult{publicabstractvoidExecuteResult(HttpContext context);}publicclassMvcHandler:IHttpHandler,IRequiresSessionState{publicconststring PREFIX ="/mvc/";//其他程式碼略publicvoidProcessRequest(HttpContext context){string path = context.Request.AppRelativeCurrentExecutionFilePath.Substring(PREFIX.Length);Int32 index = path.LastIndexOf("/");string route = path.Substring(0, index).ToLower();string actionName = path.Substring(index +1);//反射獲取Controller和Actionvar controller =null;var action =null;var actionParamters = action.GetParameters();object[] parameters =Array.ConvertAll(actionParamters, p =>{if(p.ParameterType==typeof(HttpPostedFile)){return context.Request.Files[p.Name];}returnConvert.ChangeType(collection[key], type);});var result = action.Invoke(controller, parameters,null)asActionResult;if(result !=null)
result.ExecuteResult(context);}  

  然後在web.config內的HttpHandlers內新增<add path="/mvc/*/*" type="Infrastructure.MvcHandler" verb="POST,GET"/>,規則可以任意定製,但是得注意url的格式,如果定義成了*/*/*那麼多攔截到全部的請求,那麼難度就增加了。

  接下來是頁面,與以往aspx頁面不同的是,我們需要在頁面上呼叫到相應的Model,那麼對於PageBase<TModel>就需要一個可以get Model的屬性,程式碼如下:

publicclassDynamicPageBase:Page{public T Model{protectedget;set;}}

  但是由於我們在頁面內呼叫Model之前,是要對其賦值的,因此就需要一個介面,程式碼改造如下:

publicinterfaceIMvcPage{voidSetModel(object model);}publicclassDynamicPageBase:Page,IMvcPage{private T m_Model =default(T);protected T Model{get{return m_Model;}}publicvoidSetModel(object model){if(model !=null)
m_Model =(T)model;}}

  在頁面上,我們就可以使用<%=Model.XXX%>的方式來獲取Model內的相關屬性了,對於頁面的改造大致已經完成了

  那麼我們怎麼樣像mvc那樣通過/controller/action的方式來返回html呢,使用過mvc的朋友應該知道,我們的view是要放在一些特定的位置下的,如相應的Controller資料夾內包含著相應的Action aspx頁面或razor頁面

  因此我們也可以在Web Form的目錄下建立一個Views的資料夾,專門用來存放所有對應的Action頁面,然後通過對url的解析來獲取相應的頁面,並將頁面轉化為html返回給客戶端,ViewResult大致程式碼如下:

string html ="";try{string childPath = context.Request.AppRelativeCurrentExecutionFilePath.Replace(MvcHandler.PREFIX,string.Empty);string virtualPath =string.Format("~/Views/{0}.aspx", childPath);IMvcPage page =PageParser.GetCompiledPageInstance(virtualPath, context.Server.MapPath(virtualPath), context)asIMvcPage;if(page !=null)
page.SetModel(m_model);using(StringWriter sw =newStringWriter()){
context.Server.Execute(page, sw,false);
html = sw.ToString();}}catch(Exception){
html ="無法訪問該檢視";}
context.Response.Write(html);

  其他的ActionResult都是根據返回型別的不同而有不同的實現,我就不詳細列舉出來了。

擴充套件

  相信留意過老趙部落格的朋友都看過《技巧:使用User Control做HTML生成》《方案改進:直接通過User Control生成HTML》這兩篇關於UserControl的文章,那麼我們可以參考裡面的實現來對頁面也新增相似的功能,並整合兩種方案,讓你的ViewResult可以生成aspx或ascx的html,我自己實現的規則是在頁面不存在的情況下,則查詢對應的UserControl是否存在,如果存在則返回UserControl的html,不存在的話則返回以上的無法訪問檢視的提示,程式碼改造大致如下:

//MvcHandler內string pageVirtualPath ="頁面虛擬路徑";string controlVirtualPath ="使用者控制元件虛擬路徑";//aspxif(File.Exists(context.Server.MapPath(pageVirtualPath))){var page = manager.LoadPage(pageVirtualPath)asIMvcPage;if(page !=null)
page.SetModel(m_model);
html = manager.RenderView();}//ascxelseif(File.Exists(context.Server.MapPath(controlVirtualPath))){var control = manager.LoadControl(controlVirtualPath);
html = manager.RenderView();}else{
html ="無法訪問該檢視";}publicclassViewManager{//其他程式碼略publicPageLoadPage(string virtualPath){
m_page =PageParser.GetCompiledPageInstance(virtualPath, m_context.Server.MapPath(virtualPath), m_context)asPage;
s_cache.SetViewPropertyValue(m_page, m_context.Request);return m_page;}publicControlLoadControl(string virtualPath){
m_page =newPage();
m_control = m_page.LoadControl(virtualPath);
m_page.Controls.Add(m_control);
s_cache.SetViewPropertyValue(m_control, m_context.Request);return m_control;}}

  對於MvcHandler而言,我們可以將部分的可變引數抽離出去,然後額外的進行實現,那麼仿mvc的程式碼就可以整理到一個dll中,可以讓其他的專案重用了。

  然後就是可以在MvcHandler內再新增一些Filter的功能,抽離出過濾的介面,來對於一些請求的過濾,那麼功能上就可以被進一步的擴充套件了。

結尾

  由於以往在寫文章的時候,都會提供詳細的實現原始碼,但是後來發現這樣並不能給其他人自己實現的機會,因此這次就不提供原始碼了,大部分重點的想法已經在文章中了,大家可以嘗試自己去實現,由於寫的文章也不多,如果有閱讀上的困難,請告訴我,我會發原始碼給各位,文章中如有任何錯誤和遺漏請大家指出,謝謝大家。

簡單的mvc之一:簡單的開始

  mvc學習到現在,相對所學到的一系列的知識做一個總結,於是就有了這個標題—簡單的mvc。文如名,寫的是簡單的mvc的知識,目標群也不言而喻。這一篇來個簡單的開始,從頭建立一個web專案,比如hello world。

  asp.net專案的請求處理核心是IHttpHandler,不論是之前的Page,還是之後MVC。所以最簡單的web專案,就是隻有一個IHttpHandler的專案。專案只有兩個檔案,一個.ashx檔案,內容:

<%@ WebHandler Class="Danyuers.SimpleMvc.Hello" Language="C#" %>

另外一個是相應的程式碼檔案,建立一個名為Hello的類,也就是上面的Class屬性所指向的類別,程式碼如下:

namespace Danyuers.SimpleMvc {
public class Hello : IHttpHandler {
public bool IsReusable {
get { return false; }
} public void ProcessRequest(HttpContext context) {
context.Response.Write("<h1>hello world!</h1>");
context.Response.End();
}
}
}

然後編譯(請將編譯目標檔案改為bin),對映到虛擬目錄,通過localhost:xxxx/xx.ashx即可訪問到建立的專案。記得新增相應的引用(system.web)。

  好了,簡單的專案搭建已經完成,但這不是真正的mvc,mvc最直觀的表現就是路由對映,區別於webform的檔案對映。一個簡單的mvc專案需要哪些東西呢?第一,global.asax檔案,定位到HttpApplication;第二,路由對映表,定義路由;第三,路由對映物件,也就是控制器。如同上面的ashx一樣,global.asax也只是包含一行指令:

<%@ Application Codebehind="Global.cs" Inherits="Danyuers.SimpleMvc.MvcApplication" Language="C#" %>

還是同上,建立global.cs,在其中建立SimpleMvcApplication,程式碼如下:

using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing; namespace Danyuers.SimpleMvc{
public class MvcApplication : HttpApplication {
protected void Application_Start() {
RouteRegister.Regist(RouteTable.Routes);
}
}
}

注意其中的RouteRegister,其中定義了我們需要的路由對映。建立單獨的程式碼檔案RouteRegister.cs,程式碼如下:

using System;
using System.Web.Mvc;
using System.Web.Routing; namespace Danyuers.SimpleMvc{
internal class RouteRegister {
public static void Regist(RouteCollection routes) {
routes.RouteExistingFiles = false;
routes.Ignore("{resources}.axd/{pathInfo*}"); routes.MapRoute(
name: "default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Hello", action = "Index", id = UrlParameter.Optional },
namespaces:new String[]{"Danyuers.SimpleMvc.Controllers*"}
);
}
}
}

從其中可以很明顯看到建立一個通用路由:{controller}/{action}/{id}。最後一步,建立控制器。建立Hello.cs,程式碼如下:

using System;
using System.Web.Mvc; namespace Danyuers.SimpleMvc.Controllers {
public class HelloController:Controller{
public void Index() {
Response.Write("<h1>Hello,world!!</h1>");
Response.End();
}
}
}

好了,最後編譯,iis對映,執行即可,相較於最初的單httphandler專案,這次直接訪問域名即可。

  相較於webform,mvc的程式碼分佈更加分散,卻也更加整潔,也更加富有彈性。

 
分類: C#想法