本身動手寫一個簡單的MVC框架(初版)

1、MVC概念回顧

asp.net mvc

  路由(Route)、控制器(Controller)、行爲(Action)、模型(Model)、視圖(View)html

用一句簡單地話來描述以上關鍵點:面試

  路由(Route)就至關於一個公司的前臺小姐,她負責帶你(請求)找到跟你面試的面試官(控制器Controller),面試官可能會面試不一樣的職位(Action),你(請求)也會拿到不一樣的結果(ActionResult);數組

2、開始DEMO:單一處理程序入口

2.1 建立一個空白Web程序,移除全部默認引用

  不管是ASP.NET WebForms仍是ASP.NET MVC,他們都只是一個框架,是創建在System.Web之上的框架。爲了保證程序的純淨,咱們能夠將全部默認的引用都移除。固然,咱們仍是得保留幾個必要的dll引用:mvc

注意:這裏咱們並無引入System.Web.Mvc.dll,由於咱們要實現的就是一個簡單的MVC機制。app

2.2 模擬ASP.NET MVC,建立幾個MVC文件夾

  按照ASP.NET MVC的慣例添加Controllers、Models和Views文件夾(不是必須的):框架

2.3 新建一個Controller

  咱們首先在Controllers文件夾下新建一個接口,取名爲IController,它約定了全部Controller都必需要實現的方法:Executeasp.net

    public interface IController
    {
        void Execute(HttpContext context);
    }

  IController接口只定義了一個方法聲明,它接收一個HttpContext的上下文對象。ide

  有了接口,咱們就能夠實現具體的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;
            }
        }
    }
View Code

  (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;
            }
        }
    }
View Code

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;
            }
        }
    }
View Code

  該通常處理程序接收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
        }
    }
View Code

  這裏咱們直接在代碼中hardcode了一個默認的controller和action名稱,分別是home和index。

  能夠看出,最後咱們實際上作的就是解析URL,並經過重定向到Index.ashx進行所謂的Route路由工做。

2.6 運行吧僞MVC

  (1)默認路由

  (2)/home/add

  (3)/product/index

3、改造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();
            }
        }
    }
View Code

  上述代碼中須要注意如下幾點:

  (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!");
        }
    }
View Code

  (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!");
        }
    }
View Code

3.4 運行吧僞MVC

  (1)默認路由

  (2)/product/add

  (3)/product

4、小結

  本文首先回顧了一下MVC的關鍵概念,並從一個「純淨」的ASP.NET Web空項目開始一步一步構建一個相似於MVC的應用程序,經過單一處理入口的僞靜態方式與模擬路由表的方式進行了簡單地實現,並進行了測試。這次實驗,核心就在於獲取路由數據,指定處理程序,也就是理解並模擬路由機制。路由模塊就是一個很簡單的HttpModule(若是您對HttpModule不熟悉,請瀏覽我翻譯的一篇文章:ASP.NET應用程序和頁面生命週期),而ASP.NET MVC幫咱們實現了UrlRoutingModule從而使咱們輕鬆實現了路由機制,該機制獲取了路由數據,並制定處理程序(如MvcHandler),執行MvcHandler的ProcessRequest方法找到對應的Controller類型,最後將控制權交給對應的Controller對象,就至關於前臺小妹妹幫你找到了面試官,你能夠跟着面試官去進行相應的面試了(Actioin),但願你能獲得好的結果(ActionResult)。

  固然,這個DEMO還有不少須要改進的地方,仍然須要不斷的改進才能稱之爲一個「框架」。第一個版本就到此,後續我會寫第二個版本,但願到時再寫一篇筆記來分享。

附件下載

  MySimpleMvc : 點我下載

 

相關文章
相關標籤/搜索