自己动手写一个简单的MVC框架(第一版)
一、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 : 点我下载