深入研究 蔣金楠(Artech)老師的 MiniMvc(迷你 MVC),看看 MVC 內部到底是如何運行的


前言

跟我一起順藤摸瓜剖析 Artech 老師的 MiniMVC 是如何運行的,了解它,我們就大體了解 ASP.NET MVC 是如何運行的了。既然是“順藤摸瓜”,那我們就按照 ASP.NET 的執行順序來反推代碼。准備好了嗎?Let's go!

解決方案大體結構

PS:原本很多代碼沒有注釋,我按照自己的理解,增加了一些注釋,希望能幫助您,共同提高,謝謝!

1. Global.asax 探究

ASP.NET 中的 Application_Start 方法一般是最先執行的,我們有必要知道當應用程序啟動時到底發生了什么!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;
using Artech.MiniMvc;

namespace WebApp
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // 下面注冊我們項目需要匹配的路由規則。ASP.NET Route 在接收到請求后,會把請求的
            // URL 和下面我們注冊的路由規則相比較(可以理解為正則表達式匹配的原理), 最先
            // 匹配的規則(即 Route),就由該 Route 的 RouteHandler 來處理。所以注冊路由
            // 很關鍵。
            RouteTable.Routes.Add("default_html", new RegexRoute { Url = "{controller}/{action}.html" });

            // 注意:RegexRoute 類是本人擴展的,目的是替換原先的 Route 的匹配規則,以及增加一些
            // 默認值(controller 和 action)的實現。
            RouteTable.Routes.Add("default", new RegexRoute { Url = "{controller}/{action}", Defaults = new { controller = "Home", action = "Index" } });

            // 下面是設置控制器工廠,MVC 內部僅僅只有一個實現了 IControllerFactory 的工廠
            ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory());

            // 下面是給控制器工廠添加默認的命名空間,以便 MVC 在找控制器時查詢速度會更快。
            ControllerBuilder.Current.DefaultNamespaces.Add("WebApp");
        }
    }
}

RouteTable.Routes 是一個靜態成員,保存着所有被注冊的“路由”。RegexRoute 是我自定義的路由,下面會有介紹。

2. 查看自定義的 HttpModule

找到 Web.Config 文件。

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="false" targetFramework="4.0" />
    <httpModules>
      <!-- 下面是指定我們自定義的 Url 路由 Module -->
      <add name="UrlRoutingModule" type="Artech.MiniMvc.UrlRoutingModule, Artech.MiniMvc"/>
    </httpModules>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>
    <modules>
      <!-- 如果需要在 IIS7 以上的版本的集成模式下運行,則需要添加下面的注冊 -->
      <add name="UrlRoutingModule" type="Artech.MiniMvc.UrlRoutingModule, Artech.MiniMvc"/>
    </modules>
  </system.webServer>
</configuration>

我們看到注冊了一個自定義的 HttpModule,那我們找到這個類去。

/// <summary>
/// Url 路由 Module
/// </summary>
public class UrlRoutingModule : IHttpModule
{
    public void Dispose()
    { 
        
    }
    public void Init(HttpApplication context)
    {
        context.PostResolveRequestCache += OnPostResolveRequestCache;
        // 關於這個事件,官方摘要如下:
        // 在 ASP.NET 跳過當前事件處理程序的執行並允許緩存模塊滿足來自緩存的請求時發生。
    }
    protected virtual void OnPostResolveRequestCache(object sender, EventArgs e)
    {
        // 下面是得到當前請求的 Http 上下文
        HttpContext currentHttpContext = ((HttpApplication)(sender)).Context; 
        
        // 下面是一個包裝類,把 HttpContext 轉換成 HttpContextBase。如果你問我為什么
        // HttpApplication.Context 返回的是 HttpContext,而不是 HttpContextBase,我
        // 只能說一句:歷史遺留問題
        HttpContextWrapper httpContext = new HttpContextWrapper(currentHttpContext);

        // 下面是從當前 HttpContextBase 中獲取 URL,把這個 URL 和應用程序啟動(
        // Application_Start )中注冊的靜態路由表(RouteTable)相比較(也稱匹配),
        // 第一個匹配的 RouteData 就立即返回,如果都沒有匹配,則返回 NULL。
        RouteData routeData = RouteTable.Routes.GetRouteData(httpContext);
        if (null == routeData)
        {
            return;
        }
        RequestContext requestContext = new RequestContext 
        { 
            RouteData = routeData, 
            HttpContext = httpContext 
        };
        IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext);
        httpContext.RemapHandler(handler); // 用於為請求指定處理程序。
    }
}

3. RouteTable.Routes.GetRouteData 探究

按 F12 進入 RouteTable.Routes.GetRouteData 方法。

/// <summary>
/// 一個 Key-Value 集合,Key 表示路由名稱,Value 表
/// 示路由(包含 URL Pattern、路由處理程序等等)。
/// 可以簡單的理解它用於保存 Global.asax 中注冊的值。
/// </summary>
public class RouteDictionary : Dictionary<string, RouteBase>
{
    /// <summary>
    /// 下面是從當前 HttpContextBase 中獲取 URL,把這個 URL 和應用程序啟動(
    /// Application_Start )中注冊的靜態路由表(RouteTable)相比較(也稱匹配),
    /// 第一個匹配的 RouteData 就立即返回,如果都沒有匹配,則返回 NULL。
    /// </summary>
    /// <param name="httpContext">當前請求上下文</param>
    /// <returns></returns>
    public RouteData GetRouteData(HttpContextBase httpContext)
    {
        foreach (var route in this.Values)
        {
            RouteData routeData = route.GetRouteData(httpContext);
            if (null != routeData)
            {
                return routeData;
            }
        }
        return null;
    }
}

我們看到是一個 RouteDictionary 類,繼承自 Dictionary<string, RouteBase>。疑問來了, RouteBase 是什么?

4. RouteBase 探究

按 F12 進入 RouteBase 類。

/// <summary>
/// 路由抽象類
/// </summary>
public abstract class RouteBase
{
    public abstract RouteData GetRouteData(HttpContextBase httpContext);
}

我們看到是一個抽象類,包含一個抽象方法,意味着繼承了這個 RouteBase 類的“非抽象”子類必須實現 GetRouteData 方法。疑問來了,返回的 RouteData 是什么?

5. RouteData 探究

按 F12 進入 RouteData 類。

/// <summary>
/// 路由數據,一般用於保存當前 HttpContext 下
/// 獲取的數據。
/// </summary>
public class RouteData
{
    public IDictionary<string, object> Values { get; private set; }
    public IDictionary<string, object> DataTokens { get; private set; }
    public IRouteHandler RouteHandler { get;  set; }
    public RouteBase Route { get; set; }

    public RouteData()
    {
        this.Values = new Dictionary<string, object>();
        this.DataTokens = new Dictionary<string, object>();
        this.DataTokens.Add("namespaces", new List<string>());
    }
    /// <summary>
    /// 獲取當前匹配的 Controller
    /// </summary>
    public string Controller
    {
        get
        {
            object controllerName = string.Empty;
            this.Values.TryGetValue("controller", out controllerName);
            return controllerName.ToString();
        }
    }

    /// <summary>
    /// 獲取當前匹配的 Action
    /// </summary>
    public string ActionName
    {
        get
        {
            object actionName = string.Empty;
            this.Values.TryGetValue("action", out actionName);
            return actionName.ToString();
        }
    } 
    public IEnumerable<string> Namespaces
    {
        get
        {
            return (IEnumerable<string>)this.DataTokens["namespaces"];
        }
    } 
}

正如注釋對 RouteData 的解釋:路由數據,一般用於保存當前 HttpContext 下獲取的數據。疑問來了,包含有一個 IRouteHandler 接口的 RouteHandler 是什么?

PS:我們可以簡單的理解它就只是一個封裝有信息的數據實體。

6. IRouteHandler 探究

按 F12 進入 IRouteHandler 接口。

/// <summary>
/// 路由處理程序的接口
/// </summary>
public interface IRouteHandler
{
    IHttpHandler GetHttpHandler(RequestContext requestContext);
}

其中的 IHttpHandler 是 .NET 原生類庫中 System.Web 命名空間下的接口,而 RequestContext 類卻不是,是自定義的類,它到底是什么?我們進去瞧瞧。

7. RequestContext 探究

按 F12 進入 RequestContext 類。

/// <summary>
/// 當前請求上下文,用於 MVC 調用鏈中的傳遞
/// </summary>
public class RequestContext
{
    public virtual HttpContextBase HttpContext { get; set; }
    public virtual RouteData RouteData { get; set; }
}

其中的 HttpContextBaser 是 .NET 原生類庫中 System.Web 命名空間下的類。RouteData 類是我們自定義的類,記起來了么?上面有介紹的。

8. 回到 UrlRoutingModule 類。

回到 UrlRoutingModule 類。

protected virtual void OnPostResolveRequestCache(object sender, EventArgs e)
{
    // 下面是得到當前請求的 Http 上下文
    HttpContext currentHttpContext = ((HttpApplication)(sender)).Context; 
        
    // 下面是一個包裝類,把 HttpContext 轉換成 HttpContextBase。如果你問我為什么
    // HttpApplication.Context 返回的是 HttpContext,而不是 HttpContextBase,我
    // 只能說一句:歷史遺留問題
    HttpContextWrapper httpContext = new HttpContextWrapper(currentHttpContext);

    // 下面是從當前 HttpContextBase 中獲取 URL,把這個 URL 和應用程序啟動(
    // Application_Start )中注冊的靜態路由表(RouteTable)相比較(也稱匹配),
    // 第一個匹配的 RouteData 就立即返回,如果都沒有匹配,則返回 NULL。
    RouteData routeData = RouteTable.Routes.GetRouteData(httpContext);
    if (null == routeData)
    {
        return;
    }
    RequestContext requestContext = new RequestContext 
    { 
        RouteData = routeData, 
        HttpContext = httpContext 
    };
    IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext);
    httpContext.RemapHandler(handler); // 用於為請求指定處理程序。
}

注意這段代碼:

RouteData routeData = RouteTable.Routes.GetRouteData(httpContext);

回想我們在 Application_Start 看到的 RouteTable.Routes.Add,恍然大悟,這不就是那請求的 URL 和系統中已經匹配的路由進行匹配么?哦,原來如此!

/// <summary>
/// 下面是從當前 HttpContextBase 中獲取 URL,把這個 URL 和應用程序啟動(
/// Application_Start )中注冊的靜態路由表(RouteTable)相比較(也稱匹配),
/// 第一個匹配的 RouteData 就立即返回,如果都沒有匹配,則返回 NULL。
/// </summary>
/// <param name="httpContext">當前請求上下文</param>
/// <returns></returns>
public RouteData GetRouteData(HttpContextBase httpContext)
{
    foreach (var route in this.Values)
    {
        RouteData routeData = route.GetRouteData(httpContext);
        if (null != routeData)
        {
            return routeData;
        }
    }
    return null;
}

注意:如果都沒有匹配,會返回 NULL,用來表示沒有找到匹配的項。PS:這需要調用端做是否為 NULL 判斷。如果找到,繼續回到 UrlRoutingModule 類。

9. 封裝信息到 RequestContext

下面的代碼是把剛剛找到的 RouteData 和 HttpContext 一起打包到 RequestContext 中。

RequestContext requestContext = new RequestContext 
{ 
    RouteData = routeData, 
    HttpContext = httpContext 
};

就把 RequestContext 類理解成一個比 RouteData 和 HttpContext 都大的大箱子吧!

10. 通過 RouteData 得到 HttpHandler

還記得通過路由匹配得到的 RouteData 嗎?它有一個 RouteHandler 的屬性,正是由它來得到 HttpHandler。

IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext);
httpContext.RemapHandler(handler); // 用於為請求指定處理程序。

你可能要問 RouteHandler 的實現類從何而來,是的,它通過 RouteBase 的實現類(也可以叫子類)擴展而來。

11. Route 探究

找到 Route 類。

/// <summary>
/// ASP.NET MVC 默認的路由實現
/// </summary>
public class Route : RouteBase
{
    public IRouteHandler RouteHandler { get; set; }
    public Route()
    {
        this.DataTokens = new Dictionary<string, object>();
        this.RouteHandler = new MvcRouteHandler();
    }
    /// <summary>
    /// 從當前 URL 中提取路由的 Key-Value 並封裝到 RouteData 中
    /// </summary>
    /// <param name="httpContext"></param>
    /// <returns></returns>
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        IDictionary<string, object> variables;
        if (this.Match(httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2), out variables))
        {
            RouteData routeData = new RouteData();
            foreach (var item in variables)
            {
                routeData.Values.Add(item.Key, item.Value);
            }
            foreach (var item in DataTokens)
            {
                routeData.DataTokens.Add(item.Key, item.Value);
            }
            routeData.RouteHandler = this.RouteHandler;
            return routeData;
        }
        return null;
    }

    public string Url { get; set; }

    /// <summary>
    /// 默認值
    /// </summary>
    public object Defaults { get; set; }

    public IDictionary<string, object> DataTokens { get; set; }

    protected virtual bool Match(string requestUrl, out IDictionary<string,object> variables)
    {
        variables = new Dictionary<string,object>();
        string[] strArray1 = requestUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
        string[] strArray2 = this.Url.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
        if (strArray1.Length != strArray2.Length)
        {
            return false;
        }

        for (int i = 0; i < strArray2.Length; i++)
        { 
            if(strArray2[i].StartsWith("{") && strArray2[i].EndsWith("}"))
            {
                variables.Add(strArray2[i].Trim("{}".ToCharArray()),strArray1[i]);
            }
        }
        return true;
    }

    private IDictionary<string, object> _defaultValueDictionary;

    /// <summary>
    /// 默認值集合
    /// </summary>
    protected IDictionary<string, object> DefaultValueDictionary
    {
        get
        {
            if (_defaultValueDictionary != null)
            {
                return _defaultValueDictionary;
            }
            _defaultValueDictionary = new Dictionary<string, object>();
            if (Defaults != null)
            {
                foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(Defaults))
                {
                    _defaultValueDictionary.Add(descriptor.Name, descriptor.GetValue(Defaults));
                }
            }
            return _defaultValueDictionary;
        }
    }
}

我們看到 Route 實現了 RouteBase(抽象類)。

12. 自定義 RegexRoute 類

下面我們自定義一個 RegexRoute 類來玩玩。

public class RegexRoute : Route
{
    private List<string> _routeParameterList;

    /// <summary>
    /// 得到當前注冊的路由中的參數
    /// </summary>
    protected List<string> RouteParameterList
    {
        get
        {
            if (_routeParameterList != null)
            {
                return _routeParameterList;
            }
            Regex reg1 = new Regex(@"\{(.+?)\}", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
            MatchCollection matchCollection = reg1.Matches(this.Url);
            _routeParameterList = new List<string>();
            if (matchCollection.Count == 0)
            {
                return _routeParameterList;
            }
            foreach (Match matchItem in matchCollection)
            {
                string value = matchItem.Groups[1].Value;
                if (!string.IsNullOrEmpty(value))
                {
                    _routeParameterList.Add(Regex.Escape(value));
                }
            }
            return _routeParameterList;
        }
    }

    private string _urlRegexPattern;

    protected string UrlRegexPattern
    {
        get
        {
            if (_urlRegexPattern != null)
            {
                return _urlRegexPattern;
            }
            _urlRegexPattern = Regex.Escape(this.Url).Replace("\\{", "{").Replace(@"\\}", "}");
            foreach (string param in RouteParameterList)
            {
                _urlRegexPattern = _urlRegexPattern.Replace("{" + param + "}", @"(?<" + param + ">.*?)");
            }
            _urlRegexPattern = "^" + _urlRegexPattern + "/?$";
            return _urlRegexPattern; // 比如: ^(?<controller>.*?)/(?<action>.*?)$
        }
    }

    protected override bool Match(string requestUrl, out IDictionary<string, object> variables)
    {
        variables = new Dictionary<string, object>();

        int tempIndex = requestUrl.IndexOf("?");
        if (tempIndex > -1)
        {
            if (tempIndex > 0)
            {
                requestUrl = requestUrl.Substring(0, tempIndex);
            }
            else
            {
                requestUrl = string.Empty;
            }
        }
        if (!requestUrl.EndsWith("/"))
        {
            requestUrl += "/";
        }

        Regex routeRegex = new Regex(UrlRegexPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
        Match match = routeRegex.Match(requestUrl);
        if (!match.Success)
        {
            return false;
        }

        foreach (string item in RouteParameterList)
        {
            string value = match.Groups[item].Value.ToLower();
            if (string.IsNullOrEmpty(value) && DefaultValueDictionary.ContainsKey(item))
            {
                value = DefaultValueDictionary[item].ToString();
            }
            variables.Add(item, value);
        }
        if (!variables.ContainsKey("controller"))
        {
            throw new HttpException("從當前路由中沒有找到 controller.");
        }
        if (!variables.ContainsKey("action"))
        {
            throw new HttpException("從當前路由中沒有找到 action.");
        }
        return true;
    }
}

我們看到 RegexRoute 繼承了 Route 類,並重寫了它核心方法 Match 方法,用正則表達式來匹配 URL。

13. MvcRouteHandler 探究

還記得 Route 類的構造函數中指定了 

this.RouteHandler = new MvcRouteHandler(); 

嗎?this.RouteHandler 的類型是 IRouteHandler 接口,意味着實現了 IRouteHandler 的子類都能賦值給 RouteHandler 屬性。而 MVC 中 Route 默認的 RouteHandler(路由處理程序)是 MvcRouteHandler,下面我們按 F12 進去瞧瞧。

/// <summary>
/// Mvc 路由處理程序
/// </summary>
public class MvcRouteHandler: IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new MvcHandler(requestContext);
    }
}

我們看到 MvcRouteHandler 中的 GetHttpHandler 是返回一個 MvcHandler 類。並把 RequestContext(請求的上下文,上面我叫它“大箱子”的類)傳給它。下面我們按 F12 進去瞧瞧 MvcHandler 是何方神聖。

14. MvcHandler 探究

進入 MvcHandler 。

/// <summary>
/// Mvc 處理程序
/// </summary>
public class MvcHandler: IHttpHandler
{
    public bool IsReusable
    {
        get{return false;}
    }
    public RequestContext RequestContext { get; private set; }
    public MvcHandler(RequestContext requestContext)
    {
        this.RequestContext = requestContext;
    }
    public void ProcessRequest(HttpContext context)
    {
        // 下面是從當前請求上下文中獲取控制器的名稱
        string controllerName = this.RequestContext.RouteData.Controller;
        // 下面是得到 MVC 注冊的控制器工廠
        IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
        // 下面是由控制器工廠生產出控制器
        IController controller = controllerFactory.CreateController(this.RequestContext, controllerName);
        if(controller == null)
        {
            throw new System.Web.HttpException(string.Format("無法找到名稱為 \"{0}\" 的控制器!", controllerName));
        }
        // 執行控制器,以及控制器里面的 Action
        controller.Execute(this.RequestContext);
    }
}

第一步是從當前請求上下文中獲取控制器的名稱。第二步調用了 ControllerBuilder.Current.GetControllerFactory(); 我們進去瞧瞧 ControllerBuilder 類。

15. ControllerBuilder 探究

按 F12 進入 ControllerBuilder 類。

public class ControllerBuilder
{
    // 靜態字段。該字段在下面的靜態構造函數中實例化
    public static ControllerBuilder Current { get; private set; }

    private Func<IControllerFactory> factoryThunk;

    public HashSet<string> DefaultNamespaces { get; private set; }

    static ControllerBuilder()
    {
        Current = new ControllerBuilder();
    }

    public ControllerBuilder()
    {
        this.DefaultNamespaces = new HashSet<string>();
    }
    
    public IControllerFactory GetControllerFactory()
    {
        return factoryThunk();
    }

    public void SetControllerFactory(IControllerFactory controllerFactory)
    {
        factoryThunk = () => controllerFactory;
    }
}

我們看到 Current 是一個靜態字段,DefaultNamespaces 屬性是用來保存要查找的控制器的命名空間集合,可以通過配置獲取,也可以通過反射獲取。factoryThunk 是一個委托屬性,當調用 GetControllerFactory() 方法時,都會調用一下這個委托,而非直接從靜態屬性中獲取,保證每次都要重新執行。可以看到 IControllerFactory 是由 ControllerBuilder 獲得的。而 IControllerFactory 則可以有多個實現。而 Artech 老師的 MiniMVC 則是通過在 Application_Start 中注冊獲得。

protected void Application_Start(object sender, EventArgs e)
{
    // 下面注冊我們項目需要匹配的路由規則。ASP.NET Route 在接收到請求后,會把請求的
    // URL 和下面我們注冊的路由規則相比較(可以理解為正則表達式匹配的原理), 最先
    // 匹配的規則(即 Route),就由該 Route 的 RouteHandler 來處理。所以注冊路由
    // 很關鍵。
    RouteTable.Routes.Add("default_html", new RegexRoute { Url = "{controller}/{action}.html" });

    // 注意:RegexRoute 是本人擴展的,目的是替換原先的 Route 的匹配規則,以及增加一些
    // 默認值(controller 和 action)的實現。
    RouteTable.Routes.Add("default", new RegexRoute { Url = "{controller}/{action}", Defaults = new { controller = "Home", action = "Index" } });

    // 下面是設置控制器工廠,MVC 內部僅僅只有一個實現了 IControllerFactory 的工廠
    ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory());

    // 下面是給控制器工廠添加默認的命名空間,以便 MVC 在找控制器時查詢速度會更快。
    ControllerBuilder.Current.DefaultNamespaces.Add("WebApp");
}

這里的 ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory()); 就是把 DefaultControllerFactory 類作為 IControllerFactory 接口的實現。

16. DefaultControllerFactory 探究

按 F12 進入 DefaultControllerFactory 類。

/// <summary>
/// 默認的控制器工廠。顧名思義,工廠是用來生產物品的,
/// 對應在編程中,就是生成控制器的。
/// </summary>
public class DefaultControllerFactory : IControllerFactory
{
    private List<Type> controllerTypes = new List<Type>();
    public DefaultControllerFactory()
    {
        // 下面是在當前應用下所有引用的程序集中找到 IController 的實現類
        var allAssemblies = BuildManager.GetReferencedAssemblies();
        foreach (Assembly assembly in allAssemblies)
        {
            foreach (Type type in assembly.GetTypes().Where(type => typeof(IController).IsAssignableFrom(type)))
            {
                controllerTypes.Add(type);
            }
        }
    }
    public IController CreateController(RequestContext requestContext, string controllerName)
    {
        string typeName = controllerName + "Controller";
        List<string> namespaces = new List<string>();
        namespaces.AddRange(requestContext.RouteData.Namespaces);
        namespaces.AddRange(ControllerBuilder.Current.DefaultNamespaces);
        foreach (var ns in namespaces)
        {
            string controllerTypeName = string.Format("{0}.{1}", ns, typeName);
            Type controllerType = controllerTypes.FirstOrDefault(type => string.Compare(type.FullName, controllerTypeName, true) == 0);
            if (null != controllerType)
            {
                return (IController)Activator.CreateInstance(controllerType); 
            }
        }            
        return null;
    }
}

ControllerFactory 的作用是規范如何查找類,如何去創建 Controller 的實例。
我們看到 DefaultControllerFactory 的構造函數是在當前應用下所有引用的程序集中找到 IController 的實現類,並把這些實現類保存 List 集合中。IController 是什么?我們進去瞧瞧。

17. IController 探究

按 F12 進入 IController 接口。

/// <summary>
/// 控制器接口
/// </summary>
public interface IController
{
    void Execute(RequestContext requestContext);
}

我們看到 IController 就簡簡單單一個 Execute 方法,參數是 RequestContext 類(請求的上下文,上面我叫它“大箱子”的類)。至於 IController 的實現,是 ControllerBase 類。

18. ControllerBase 探究。

找到 ControllerBase 類。

/// <summary>
/// 控制器基類
/// </summary>
public abstract class ControllerBase : IController
{
    protected IActionInvoker ActionInvoker { get; set; }
    public ControllerBase()
    {
        this.ActionInvoker = new ControllerActionInvoker();
    }
    public void Execute(RequestContext requestContext)
    {
        ControllerContext context = new ControllerContext { RequestContext = requestContext, Controller = this };
        string actionName = requestContext.RouteData.ActionName;
        // 下面是激活 Action,准備開始調用 Action
        this.ActionInvoker.InvokeAction(context, actionName);
    }
}

我們所有定義的 Controller 都繼承自 ControllerBase,這樣就避免我們每次創建(新建)一個控制器都要手動地實現 IController,想想就比較麻煩。 

19. 回到 DefaultControllerFactory

回到 DefaultControllerFactory 類(注意 CreateController 方法)。

/// <summary>
/// 默認的控制器工廠。顧名思義,工廠是用來生產物品的,
/// 對應在編程中,就是生成控制器的。
/// </summary>
public class DefaultControllerFactory : IControllerFactory
{
    private List<Type> controllerTypes = new List<Type>();
    public DefaultControllerFactory()
    {
        // 下面是在當前應用下所有引用的程序集中找到 IController 的實現類
        var allAssemblies = BuildManager.GetReferencedAssemblies();
        foreach (Assembly assembly in allAssemblies)
        {
            foreach (Type type in assembly.GetTypes().Where(type => typeof(IController).IsAssignableFrom(type)))
            {
                controllerTypes.Add(type);
            }
        }
    }
    public IController CreateController(RequestContext requestContext, string controllerName)
    {
        string typeName = controllerName + "Controller";
        List<string> namespaces = new List<string>();
        namespaces.AddRange(requestContext.RouteData.Namespaces);
        // 從當前匹配的路由數據中得到命名空間(因為如果應用程序啟動時給路由配置了命名空間)
        namespaces.AddRange(ControllerBuilder.Current.DefaultNamespaces);
        // 系統默認的命名空間中找
        foreach (var ns in namespaces)
        {
            string controllerTypeName = string.Format("{0}.{1}", ns, typeName);
            Type controllerType = controllerTypes.FirstOrDefault(type => string.Compare(type.FullName, controllerTypeName, true) == 0);
            if (null != controllerType)
            {
                return (IController)Activator.CreateInstance(controllerType); 
            }
        }           
        return null;
    }
}

我們看到 MVC 默認會從當前 Web 程序集和被當前匹配的路由數據中得到命名空間(因為如果應用程序啟動時給路由配置了命名空間),如果找到了,就創建實例,沒有找到就返回 NULL。

20. 回到 MvcHandler

再次回到 MvcHandler 類。

public void ProcessRequest(HttpContext context)
{
    // 下面是從當前請求上下文中獲取控制器的名稱
    string controllerName = this.RequestContext.RouteData.Controller;
    // 下面是得到 MVC 注冊的控制器工廠
    IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
    // 下面是由控制器工廠生產出控制器
    IController controller = controllerFactory.CreateController(this.RequestContext, controllerName);
    if(controller == null)
    {
        throw new System.Web.HttpException(string.Format("無法找到名稱為 \"{0}\" 的控制器!", controllerName));
    }
    // 執行控制器,以及控制器里面的 Action
    controller.Execute(this.RequestContext);
}

由於 Application_Start 中設置了

ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory());

那么 MvcHandler 中得到 IControllerFactory 的一段代碼可以等於

IControllerFactory controllerFactory = new DefaultControllerFactory();

對於下一行代碼,controllerFactory.CreateController 我們已經知道它是根據控制器名稱從命名空間中找到相應的控制器,然后創建實例,如果沒有找到,則返回 NULL。

最后一段代碼,判斷找到的控制器實例是否為 NULL,如果為 NULL,就拋出一個異常,提示 Web 訪問者有錯誤。如果不為 NULL,就調用 Execute 方法。

21. 回到 ControllerBase

由於我們自定義的 Controller(比如:HomeController)要繼承 ControllerBase,當 IController.Execute 時,其實當前程序會去執行 ControllerBase.Execute 方法。

/// <summary>
/// 控制器基類
/// </summary>
public abstract class ControllerBase: IController
{
    protected IActionInvoker ActionInvoker { get; set; }
    public ControllerBase()
    {
        this.ActionInvoker = new ControllerActionInvoker();
    }
    public void Execute(RequestContext requestContext)
    {
        ControllerContext context = new ControllerContext { RequestContext = requestContext, Controller = this };
        string actionName = requestContext.RouteData.ActionName;
        // 下面是激活 Action,准備開始調用 Action
        this.ActionInvoker.InvokeAction(context, actionName);
    }
}

我們看到第一步是實例化一個 ControllerContext 類,ControllerContext 類是什么呢?它其實和 RequestContext 類似,也是一個上下文類,用來傳遞封裝的信息,好比一個比 RequestContext 還要大的“箱子”。

第二步是從路由數據中得到 Action 的名稱,然后用 IActionInvoker 來調用。IActionInvoker 是什么?IActionInvoker 接口的默認實現類是 ControllerActionInvoker,那 ControllerActionInvoker 又是什么,我們進去瞧瞧。

22. IActionInvoker 探究

按 F12 進入 IActionInvoker 接口。

/// <summary>
/// 定義調用 Action 的規則,可以理解為 Action 調用器的接口
/// </summary>
public interface IActionInvoker
{
    void InvokeAction(ControllerContext controllerContext, string actionName);
}

我們看到 IActionInvoker 接口就定義了一個方法,就可以用來“調用”Action 方法。

23. ControllerActionInvoker 探究

按 F12 進入 ControllerActionInvoker 類。

/// <summary>
/// 默認的 Action 調用器的實現
/// </summary>
public class ControllerActionInvoker : IActionInvoker
{
    /// <summary>
    /// 模型綁定實現
    /// </summary>
    public IModelBinder ModelBinder { get; private set; }

    public ControllerActionInvoker()
    {
        this.ModelBinder = new DefaultModelBinder();
    }
    public void InvokeAction(ControllerContext controllerContext, string actionName)
    {
        // 下面是根據當前路由中的 Action 名字,反射當前獲取的 Controller 的類型,並找到 Action Method
        MethodInfo method = controllerContext.Controller.GetType().GetMethods().First(m => string.Compare(actionName, m.Name, true) == 0);
        List<object> parameters = new List<object>();
        foreach (ParameterInfo parameter in method.GetParameters())
        {
            parameters.Add(this.ModelBinder.BindModel(controllerContext, parameter.Name, parameter.ParameterType));
        }
        ActionResult actionResult = method.Invoke(controllerContext.Controller, parameters.ToArray()) as ActionResult;
        if (actionResult != null)
        {
            actionResult.ExecuteResult(controllerContext);
        }
    }
}

我們看到 InvokeAction 方法的第一步是反射剛剛創建完的 Controller 實例的所有公共方法(其實 MVC 內部遠比 MiniMVC 要復雜,為了講解,Artech 老師簡化了。)取出名字為路由中的 ActionName 的方法。第二步則是反射這個 Action 方法,獲取它所有的參數,然后把 HttpContext 中獲取的 QueryString 集合、Forms 集合、Cookies 集合等等客戶端提交的信息集合一同交給模型綁定 IModelBinder。那 IModelBinder 是什么?它的默認實現類 DefaultModelBinder 又是什么呢?我們進去瞧瞧。

24. IModelBinder 探究

按 F12 進入 IModelBinder 接口。

/// <summary>
/// 模型綁定規則,當准備開始調用 Action 時,
/// 就會觸發,即在 ControllerActionInvoker 中。
/// </summary>
public interface IModelBinder
{
    object BindModel(ControllerContext controllerContext, string modelName, Type modelType);
}

我們看到 IModelBinder 接口就定義了一個方法,就可以用來“綁定”模型給 Action 方法。

25. DefaultModelBinder 探究

按 F12 進入 DefaultModelBinder 類。

/// <summary>
/// 默認的模型綁定規則
/// </summary>
public class DefaultModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, string modelName, Type modelType)
    {
        if (modelType.IsValueType || typeof(string) == modelType)
        {
            // 這里是值類型或者 String 類型的綁定規則
            object instance;
            if (GetValueTypeInstance(controllerContext, modelName, modelType, out instance))
            {
                return instance;
            };
            return Activator.CreateInstance(modelType);
        }
        object modelInstance = Activator.CreateInstance(modelType); // 復雜類型
        foreach (PropertyInfo property in modelType.GetProperties())
        {
            if (!property.CanWrite || (!property.PropertyType.IsValueType && property.PropertyType!= typeof(string)))
            {
                continue;
            }
            object propertyValue;
            if (GetValueTypeInstance(controllerContext, property.Name, property.PropertyType, out propertyValue))
            {
                property.SetValue(modelInstance, propertyValue, null);
            }
        }
        return modelInstance;
    }

    /// <summary>
    /// 得到類型的實例
    /// </summary>
    /// <param name="controllerContext"></param>
    /// <param name="modelName"></param>
    /// <param name="modelType"></param>
    /// <param name="value"></param>
    /// <returns></returns>
    private  bool GetValueTypeInstance(ControllerContext controllerContext, 
        string modelName, 
        Type modelType, 
        out object value)
    {
        var form = HttpContext.Current.Request.Form;
        string key;
        if (null != form)
        {
            key = form.AllKeys.FirstOrDefault(k => string.Compare(k, modelName, true) == 0);
            if (key != null)
            {
                value =  Convert.ChangeType(form[key], modelType);
                return true;
            }
        }

        key = controllerContext.RequestContext.RouteData.Values
            .Where(item => string.Compare(item.Key, modelName, true) == 0)
            .Select(item => item.Key).FirstOrDefault();
        if (null != key)
        {
            value = Convert.ChangeType(controllerContext.RequestContext.RouteData.Values[key], modelType);
            return true;
        }

        key = controllerContext.RequestContext.RouteData.DataTokens
            .Where(item => string.Compare(item.Key, modelName, true) == 0)
            .Select(item => item.Key).FirstOrDefault();
        if (null != key)
        {
            value = Convert.ChangeType(controllerContext.RequestContext.RouteData.DataTokens[key], modelType);
            return true;
        }
        value = null;
        return false;
    }
}

我們看到 BindModel 方法內部是先判斷是否是值類型或 String 類型,還是復雜類型(一般只類)。然后依次把 HttpContext 中獲取的 QueryString 集合、Forms 集合、Cookies 集合等等客戶端提交的信息集合篩選出 Key-Value ,最后創建實例,賦值。當然其實 MVC 內部的模型綁定比這里復雜的多,這里只是演示,感興趣的朋友可以去研究 MVC 的源代碼,這里暫時跳過。

26. 回到 ControllerActionInvoker

回到 ControllerActionInvoker 類。(注意 InvokeAction 方法)

public void InvokeAction(ControllerContext controllerContext, string actionName)
{
    // 下面是根據當前路由中的 Action 名字,反射當前獲取的 Controller 的類型,並找到 Action Method
    MethodInfo method = controllerContext.Controller.GetType().GetMethods().First(m => string.Compare(actionName, m.Name, true) == 0);
    List<object> parameters = new List<object>();
    foreach (ParameterInfo parameter in method.GetParameters())
    {
        parameters.Add(this.ModelBinder.BindModel(controllerContext, parameter.Name, parameter.ParameterType));
    }
    ActionResult actionResult = method.Invoke(controllerContext.Controller, parameters.ToArray()) as ActionResult;
    if (actionResult != null)
    {
        actionResult.ExecuteResult(controllerContext);
    }
}

我們注意這一段代碼:

ActionResult actionResult = method.Invoke(controllerContext.Controller, parameters.ToArray()) as ActionResult;

這一步就是反射調用找到的 Action 方法,並嘗試着把返回值轉換成 ActionResult。如果返回值是 ActionResult,則調用它的 ExecuteResult 方法,否則什么都不做。ActionResult 是什么?我們進去瞧瞧。

27. ActionResult 探究

按 F12 進入 ActionResult 類。

/// <summary>
/// 抽象的 Action 返回的結果
/// </summary>
public abstract class ActionResult
{        
    public abstract void ExecuteResult(ControllerContext context);
}

我們看到,它是一個抽象類,定義了一個抽象方法 ExecuteResult。聯想到 MVC ,我們仿佛明白了 ViewResult、FileResult、FileStreamResult、JsonResult、JavascriptResult 等等繼承了 ActionResult 的子類。

28. RawContentResult 探究

找到 RawContentResult 類。

/// <summary>
/// 原樣輸出結果
/// </summary>
public class RawContentResult : ActionResult
{
    public string RawData { get; private set; }
    public RawContentResult(string rawData)
    {
        RawData = rawData;
    }
    public override void ExecuteResult(ControllerContext context)
    {
        context.RequestContext.HttpContext.Response.Write(this.RawData);
    }
}

/// <summary>
/// 原樣輸出結果
/// </summary>
public class RawContentResult : ActionResult
{
    public string RawData { get; private set; }
    public RawContentResult(string rawData)
    {
        RawData = rawData;
    }
    public override void ExecuteResult(ControllerContext context)
    {
        context.RequestContext.HttpContext.Response.Write(this.RawData);
    }
}

我們看到 RawContentResult 類的構造函數接收一個字符串,然后執行 ExecuteResult 時直接 Response.Write 它,還真夠簡單的,呵呵。同樣我們可以任意擴展 ActionResult,來完成我們自己的特定功能,比如 XmlResult、JsonpResult 等等。

29. 運行效果

運行效果截圖:

30. 最后

至此, Artech 老師的 MiniMVC 已經全部介紹完了,感覺自己收獲了很多,也希望讀者您也能有所提高,謝謝!

最后,再次感謝 Artech 老師!

我優化后的 MiniMVC 下載:點擊這里

謝謝瀏覽!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM