一、MVC概念回顧
路由(Route)、控制器(Controller)、行為(Action)、模型(Model)、視圖(View)
用一句簡單地話來描述以上關鍵點:
路由(Route)就相當於一個公司的前台小姐,她負責帶你(請求)找到跟你面試的面試官(控制器Controller),面試官可能會面試不同的職位(Action),你(請求)也會拿到不同的結果(ActionResult);
二、開始DEMO:單一處理程序入口
2.1 創建一個空白Web程序,移除所有默認引用
無論是ASP.NET WebForms還是ASP.NET MVC,他們都只是一個框架,是建立在System.Web之上的框架。為了保證程序的純凈,我們可以將所有默認的引用都移除。當然,我們還是得保留幾個必要的dll引用:
注意:這里我們並沒有引入System.Web.Mvc.dll,因為我們要實現的就是一個簡單的MVC機制。
2.2 模擬ASP.NET MVC,創建幾個MVC文件夾
按照ASP.NET MVC的慣例添加Controllers、Models和Views文件夾(不是必須的):
2.3 新建一個Controller
我們首先在Controllers文件夾下新建一個接口,取名為IController,它約定了所有Controller都必須要實現的方法:Execute
public interface IController { void Execute(HttpContext context); }
IController接口只定義了一個方法聲明,它接收一個HttpContext的上下文對象。
有了接口,我們就可以實現具體的Controller了,這里我們實現了兩個Controller:HomeController和ProductController。
(1)HomeController

public class HomeController : IController { private HttpContext currentContext; // action 1 : Index public void Index() { currentContext.Response.Write("Home Index Success!"); } // action 2 : Add public void Add() { currentContext.Response.Write("Home Add Success!"); } public void Execute(HttpContext context) { currentContext = context; // 默認Action名稱 string actionName = "index"; // 獲取Action名稱 if (!string.IsNullOrEmpty(context.Request["action"])) { actionName = context.Request["action"]; } switch (actionName.ToLower()) { case "index": this.Index(); break; case "add": this.Add(); break; default: this.Index(); break; } } }
(2)ProductController

public class ProductController : IController { private HttpContext currentContext; // action 1 : Index public void Index() { currentContext.Response.Write("Product Index Success!"); } // action 2 : Add public void Add() { currentContext.Response.Write("Product Add Success!"); } public void Execute(HttpContext context) { currentContext = context; // 默認Action名稱 string actionName = "index"; // 獲取Action名稱 if (!string.IsNullOrEmpty(context.Request["action"])) { actionName = context.Request["action"]; } switch (actionName.ToLower()) { case "index": this.Index(); break; case "add": this.Add(); break; default: this.Index(); break; } } }
2.4 新建一個ashx(一般處理程序),作為處理程序的入口
有了Controller之后,需要借助一個入口來指引請求到達指定Controller,所以這里我們實現一個最簡單的一般處理程序,它將url中的參數進行解析並實例化指定的Controller進行后續請求處理:

/// <summary> /// 模擬MVC程序的單一入口 /// </summary> public class Index : IHttpHandler { public void ProcessRequest(HttpContext context) { // 獲取Controller名稱 var controllerName = context.Request.QueryString["c"]; // 聲明IControoler接口-根據Controller Name找到對應的Controller IController controller = null; if (string.IsNullOrEmpty(controllerName)) { controllerName = "home"; } switch (controllerName.ToLower()) { case "home": controller = new HomeController(); break; case "product": controller = new ProductController(); break; default: controller = new HomeController(); break; } controller.Execute(context); } public bool IsReusable { get { return false; } } }
該一般處理程序接收http請求的兩個參數controller和action,並通過controller的參數名稱生成對應的Controller實例對象,將HttpContext對象作為參數傳遞給對應的Controller對象進行后續處理。
2.5 新建一個Global(全局處理程序),作為路由映射的入口
在Global.asax中有一個Application_BeginRequest的事件,它發生在每個Request開始處理之前,因此在這里我們可以進行一些類似於URL重寫的工作。解析URL當然也在這里進行,我們要做的就是將用戶輸入的類似於MVC形式的URL:http://www.xxx.com/home/index 進行正確的解析,將該請求交由HomeController進行處理。

public class Global : System.Web.HttpApplication { protected void Application_BeginRequest(object sender, EventArgs e) { #region 方式一:偽靜態方式實現路由映射服務 // 獲得當前請求的URL地址 var executePath = Request.AppRelativeCurrentExecutionFilePath; // 獲得當前請求的參數數組 var paraArray = executePath.Substring(2).Split('/'); // 如果沒有參數則執行默認配置 if (string.IsNullOrEmpty(executePath) || executePath.Equals("~/") || paraArray.Length == 0) { return; } string controllerName = "home"; if (paraArray.Length > 0) { controllerName = paraArray[0]; } string actionName = "index"; if (paraArray.Length > 1) { actionName = paraArray[1]; } // 入口一:單一入口 Index.ashx Context.RewritePath(string.Format("~/Index.ashx?controller={0}&action={1}", controllerName, actionName)); // 入口二:指定MvcHandler進行后續處理 //Context.RemapHandler(new MvcHandler()); #endregion } }
這里我們直接在代碼中hardcode了一個默認的controller和action名稱,分別是home和index。
可以看出,最后我們實際上做的就是解析URL,並通過重定向到Index.ashx進行所謂的Route路由工作。
2.6 運行吧偽MVC
(1)默認路由
(2)/home/add
(3)/product/index
三、改造DEMO:借助反射讓多態發光
3.1 在Global文件中模擬路由規則表
想想我們在ASP.NET MVC項目中是不是首先向程序注冊一些指定的路由規則,因此這里我們也在Global.asax中模擬一個路由規則表:
(1)增加一個靜態的路由規則集合
// 定義路由規則 private static IList<string> Routes;
(2)在Application_Start事件中注冊路由規則
protected void Application_Start(object sender, EventArgs e) { Routes = new List<string>(); // http://www.edisonchou.cn/controller/action Routes.Add("{controller}/{action}"); // http://www.edisonchou.cn/controller Routes.Add("{controller}"); }
(3)改寫Application_BeginRequest事件,使URL與路由規則進行匹配
protected void Application_BeginRequest(object sender, EventArgs e) { #region 方式二:模擬路由表實現映射服務 // 模擬路由字典 IDictionary<string, string> routeData = new Dictionary<string, string>(); // 將URL與路由表中每一條記錄進行匹配 foreach (var item in Routes) { var executePath = Request.AppRelativeCurrentExecutionFilePath;//// 獲得當前請求的參數數組 // 如果沒有參數則執行默認配置 if (string.IsNullOrEmpty(executePath) || executePath.Equals("~/")) { executePath += "/home/index"; } var executePathArray = executePath.Substring(2).Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); var routeKeys = item.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (executePathArray.Length == routeKeys.Length) { for (int i = 0; i < routeKeys.Length; i++) { routeData.Add(routeKeys[i], executePathArray[i]); } // 入口一:單一入口 Index.ashx //Context.RewritePath(string.Format("~/Index.ashx?c={0}&a={1}", routeData["{controller}"], routeData["{action}"])); // 入口二:指定MvcHandler進行后續處理 Context.RemapHandler(new MvcHandler(routeData)); // 只要滿足一條規則就跳出循環匹配 break; } } #endregion }
3.2 模擬ASP.NET管道工作,實現MvcHandler
在ASP.NET請求處理管道中,具體的處理工作都是轉交給了實現IHttpHandler接口的Handler對象進行處理。因此,這里我們也遵照這個規則,實現一個MvcHandler來代替剛才的Index.ashx來進行路由工作:

public class MvcHandler : IHttpHandler { // 路由表 private IDictionary<string, string> routeData; // 所有控制器的類型集合 private static IList<Type> alloctionControllerTypes; // 當前類第一次加載時調用靜態構造函數 static MvcHandler() { alloctionControllerTypes = new List<Type>(); // 獲得當前所有引用的程序集 var assemblies = BuildManager.GetReferencedAssemblies(); // 遍歷所有的程序集 foreach (Assembly assembly in assemblies) { // 獲取當前程序集中所有的類型 var allTypes = assembly.GetTypes(); // 遍歷所有的類型 foreach (Type type in allTypes) { // 如果當前類型滿足條件 if (type.IsClass && !type.IsAbstract && type.IsPublic && typeof(IController).IsAssignableFrom(type)) { // 將所有Controller加入集合 alloctionControllerTypes.Add(type); } } } } public MvcHandler(IDictionary<string, string> routeData) { this.routeData = routeData; } public void ProcessRequest(HttpContext context) { var controllerName = routeData["{controller}"]; if (string.IsNullOrEmpty(controllerName)) { // 指定默認控制器 controllerName = "home"; } IController controller = null; // 通過反射的方式加載具體實例 foreach (var controllerItem in alloctionControllerTypes) { if (controllerItem.Name.Equals(string.Format("{0}Controller", controllerName), StringComparison.InvariantCultureIgnoreCase)) { controller = Activator.CreateInstance(controllerItem) as IController; break; } } var requestContext = new HttpContextWrapper() { Context = context, RouteData = routeData }; controller.Execute(requestContext); } public bool IsReusable { get { throw new NotImplementedException(); } } }
上述代碼中需要注意以下幾點:
(1)在靜態構造函數中初始化所有Controller
// 路由表 private IDictionary<string, string> routeData; // 所有控制器的類型集合 private static IList<Type> alloctionControllerTypes; // 當前類第一次加載時調用靜態構造函數 static MvcHandler() { alloctionControllerTypes = new List<Type>(); // 獲得當前所有引用的程序集 var assemblies = BuildManager.GetReferencedAssemblies(); // 遍歷所有的程序集 foreach (Assembly assembly in assemblies) { // 獲取當前程序集中所有的類型 var allTypes = assembly.GetTypes(); // 遍歷所有的類型 foreach (Type type in allTypes) { // 如果當前類型滿足條件 if (type.IsClass && !type.IsAbstract && type.IsPublic && typeof(IController).IsAssignableFrom(type)) { // 將所有Controller加入集合 alloctionControllerTypes.Add(type); } } } }
此段代碼利用反射加載了所有實現了IController接口的Controller類,並存入了一個靜態集合alloctionControllerTypes里面,便於后面所有請求進行匹配。
(2)在ProcessRequest方法中再次利用反射動態創建Controller實例
public void ProcessRequest(HttpContext context) { var controllerName = routeData["{controller}"]; if (string.IsNullOrEmpty(controllerName)) { // 指定默認控制器 controllerName = "home"; } IController controller = null; // 通過反射的方式加載具體實例 foreach (var controllerItem in alloctionControllerTypes) { if (controllerItem.Name.Equals(string.Format("{0}Controller", controllerName), StringComparison.InvariantCultureIgnoreCase)) { controller = Activator.CreateInstance(controllerItem) as IController; break; } } var requestContext = new HttpContextWrapper() { Context = context, RouteData = routeData }; controller.Execute(requestContext); }
這里由於要使用到RouteData這個路由表的Dictionary對象,所以我們需要改寫一下傳遞的對象由原來的HttpContext類型轉換為自定義的包裝類HttpContextWrapper:
public class HttpContextWrapper { public HttpContext Context { get; set; } public IDictionary<string, string> RouteData { get; set; } }
可以看出,其實就是簡單地包裹了一下,添加了一個RouteData的路由表屬性。
當然,IController接口的方法定義也得隨之改一下:
public interface IController { void Execute(HttpContextWrapper context); }
至此,MvcHandler的代碼就寫完,我們可以總結一下它的主要流程:
3.3 改寫Controller匹配新接口
(1)HomeController

public class HomeController : IController { private HttpContext currentContext; public void Execute(HttpContextWrapper context) { currentContext = context.Context; // 獲取Action名稱 string actionName = "index"; if (context.RouteData.ContainsKey("{action}")) { actionName = context.RouteData["{action}"]; } switch (actionName.ToLower()) { case "index": this.Index(); break; case "add": this.Add(); break; default: this.Index(); break; } } // action 1 : Index public void Index() { currentContext.Response.Write("Home Index Success!"); } // action 2 : Add public void Add() { currentContext.Response.Write("Home Add Success!"); } }
(2)ProductController

public class ProductController : IController { private HttpContext currentContext; public void Execute(HttpContextWrapper context) { currentContext = context.Context; // 獲取Action名稱 string actionName = "index"; if (context.RouteData.ContainsKey("{action}")) { actionName = context.RouteData["{action}"]; } switch (actionName.ToLower()) { case "index": this.Index(); break; case "add": this.Add(); break; default: this.Index(); break; } } // action 1 : Index public void Index() { currentContext.Response.Write("Product Index Success!"); } // action 2 : Add public void Add() { currentContext.Response.Write("Product Add Success!"); } }
3.4 運行吧偽MVC
(1)默認路由
(2)/product/add
(3)/product
四、小結
本文首先回顧了一下MVC的關鍵概念,並從一個“純凈”的ASP.NET Web空項目開始一步一步構建一個類似於MVC的應用程序,通過單一處理入口的偽靜態方式與模擬路由表的方式進行了簡單地實現,並進行了測試。此次實驗,核心就在於獲取路由數據,指定處理程序,也就是理解並模擬路由機制。路由模塊就是一個很簡單的HttpModule(如果您對HttpModule不熟悉,請瀏覽我翻譯的一篇文章:ASP.NET應用程序和頁面生命周期),而ASP.NET MVC幫我們實現了UrlRoutingModule從而使我們輕松實現了路由機制,該機制獲取了路由數據,並制定處理程序(如MvcHandler),執行MvcHandler的ProcessRequest方法找到對應的Controller類型,最后將控制權交給對應的Controller對象,就相當於前台小妹妹幫你找到了面試官,你可以跟着面試官去進行相應的面試了(Actioin),希望你能得到好的結果(ActionResult)。
當然,這個DEMO還有很多需要改進的地方,仍然需要不斷的改進才能稱之為一個“框架”。第一個版本就到此,后續我會寫第二個版本,希望到時再寫一篇筆記來分享。
附件下載
MySimpleMvc : 點我下載