深入研究 蒋金楠(Artech)老师的 MiniMvc(迷你 MVC),看看 MVC 内部到底是如何运行的

2014-04-05 13:52 by 自由的生活, 645 阅读, 2 评论, 收藏编辑

前言

跟我一起顺藤摸瓜剖析 Artech 老师的 MiniMVC 是如何运行的,了解它,我们就大体了解 ASP.NET MVC 是如何运行的了。既然是“顺藤摸瓜”,那我们就按照 ASP.NET 的执行顺序来反推代码。准备好了吗?Let's go!

解决方案大体结构

PS:原本很多代码没有注释,我按照自己的理解,增加了一些注释,希望能帮助您,共同提高,谢谢!

1. Global.asax 探究

ASP.NET 中的 Application_Start 方法一般是最先执行的,我们有必要知道当应用程序启动时到底发生了什么!

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using System.Web.Security;
  6. using System.Web.SessionState;
  7. using Artech.MiniMvc;
  8.  
  9. namespace WebApp
  10. {
  11. public class Global : System.Web.HttpApplication
  12. {
  13. protected void Application_Start(object sender, EventArgs e)
  14. {
  15. // 下面注册我们项目需要匹配的路由规则。ASP.NET Route 在接收到请求后,会把请求的
  16. // URL 和下面我们注册的路由规则相比较(可以理解为正则表达式匹配的原理), 最先
  17. // 匹配的规则(即 Route),就由该 Route 的 RouteHandler 来处理。所以注册路由
  18. // 很关键。
  19. RouteTable.Routes.Add("default_html", new RegexRoute { Url = "{controller}/{action}.html" });
  20.  
  21. // 注意:RegexRoute 类是本人扩展的,目的是替换原先的 Route 的匹配规则,以及增加一些
  22. // 默认值(controller 和 action)的实现。
  23. RouteTable.Routes.Add("default", new RegexRoute { Url = "{controller}/{action}", Defaults = new { controller = "Home", action = "Index" } });
  24.  
  25. // 下面是设置控制器工厂,MVC 内部仅仅只有一个实现了 IControllerFactory 的工厂
  26. ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory());
  27.  
  28. // 下面是给控制器工厂添加默认的命名空间,以便 MVC 在找控制器时查询速度会更快。
  29. ControllerBuilder.Current.DefaultNamespaces.Add("WebApp");
  30. }
  31. }
  32. }

RouteTable.Routes 是一个静态成员,保存着所有被注册的“路由”。RegexRoute 是我自定义的路由,下面会有介绍。

2. 查看自定义的 HttpModule

找到 Web.Config 文件。

  1. <?xml version="1.0"?>
  2. <configuration>
  3. <system.web>
  4. <compilation debug="false" targetFramework="4.0" />
  5. <httpModules>
  6. <!-- 下面是指定我们自定义的 Url 路由 Module -->
  7. <add name="UrlRoutingModule" type="Artech.MiniMvc.UrlRoutingModule, Artech.MiniMvc"/>
  8. </httpModules>
  9. </system.web>
  10. <system.webServer>
  11. <validation validateIntegratedModeConfiguration="false"/>
  12. <modules>
  13. <!-- 如果需要在 IIS7 以上的版本的集成模式下运行,则需要添加下面的注册 -->
  14. <add name="UrlRoutingModule" type="Artech.MiniMvc.UrlRoutingModule, Artech.MiniMvc"/>
  15. </modules>
  16. </system.webServer>
  17. </configuration>

我们看到注册了一个自定义的 HttpModule,那我们找到这个类去。

  1. /// <summary>
  2. /// Url 路由 Module
  3. /// </summary>
  4. public class UrlRoutingModule : IHttpModule
  5. {
  6. public void Dispose()
  7. {
  8.  
  9. }
  10. public void Init(HttpApplication context)
  11. {
  12. context.PostResolveRequestCache += OnPostResolveRequestCache;
  13. // 关于这个事件,官方摘要如下:
  14. // 在 ASP.NET 跳过当前事件处理程序的执行并允许缓存模块满足来自缓存的请求时发生。
  15. }
  16. protected virtual void OnPostResolveRequestCache(object sender, EventArgs e)
  17. {
  18. // 下面是得到当前请求的 Http 上下文
  19. HttpContext currentHttpContext = ((HttpApplication)(sender)).Context;
  20.  
  21. // 下面是一个包装类,把 HttpContext 转换成 HttpContextBase。如果你问我为什么
  22. // HttpApplication.Context 返回的是 HttpContext,而不是 HttpContextBase,我
  23. // 只能说一句:历史遗留问题
  24. HttpContextWrapper httpContext = new HttpContextWrapper(currentHttpContext);
  25.  
  26. // 下面是从当前 HttpContextBase 中获取 URL,把这个 URL 和应用程序启动(
  27. // Application_Start )中注册的静态路由表(RouteTable)相比较(也称匹配),
  28. // 第一个匹配的 RouteData 就立即返回,如果都没有匹配,则返回 NULL。
  29. RouteData routeData = RouteTable.Routes.GetRouteData(httpContext);
  30. if (null == routeData)
  31. {
  32. return;
  33. }
  34. RequestContext requestContext = new RequestContext
  35. {
  36. RouteData = routeData,
  37. HttpContext = httpContext
  38. };
  39. IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext);
  40. httpContext.RemapHandler(handler); // 用于为请求指定处理程序。
  41. }
  42. }

3. RouteTable.Routes.GetRouteData 探究

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

  1. /// <summary>
  2. /// 一个 Key-Value 集合,Key 表示路由名称,Value 表
  3. /// 示路由(包含 URL Pattern、路由处理程序等等)。
  4. /// 可以简单的理解它用于保存 Global.asax 中注册的值。
  5. /// </summary>
  6. public class RouteDictionary : Dictionary<string, RouteBase>
  7. {
  8. /// <summary>
  9. /// 下面是从当前 HttpContextBase 中获取 URL,把这个 URL 和应用程序启动(
  10. /// Application_Start )中注册的静态路由表(RouteTable)相比较(也称匹配),
  11. /// 第一个匹配的 RouteData 就立即返回,如果都没有匹配,则返回 NULL。
  12. /// </summary>
  13. /// <param name="httpContext">当前请求上下文</param>
  14. /// <returns></returns>
  15. public RouteData GetRouteData(HttpContextBase httpContext)
  16. {
  17. foreach (var route in this.Values)
  18. {
  19. RouteData routeData = route.GetRouteData(httpContext);
  20. if (null != routeData)
  21. {
  22. return routeData;
  23. }
  24. }
  25. return null;
  26. }
  27. }

我们看到是一个 RouteDictionary 类,继承自 Dictionary<string, RouteBase>。疑问来了, RouteBase 是什么?

4. RouteBase 探究

按 F12 进入 RouteBase 类。

  1. /// <summary>
  2. /// 路由抽象类
  3. /// </summary>
  4. public abstract class RouteBase
  5. {
  6. public abstract RouteData GetRouteData(HttpContextBase httpContext);
  7. }

我们看到是一个抽象类,包含一个抽象方法,意味着继承了这个 RouteBase 类的“非抽象”子类必须实现 GetRouteData 方法。疑问来了,返回的 RouteData 是什么?

5. RouteData 探究

按 F12 进入 RouteData 类。

  1. /// <summary>
  2. /// 路由数据,一般用于保存当前 HttpContext 下
  3. /// 获取的数据。
  4. /// </summary>
  5. public class RouteData
  6. {
  7. public IDictionary<string, object> Values { get; private set; }
  8. public IDictionary<string, object> DataTokens { get; private set; }
  9. public IRouteHandler RouteHandler { get; set; }
  10. public RouteBase Route { get; set; }
  11.  
  12. public RouteData()
  13. {
  14. this.Values = new Dictionary<string, object>();
  15. this.DataTokens = new Dictionary<string, object>();
  16. this.DataTokens.Add("namespaces", new List<string>());
  17. }
  18. /// <summary>
  19. /// 获取当前匹配的 Controller
  20. /// </summary>
  21. public string Controller
  22. {
  23. get
  24. {
  25. object controllerName = string.Empty;
  26. this.Values.TryGetValue("controller", out controllerName);
  27. return controllerName.ToString();
  28. }
  29. }
  30.  
  31. /// <summary>
  32. /// 获取当前匹配的 Action
  33. /// </summary>
  34. public string ActionName
  35. {
  36. get
  37. {
  38. object actionName = string.Empty;
  39. this.Values.TryGetValue("action", out actionName);
  40. return actionName.ToString();
  41. }
  42. }
  43. public IEnumerable<string> Namespaces
  44. {
  45. get
  46. {
  47. return (IEnumerable<string>)this.DataTokens["namespaces"];
  48. }
  49. }
  50. }

正如注释对 RouteData 的解释:路由数据,一般用于保存当前 HttpContext 下获取的数据。疑问来了,包含有一个 IRouteHandler 接口的 RouteHandler 是什么?

PS:我们可以简单的理解它就只是一个封装有信息的数据实体。

6. IRouteHandler 探究

按 F12 进入 IRouteHandler 接口。

  1. /// <summary>
  2. /// 路由处理程序的接口
  3. /// </summary>
  4. public interface IRouteHandler
  5. {
  6. IHttpHandler GetHttpHandler(RequestContext requestContext);
  7. }

其中的 IHttpHandler 是 .NET 原生类库中 System.Web 命名空间下的接口,而 RequestContext 类却不是,是自定义的类,它到底是什么?我们进去瞧瞧。

7. RequestContext 探究

按 F12 进入 RequestContext 类。

  1. /// <summary>
  2. /// 当前请求上下文,用于 MVC 调用链中的传递
  3. /// </summary>
  4. public class RequestContext
  5. {
  6. public virtual HttpContextBase HttpContext { get; set; }
  7. public virtual RouteData RouteData { get; set; }
  8. }

其中的 HttpContextBaser 是 .NET 原生类库中 System.Web 命名空间下的类。RouteData 类是我们自定义的类,记起来了么?上面有介绍的。

8. 回到 UrlRoutingModule 类。

回到 UrlRoutingModule 类。

  1. protected virtual void OnPostResolveRequestCache(object sender, EventArgs e)
  2. {
  3. // 下面是得到当前请求的 Http 上下文
  4. HttpContext currentHttpContext = ((HttpApplication)(sender)).Context;
  5.  
  6. // 下面是一个包装类,把 HttpContext 转换成 HttpContextBase。如果你问我为什么
  7. // HttpApplication.Context 返回的是 HttpContext,而不是 HttpContextBase,我
  8. // 只能说一句:历史遗留问题
  9. HttpContextWrapper httpContext = new HttpContextWrapper(currentHttpContext);
  10.  
  11. // 下面是从当前 HttpContextBase 中获取 URL,把这个 URL 和应用程序启动(
  12. // Application_Start )中注册的静态路由表(RouteTable)相比较(也称匹配),
  13. // 第一个匹配的 RouteData 就立即返回,如果都没有匹配,则返回 NULL。
  14. RouteData routeData = RouteTable.Routes.GetRouteData(httpContext);
  15. if (null == routeData)
  16. {
  17. return;
  18. }
  19. RequestContext requestContext = new RequestContext
  20. {
  21. RouteData = routeData,
  22. HttpContext = httpContext
  23. };
  24. IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext);
  25. httpContext.RemapHandler(handler); // 用于为请求指定处理程序。
  26. }

注意这段代码:

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

回想我们在 Application_Start 看到的 RouteTable.Routes.Add,恍然大悟,这不就是那请求的 URL 和系统中已经匹配的路由进行匹配么?哦,原来如此!

  1. /// <summary>
  2. /// 下面是从当前 HttpContextBase 中获取 URL,把这个 URL 和应用程序启动(
  3. /// Application_Start )中注册的静态路由表(RouteTable)相比较(也称匹配),
  4. /// 第一个匹配的 RouteData 就立即返回,如果都没有匹配,则返回 NULL。
  5. /// </summary>
  6. /// <param name="httpContext">当前请求上下文</param>
  7. /// <returns></returns>
  8. public RouteData GetRouteData(HttpContextBase httpContext)
  9. {
  10. foreach (var route in this.Values)
  11. {
  12. RouteData routeData = route.GetRouteData(httpContext);
  13. if (null != routeData)
  14. {
  15. return routeData;
  16. }
  17. }
  18. return null;
  19. }

注意:如果都没有匹配,会返回 NULL,用来表示没有找到匹配的项。PS:这需要调用端做是否为 NULL 判断。如果找到,继续回到 UrlRoutingModule 类。

9. 封装信息到 RequestContext

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

  1. RequestContext requestContext = new RequestContext
  2. {
  3. RouteData = routeData,
  4. HttpContext = httpContext
  5. };

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

10. 通过 RouteData 得到 HttpHandler

还记得通过路由匹配得到的 RouteData 吗?它有一个 RouteHandler 的属性,正是由它来得到 HttpHandler。

  1. IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext);
  2. httpContext.RemapHandler(handler); // 用于为请求指定处理程序。

你可能要问 RouteHandler 的实现类从何而来,是的,它通过 RouteBase 的实现类(也可以叫子类)扩展而来。

11. Route 探究

找到 Route 类。

  1. /// <summary>
  2. /// ASP.NET MVC 默认的路由实现
  3. /// </summary>
  4. public class Route : RouteBase
  5. {
  6. public IRouteHandler RouteHandler { get; set; }
  7. public Route()
  8. {
  9. this.DataTokens = new Dictionary<string, object>();
  10. this.RouteHandler = new MvcRouteHandler();
  11. }
  12. /// <summary>
  13. /// 从当前 URL 中提取路由的 Key-Value 并封装到 RouteData 中
  14. /// </summary>
  15. /// <param name="httpContext"></param>
  16. /// <returns></returns>
  17. public override RouteData GetRouteData(HttpContextBase httpContext)
  18. {
  19. IDictionary<string, object> variables;
  20. if (this.Match(httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2), out variables))
  21. {
  22. RouteData routeData = new RouteData();
  23. foreach (var item in variables)
  24. {
  25. routeData.Values.Add(item.Key, item.Value);
  26. }
  27. foreach (var item in DataTokens)
  28. {
  29. routeData.DataTokens.Add(item.Key, item.Value);
  30. }
  31. routeData.RouteHandler = this.RouteHandler;
  32. return routeData;
  33. }
  34. return null;
  35. }
  36.  
  37. public string Url { get; set; }
  38.  
  39. /// <summary>
  40. /// 默认值
  41. /// </summary>
  42. public object Defaults { get; set; }
  43.  
  44. public IDictionary<string, object> DataTokens { get; set; }
  45.  
  46. protected virtual bool Match(string requestUrl, out IDictionary<string,object> variables)
  47. {
  48. variables = new Dictionary<string,object>();
  49. string[] strArray1 = requestUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
  50. string[] strArray2 = this.Url.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
  51. if (strArray1.Length != strArray2.Length)
  52. {
  53. return false;
  54. }
  55.  
  56. for (int i = 0; i < strArray2.Length; i++)
  57. {
  58. if(strArray2[i].StartsWith("{") && strArray2[i].EndsWith("}"))
  59. {
  60. variables.Add(strArray2[i].Trim("{}".ToCharArray()),strArray1[i]);
  61. }
  62. }
  63. return true;
  64. }
  65.  
  66. private IDictionary<string, object> _defaultValueDictionary;
  67.  
  68. /// <summary>
  69. /// 默认值集合
  70. /// </summary>
  71. protected IDictionary<string, object> DefaultValueDictionary
  72. {
  73. get
  74. {
  75. if (_defaultValueDictionary != null)
  76. {
  77. return _defaultValueDictionary;
  78. }
  79. _defaultValueDictionary = new Dictionary<string, object>();
  80. if (Defaults != null)
  81. {
  82. foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(Defaults))
  83. {
  84. _defaultValueDictionary.Add(descriptor.Name, descriptor.GetValue(Defaults));
  85. }
  86. }
  87. return _defaultValueDictionary;
  88. }
  89. }
  90. }

我们看到 Route 实现了 RouteBase(抽象类)。

12. 自定义 RegexRoute 类

下面我们自定义一个 RegexRoute 类来玩玩。

  1. public class RegexRoute : Route
  2. {
  3. private List<string> _routeParameterList;
  4.  
  5. /// <summary>
  6. /// 得到当前注册的路由中的参数
  7. /// </summary>
  8. protected List<string> RouteParameterList
  9. {
  10. get
  11. {
  12. if (_routeParameterList != null)
  13. {
  14. return _routeParameterList;
  15. }
  16. Regex reg1 = new Regex(@"\{(.+?)\}", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
  17. MatchCollection matchCollection = reg1.Matches(this.Url);
  18. _routeParameterList = new List<string>();
  19. if (matchCollection.Count == 0)
  20. {
  21. return _routeParameterList;
  22. }
  23. foreach (Match matchItem in matchCollection)
  24. {
  25. string value = matchItem.Groups[1].Value;
  26. if (!string.IsNullOrEmpty(value))
  27. {
  28. _routeParameterList.Add(Regex.Escape(value));
  29. }
  30. }
  31. return _routeParameterList;
  32. }
  33. }
  34.  
  35. private string _urlRegexPattern;
  36.  
  37. protected string UrlRegexPattern
  38. {
  39. get
  40. {
  41. if (_urlRegexPattern != null)
  42. {
  43. return _urlRegexPattern;
  44. }
  45. _urlRegexPattern = Regex.Escape(this.Url).Replace("\\{", "{").Replace(@"\\}", "}");
  46. foreach (string param in RouteParameterList)
  47. {
  48. _urlRegexPattern = _urlRegexPattern.Replace("{" + param + "}", @"(?<" + param + ">.*?)");
  49. }
  50. _urlRegexPattern = "^" + _urlRegexPattern + "/?$";
  51. return _urlRegexPattern; // 比如: ^(?<controller>.*?)/(?<action>.*?)$
  52. }
  53. }
  54.  
  55. protected override bool Match(string requestUrl, out IDictionary<string, object> variables)
  56. {
  57. variables = new Dictionary<string, object>();
  58.  
  59. int tempIndex = requestUrl.IndexOf("?");
  60. if (tempIndex > -1)
  61. {
  62. if (tempIndex > 0)
  63. {
  64. requestUrl = requestUrl.Substring(0, tempIndex);
  65. }
  66. else
  67. {
  68. requestUrl = string.Empty;
  69. }
  70. }
  71. if (!requestUrl.EndsWith("/"))
  72. {
  73. requestUrl += "/";
  74. }
  75.  
  76. Regex routeRegex = new Regex(UrlRegexPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
  77. Match match = routeRegex.Match(requestUrl);
  78. if (!match.Success)
  79. {
  80. return false;
  81. }
  82.  
  83. foreach (string item in RouteParameterList)
  84. {
  85. string value = match.Groups[item].Value.ToLower();
  86. if (string.IsNullOrEmpty(value) && DefaultValueDictionary.ContainsKey(item))
  87. {
  88. value = DefaultValueDictionary[item].ToString();
  89. }
  90. variables.Add(item, value);
  91. }
  92. if (!variables.ContainsKey("controller"))
  93. {
  94. throw new HttpException("从当前路由中没有找到 controller.");
  95. }
  96. if (!variables.ContainsKey("action"))
  97. {
  98. throw new HttpException("从当前路由中没有找到 action.");
  99. }
  100. return true;
  101. }
  102. }

我们看到 RegexRoute 继承了 Route 类,并重写了它核心方法 Match 方法,用正则表达式来匹配 URL。

13. MvcRouteHandler 探究

还记得 Route 类的构造函数中指定了

  1. this.RouteHandler = new MvcRouteHandler();

吗?this.RouteHandler 的类型是 IRouteHandler 接口,意味着实现了 IRouteHandler 的子类都能赋值给 RouteHandler 属性。而 MVC 中 Route 默认的 RouteHandler(路由处理程序)是 MvcRouteHandler,下面我们按 F12 进去瞧瞧。

  1. /// <summary>
  2. /// Mvc 路由处理程序
  3. /// </summary>
  4. public class MvcRouteHandler: IRouteHandler
  5. {
  6. public IHttpHandler GetHttpHandler(RequestContext requestContext)
  7. {
  8. return new MvcHandler(requestContext);
  9. }
  10. }

我们看到 MvcRouteHandler 中的 GetHttpHandler 是返回一个 MvcHandler 类。并把 RequestContext(请求的上下文,上面我叫它“大箱子”的类)传给它。下面我们按 F12 进去瞧瞧 MvcHandler 是何方神圣。

14. MvcHandler 探究

进入 MvcHandler 。

  1. /// <summary>
  2. /// Mvc 处理程序
  3. /// </summary>
  4. public class MvcHandler: IHttpHandler
  5. {
  6. public bool IsReusable
  7. {
  8. get{return false;}
  9. }
  10. public RequestContext RequestContext { get; private set; }
  11. public MvcHandler(RequestContext requestContext)
  12. {
  13. this.RequestContext = requestContext;
  14. }
  15. public void ProcessRequest(HttpContext context)
  16. {
  17. // 下面是从当前请求上下文中获取控制器的名称
  18. string controllerName = this.RequestContext.RouteData.Controller;
  19. // 下面是得到 MVC 注册的控制器工厂
  20. IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
  21. // 下面是由控制器工厂生产出控制器
  22. IController controller = controllerFactory.CreateController(this.RequestContext, controllerName);
  23. if(controller == null)
  24. {
  25. throw new System.Web.HttpException(string.Format("无法找到名称为 \"{0}\" 的控制器!", controllerName));
  26. }
  27. // 执行控制器,以及控制器里面的 Action
  28. controller.Execute(this.RequestContext);
  29. }
  30. }

第一步是从当前请求上下文中获取控制器的名称。第二步调用了 ControllerBuilder.Current.GetControllerFactory(); 我们进去瞧瞧 ControllerBuilder 类。

15. ControllerBuilder 探究

按 F12 进入 ControllerBuilder 类。

  1. public class ControllerBuilder
  2. {
  3. // 静态字段。该字段在下面的静态构造函数中实例化
  4. public static ControllerBuilder Current { get; private set; }
  5.  
  6. private Func<IControllerFactory> factoryThunk;
  7.  
  8. public HashSet<string> DefaultNamespaces { get; private set; }
  9.  
  10. static ControllerBuilder()
  11. {
  12. Current = new ControllerBuilder();
  13. }
  14.  
  15. public ControllerBuilder()
  16. {
  17. this.DefaultNamespaces = new HashSet<string>();
  18. }
  19.  
  20. public IControllerFactory GetControllerFactory()
  21. {
  22. return factoryThunk();
  23. }
  24.  
  25. public void SetControllerFactory(IControllerFactory controllerFactory)
  26. {
  27. factoryThunk = () => controllerFactory;
  28. }
  29. }

我们看到 Current 是一个静态字段,DefaultNamespaces 属性是用来保存要查找的控制器的命名空间集合,可以通过配置获取,也可以通过反射获取。factoryThunk 是一个委托属性,当调用 GetControllerFactory() 方法时,都会调用一下这个委托,而非直接从静态属性中获取,保证每次都要重新执行。可以看到 IControllerFactory 是由 ControllerBuilder 获得的。而 IControllerFactory 则可以有多个实现。而 Artech 老师的 MiniMVC 则是通过在 Application_Start 中注册获得。

  1. protected void Application_Start(object sender, EventArgs e)
  2. {
  3. // 下面注册我们项目需要匹配的路由规则。ASP.NET Route 在接收到请求后,会把请求的
  4. // URL 和下面我们注册的路由规则相比较(可以理解为正则表达式匹配的原理), 最先
  5. // 匹配的规则(即 Route),就由该 Route 的 RouteHandler 来处理。所以注册路由
  6. // 很关键。
  7. RouteTable.Routes.Add("default_html", new RegexRoute { Url = "{controller}/{action}.html" });
  8.  
  9. // 注意:RegexRoute 是本人扩展的,目的是替换原先的 Route 的匹配规则,以及增加一些
  10. // 默认值(controller 和 action)的实现。
  11. RouteTable.Routes.Add("default", new RegexRoute { Url = "{controller}/{action}", Defaults = new { controller = "Home", action = "Index" } });
  12.  
  13. // 下面是设置控制器工厂,MVC 内部仅仅只有一个实现了 IControllerFactory 的工厂
  14. ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory());
  15.  
  16. // 下面是给控制器工厂添加默认的命名空间,以便 MVC 在找控制器时查询速度会更快。
  17. ControllerBuilder.Current.DefaultNamespaces.Add("WebApp");
  18. }

这里的 ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory()); 就是把 DefaultControllerFactory 类作为 IControllerFactory 接口的实现。

16. DefaultControllerFactory 探究

按 F12 进入 DefaultControllerFactory 类。

  1. /// <summary>
  2. /// 默认的控制器工厂。顾名思义,工厂是用来生产物品的,
  3. /// 对应在编程中,就是生成控制器的。
  4. /// </summary>
  5. public class DefaultControllerFactory : IControllerFactory
  6. {
  7. private List<Type> controllerTypes = new List<Type>();
  8. public DefaultControllerFactory()
  9. {
  10. // 下面是在当前应用下所有引用的程序集中找到 IController 的实现类
  11. var allAssemblies = BuildManager.GetReferencedAssemblies();
  12. foreach (Assembly assembly in allAssemblies)
  13. {
  14. foreach (Type type in assembly.GetTypes().Where(type => typeof(IController).IsAssignableFrom(type)))
  15. {
  16. controllerTypes.Add(type);
  17. }
  18. }
  19. }
  20. public IController CreateController(RequestContext requestContext, string controllerName)
  21. {
  22. string typeName = controllerName + "Controller";
  23. List<string> namespaces = new List<string>();
  24. namespaces.AddRange(requestContext.RouteData.Namespaces);
  25. namespaces.AddRange(ControllerBuilder.Current.DefaultNamespaces);
  26. foreach (var ns in namespaces)
  27. {
  28. string controllerTypeName = string.Format("{0}.{1}", ns, typeName);
  29. Type controllerType = controllerTypes.FirstOrDefault(type => string.Compare(type.FullName, controllerTypeName, true) == 0);
  30. if (null != controllerType)
  31. {
  32. return (IController)Activator.CreateInstance(controllerType);
  33. }
  34. }
  35. return null;
  36. }
  37. }

ControllerFactory 的作用是规范如何查找类,如何去创建 Controller 的实例。
我们看到 DefaultControllerFactory 的构造函数是在当前应用下所有引用的程序集中找到 IController 的实现类,并把这些实现类保存 List 集合中。IController 是什么?我们进去瞧瞧。

17. IController 探究

按 F12 进入 IController 接口。

  1. /// <summary>
  2. /// 控制器接口
  3. /// </summary>
  4. public interface IController
  5. {
  6. void Execute(RequestContext requestContext);
  7. }

我们看到 IController 就简简单单一个 Execute 方法,参数是 RequestContext 类(请求的上下文,上面我叫它“大箱子”的类)。至于 IController 的实现,是 ControllerBase 类。

18. ControllerBase 探究。

找到 ControllerBase 类。

  1. /// <summary>
  2. /// 控制器基类
  3. /// </summary>
  4. public abstract class ControllerBase : IController
  5. {
  6. protected IActionInvoker ActionInvoker { get; set; }
  7. public ControllerBase()
  8. {
  9. this.ActionInvoker = new ControllerActionInvoker();
  10. }
  11. public void Execute(RequestContext requestContext)
  12. {
  13. ControllerContext context = new ControllerContext { RequestContext = requestContext, Controller = this };
  14. string actionName = requestContext.RouteData.ActionName;
  15. // 下面是激活 Action,准备开始调用 Action
  16. this.ActionInvoker.InvokeAction(context, actionName);
  17. }
  18. }

我们所有定义的 Controller 都继承自 ControllerBase,这样就避免我们每次创建(新建)一个控制器都要手动地实现 IController,想想就比较麻烦。

19. 回到 DefaultControllerFactory

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

  1. /// <summary>
  2. /// 默认的控制器工厂。顾名思义,工厂是用来生产物品的,
  3. /// 对应在编程中,就是生成控制器的。
  4. /// </summary>
  5. public class DefaultControllerFactory : IControllerFactory
  6. {
  7. private List<Type> controllerTypes = new List<Type>();
  8. public DefaultControllerFactory()
  9. {
  10. // 下面是在当前应用下所有引用的程序集中找到 IController 的实现类
  11. var allAssemblies = BuildManager.GetReferencedAssemblies();
  12. foreach (Assembly assembly in allAssemblies)
  13. {
  14. foreach (Type type in assembly.GetTypes().Where(type => typeof(IController).IsAssignableFrom(type)))
  15. {
  16. controllerTypes.Add(type);
  17. }
  18. }
  19. }
  20. public IController CreateController(RequestContext requestContext, string controllerName)
  21. {
  22. string typeName = controllerName + "Controller";
  23. List<string> namespaces = new List<string>();
  24. namespaces.AddRange(requestContext.RouteData.Namespaces);
  25. // 从当前匹配的路由数据中得到命名空间(因为如果应用程序启动时给路由配置了命名空间)
  26. namespaces.AddRange(ControllerBuilder.Current.DefaultNamespaces);
  27. // 系统默认的命名空间中找
  28. foreach (var ns in namespaces)
  29. {
  30. string controllerTypeName = string.Format("{0}.{1}", ns, typeName);
  31. Type controllerType = controllerTypes.FirstOrDefault(type => string.Compare(type.FullName, controllerTypeName, true) == 0);
  32. if (null != controllerType)
  33. {
  34. return (IController)Activator.CreateInstance(controllerType);
  35. }
  36. }
  37. return null;
  38. }
  39. }

我们看到 MVC 默认会从当前 Web 程序集和被当前匹配的路由数据中得到命名空间(因为如果应用程序启动时给路由配置了命名空间),如果找到了,就创建实例,没有找到就返回 NULL。

20. 回到 MvcHandler

再次回到 MvcHandler 类。

  1. public void ProcessRequest(HttpContext context)
  2. {
  3. // 下面是从当前请求上下文中获取控制器的名称
  4. string controllerName = this.RequestContext.RouteData.Controller;
  5. // 下面是得到 MVC 注册的控制器工厂
  6. IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
  7. // 下面是由控制器工厂生产出控制器
  8. IController controller = controllerFactory.CreateController(this.RequestContext, controllerName);
  9. if(controller == null)
  10. {
  11. throw new System.Web.HttpException(string.Format("无法找到名称为 \"{0}\" 的控制器!", controllerName));
  12. }
  13. // 执行控制器,以及控制器里面的 Action
  14. controller.Execute(this.RequestContext);
  15. }

由于 Application_Start 中设置了

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

那么 MvcHandler 中得到 IControllerFactory 的一段代码可以等于

  1. IControllerFactory controllerFactory = new DefaultControllerFactory();

对于下一行代码,controllerFactory.CreateController 我们已经知道它是根据控制器名称从命名空间中找到相应的控制器,然后创建实例,如果没有找到,则返回 NULL。

最后一段代码,判断找到的控制器实例是否为 NULL,如果为 NULL,就抛出一个异常,提示 Web 访问者有错误。如果不为 NULL,就调用 Execute 方法。

21. 回到 ControllerBase

由于我们自定义的 Controller(比如:HomeController)要继承 ControllerBase,当 IController.Execute 时,其实当前程序会去执行 ControllerBase.Execute 方法。

  1. /// <summary>
  2. /// 控制器基类
  3. /// </summary>
  4. public abstract class ControllerBase: IController
  5. {
  6. protected IActionInvoker ActionInvoker { get; set; }
  7. public ControllerBase()
  8. {
  9. this.ActionInvoker = new ControllerActionInvoker();
  10. }
  11. public void Execute(RequestContext requestContext)
  12. {
  13. ControllerContext context = new ControllerContext { RequestContext = requestContext, Controller = this };
  14. string actionName = requestContext.RouteData.ActionName;
  15. // 下面是激活 Action,准备开始调用 Action
  16. this.ActionInvoker.InvokeAction(context, actionName);
  17. }
  18. }

我们看到第一步是实例化一个 ControllerContext 类,ControllerContext 类是什么呢?它其实和 RequestContext 类似,也是一个上下文类,用来传递封装的信息,好比一个比 RequestContext 还要大的“箱子”。

第二步是从路由数据中得到 Action 的名称,然后用 IActionInvoker 来调用。IActionInvoker 是什么?IActionInvoker 接口的默认实现类是 ControllerActionInvoker,那 ControllerActionInvoker 又是什么,我们进去瞧瞧。

22. IActionInvoker 探究

按 F12 进入 IActionInvoker 接口。

  1. /// <summary>
  2. /// 定义调用 Action 的规则,可以理解为 Action 调用器的接口
  3. /// </summary>
  4. public interface IActionInvoker
  5. {
  6. void InvokeAction(ControllerContext controllerContext, string actionName);
  7. }

我们看到 IActionInvoker 接口就定义了一个方法,就可以用来“调用”Action 方法。

23. ControllerActionInvoker 探究

按 F12 进入 ControllerActionInvoker 类。

  1. /// <summary>
  2. /// 默认的 Action 调用器的实现
  3. /// </summary>
  4. public class ControllerActionInvoker : IActionInvoker
  5. {
  6. /// <summary>
  7. /// 模型绑定实现
  8. /// </summary>
  9. public IModelBinder ModelBinder { get; private set; }
  10.  
  11. public ControllerActionInvoker()
  12. {
  13. this.ModelBinder = new DefaultModelBinder();
  14. }
  15. public void InvokeAction(ControllerContext controllerContext, string actionName)
  16. {
  17. // 下面是根据当前路由中的 Action 名字,反射当前获取的 Controller 的类型,并找到 Action Method
  18. MethodInfo method = controllerContext.Controller.GetType().GetMethods().First(m => string.Compare(actionName, m.Name, true) == 0);
  19. List<object> parameters = new List<object>();
  20. foreach (ParameterInfo parameter in method.GetParameters())
  21. {
  22. parameters.Add(this.ModelBinder.BindModel(controllerContext, parameter.Name, parameter.ParameterType));
  23. }
  24. ActionResult actionResult = method.Invoke(controllerContext.Controller, parameters.ToArray()) as ActionResult;
  25. if (actionResult != null)
  26. {
  27. actionResult.ExecuteResult(controllerContext);
  28. }
  29. }
  30. }

我们看到 InvokeAction 方法的第一步是反射刚刚创建完的 Controller 实例的所有公共方法(其实 MVC 内部远比 MiniMVC 要复杂,为了讲解,Artech 老师简化了。)取出名字为路由中的 ActionName 的方法。第二步则是反射这个 Action 方法,获取它所有的参数,然后把 HttpContext 中获取的 QueryString 集合、Forms 集合、Cookies 集合等等客户端提交的信息集合一同交给模型绑定 IModelBinder。那 IModelBinder 是什么?它的默认实现类 DefaultModelBinder 又是什么呢?我们进去瞧瞧。

24. IModelBinder 探究

按 F12 进入 IModelBinder 接口。

  1. /// <summary>
  2. /// 模型绑定规则,当准备开始调用 Action 时,
  3. /// 就会触发,即在 ControllerActionInvoker 中。
  4. /// </summary>
  5. public interface IModelBinder
  6. {
  7. object BindModel(ControllerContext controllerContext, string modelName, Type modelType);
  8. }

我们看到 IModelBinder 接口就定义了一个方法,就可以用来“绑定”模型给 Action 方法。

25. DefaultModelBinder 探究

按 F12 进入 DefaultModelBinder 类。

  1. /// <summary>
  2. /// 默认的模型绑定规则
  3. /// </summary>
  4. public class DefaultModelBinder : IModelBinder
  5. {
  6. public object BindModel(ControllerContext controllerContext, string modelName, Type modelType)
  7. {
  8. if (modelType.IsValueType || typeof(string) == modelType)
  9. {
  10. // 这里是值类型或者 String 类型的绑定规则
  11. object instance;
  12. if (GetValueTypeInstance(controllerContext, modelName, modelType, out instance))
  13. {
  14. return instance;
  15. };
  16. return Activator.CreateInstance(modelType);
  17. }
  18. object modelInstance = Activator.CreateInstance(modelType); // 复杂类型
  19. foreach (PropertyInfo property in modelType.GetProperties())
  20. {
  21. if (!property.CanWrite || (!property.PropertyType.IsValueType && property.PropertyType!= typeof(string)))
  22. {
  23. continue;
  24. }
  25. object propertyValue;
  26. if (GetValueTypeInstance(controllerContext, property.Name, property.PropertyType, out propertyValue))
  27. {
  28. property.SetValue(modelInstance, propertyValue, null);
  29. }
  30. }
  31. return modelInstance;
  32. }
  33.  
  34. /// <summary>
  35. /// 得到类型的实例
  36. /// </summary>
  37. /// <param name="controllerContext"></param>
  38. /// <param name="modelName"></param>
  39. /// <param name="modelType"></param>
  40. /// <param name="value"></param>
  41. /// <returns></returns>
  42. private bool GetValueTypeInstance(ControllerContext controllerContext,
  43. string modelName,
  44. Type modelType,
  45. out object value)
  46. {
  47. var form = HttpContext.Current.Request.Form;
  48. string key;
  49. if (null != form)
  50. {
  51. key = form.AllKeys.FirstOrDefault(k => string.Compare(k, modelName, true) == 0);
  52. if (key != null)
  53. {
  54. value = Convert.ChangeType(form[key], modelType);
  55. return true;
  56. }
  57. }
  58.  
  59. key = controllerContext.RequestContext.RouteData.Values
  60. .Where(item => string.Compare(item.Key, modelName, true) == 0)
  61. .Select(item => item.Key).FirstOrDefault();
  62. if (null != key)
  63. {
  64. value = Convert.ChangeType(controllerContext.RequestContext.RouteData.Values[key], modelType);
  65. return true;
  66. }
  67.  
  68. key = controllerContext.RequestContext.RouteData.DataTokens
  69. .Where(item => string.Compare(item.Key, modelName, true) == 0)
  70. .Select(item => item.Key).FirstOrDefault();
  71. if (null != key)
  72. {
  73. value = Convert.ChangeType(controllerContext.RequestContext.RouteData.DataTokens[key], modelType);
  74. return true;
  75. }
  76. value = null;
  77. return false;
  78. }
  79. }

我们看到 BindModel 方法内部是先判断是否是值类型或 String 类型,还是复杂类型(一般只类)。然后依次把 HttpContext 中获取的 QueryString 集合、Forms 集合、Cookies 集合等等客户端提交的信息集合筛选出 Key-Value ,最后创建实例,赋值。当然其实 MVC 内部的模型绑定比这里复杂的多,这里只是演示,感兴趣的朋友可以去研究 MVC 的源代码,这里暂时跳过。

26. 回到 ControllerActionInvoker

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

  1. public void InvokeAction(ControllerContext controllerContext, string actionName)
  2. {
  3. // 下面是根据当前路由中的 Action 名字,反射当前获取的 Controller 的类型,并找到 Action Method
  4. MethodInfo method = controllerContext.Controller.GetType().GetMethods().First(m => string.Compare(actionName, m.Name, true) == 0);
  5. List<object> parameters = new List<object>();
  6. foreach (ParameterInfo parameter in method.GetParameters())
  7. {
  8. parameters.Add(this.ModelBinder.BindModel(controllerContext, parameter.Name, parameter.ParameterType));
  9. }
  10. ActionResult actionResult = method.Invoke(controllerContext.Controller, parameters.ToArray()) as ActionResult;
  11. if (actionResult != null)
  12. {
  13. actionResult.ExecuteResult(controllerContext);
  14. }
  15. }

我们注意这一段代码:

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

这一步就是反射调用找到的 Action 方法,并尝试着把返回值转换成 ActionResult。如果返回值是 ActionResult,则调用它的 ExecuteResult 方法,否则什么都不做。ActionResult 是什么?我们进去瞧瞧。

27. ActionResult 探究

按 F12 进入 ActionResult 类。

  1. /// <summary>
  2. /// 抽象的 Action 返回的结果
  3. /// </summary>
  4. public abstract class ActionResult
  5. {
  6. public abstract void ExecuteResult(ControllerContext context);
  7. }

我们看到,它是一个抽象类,定义了一个抽象方法 ExecuteResult。联想到 MVC ,我们仿佛明白了 ViewResult、FileResult、FileStreamResult、JsonResult、JavascriptResult 等等继承了 ActionResult 的子类。

28. RawContentResult 探究

找到 RawContentResult 类。

  1. /// <summary>
  2. /// 原样输出结果
  3. /// </summary>
  4. public class RawContentResult : ActionResult
  5. {
  6. public string RawData { get; private set; }
  7. public RawContentResult(string rawData)
  8. {
  9. RawData = rawData;
  10. }
  11. public override void ExecuteResult(ControllerContext context)
  12. {
  13. context.RequestContext.HttpContext.Response.Write(this.RawData);
  14. }
  15. }
  16.  
  17. /// <summary>
  18. /// 原样输出结果
  19. /// </summary>
  20. public class RawContentResult : ActionResult
  21. {
  22. public string RawData { get; private set; }
  23. public RawContentResult(string rawData)
  24. {
  25. RawData = rawData;
  26. }
  27. public override void ExecuteResult(ControllerContext context)
  28. {
  29. context.RequestContext.HttpContext.Response.Write(this.RawData);
  30. }
  31. }

我们看到 RawContentResult 类的构造函数接收一个字符串,然后执行 ExecuteResult 时直接 Response.Write 它,还真够简单的,呵呵。同样我们可以任意扩展 ActionResult,来完成我们自己的特定功能,比如 XmlResult、JsonpResult 等等。

29. 运行效果

运行效果截图:

30. 最后

至此, Artech 老师的 MiniMVC 已经全部介绍完了,感觉自己收获了很多,也希望读者您也能有所提高,谢谢!

最后,再次感谢 Artech 老师!

我优化后的 MiniMVC 下载:点击这里

谢谢浏览!

 

作者:音乐让我说自由的生活 - 博客园
微博:http://weibo.com/liuzuliang 
推特:https://twitter.com/liuzuliang 
脸谱:http://facebook.com/liuzuliang0813 
出处:http://music.cnblogs.com/
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

 
分类: ASP.NET MVC

迷你 MVC的更多相关文章

  1. ASP.NET 学习小记 -- “迷你”MVC实现(2)

    Controller的激活 ASP.NET MVC的URL路由系统通过注册的路由表对HTTO请求进行解析从而得到一个用户封装路由数据的RouteData对象,而这个过程是通过自定义的UrlRoutin ...

  2. ASP.NET 学习小记 -- “迷你”MVC实现(1)

    ASP.NET 由于采用了管道式设计,具有很好的扩展性.整个ASP.NET MVC应用框架就是通过扩展ASP.NET实现的.通过ASP.NET的管道设计,我们知道,ASP.NET的扩展点主要是体现在H ...

  3. 深入研究 蒋金楠(Artech)老师的 MiniMvc(迷你 MVC),看看 MVC 内部到底是如何运行的

    前言 跟我一起顺藤摸瓜剖析 Artech 老师的 MiniMVC 是如何运行的,了解它,我们就大体了解 ASP.NET MVC 是如何运行的了.既然是“顺藤摸瓜”,那我们就按照 ASP.NET 的执行 ...

  4. (转)深入研究 蒋金楠(Artech)老师的 MiniMvc(迷你 MVC),看看 MVC 内部到底是如何运行的

    原文地址:http://www.cnblogs.com/Music/p/mini-mvc.html (含代码) ASP.NET MVC是如何运行的[1]: 建立在“伪”MVC框架上的Web应用 地址: ...

  5. (转)深入研究 蒋金楠(Artech)老师的 MiniMvc(迷你 MVC),看看 MVC 内部到底是如何运行的

    前言 跟我一起顺藤摸瓜剖析 Artech 老师的 MiniMVC 是如何运行的,了解它,我们就大体了解 ASP.NET MVC 是如何运行的了.既然是“顺藤摸瓜”,那我们就按照 ASP.NET 的执行 ...

  6. 【Head First Servlets and JSP】迷你MVC:JarDownload的完整实现

    1.首先,写一个download.html放至D:\apache-tomcat-7.0.77\webapps\JarDownload-v1. <!DOCTYPE HTML> <htm ...

  7. 学习“迷你ASP.NET MVC框架”后的小结

    看蒋老师MVC的书第二个大收获可以是算是看了这个迷你ASP.NET MVC框架了,虽然它远不如真正ASP.NET MVC(下文简称“MVC”)那么复杂庞大,但在迷你版中绕来绕去也够呛的.这部分我看了几 ...

  8. 自己动手写一个简单的MVC框架(第二版)

    一.ASP.NET MVC核心机制回顾 在ASP.NET MVC中,最核心的当属“路由系统”,而路由系统的核心则源于一个强大的System.Web.Routing.dll组件. 在这个System.W ...

  9. ASP.Net MVC开发基础学习笔记:五、区域、模板页与WebAPI初步

    一.区域—麻雀虽小,五脏俱全的迷你MVC项目 1.1 Area的兴起 为了方便大规模网站中的管理大量文件,在ASP.NET MVC 2.0版本中引入了一个新概念—区域(Area). 在项目上右击创建新 ...

随机推荐

  1. B/S 类项目改善

    B/S 类项目改善的一些建议   要分享的议题 性能提升:在访问量逐渐增大的同时,如何增大单台服务器的 PV2 上限,增加 TPS3 ? RESTful:相较于传统的 SOAP1,RESTful 风格 ...

  2. Unity3D 如何图形问题修正旋转模型已导入?

     如何纠正旋转模型被导入? 一些立体艺术资源包导出其模式,以便 Z 轴向上.Unity 大多数标准的脚本中假定的三维世界 Y 轴代表了.在 Unity 比改动脚本使其契合easy得多. Z 轴朝上 ...

  3. JavaScript中的分号插入机制

    原文:JavaScript中的分号插入机制 仅在}之前.一个或多个换行之后和程序输入的结尾被插入 也就是说你只能在一行.一个代码块和一段程序结束的地方省略分号. 也就是说你可以写如下代码 functi ...

  4. google面试题,男孩男女比例?

    Google面试题: 在一个重男轻女的国家里,每一个家庭都想生男孩.假设他们生的孩子是女孩.就再生一个,直到生下的是男孩为止,这种国家.男女比例会是多少? 答案:1:1 分析:  出生男女概率是50% ...

  5. Codeforces548D:Mike and Feet(单调栈)

    Mike is the president of country What-The-Fatherland. There are n bears living in this country besid ...

  6. HDU 1248 冰封王座(dp)

    Problem Description 不死巫妖王拉工资,死亡骑士得到N美元的钞票(记,只有一个纸币),战斗中频繁的死掉,他决定给自己买一些道具,于是他来到了地精商店前. 死亡骑士:"我要买 ...

  7. 关于Promise的一个案例

    题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次:如何让三个灯不断交替重复亮灯?(用Promise实现) 解答思路一: function red(){ console.log('red'); } ...

  8. linux_shell_类似sql的orderby 取最大值

    {} {} {} {} {} {} {} {} {} {} 需求场景 ,通过shell 筛选 每分钟的最大一条输出(最后一条) : .info | head

  9. java_eclipse_设置全局编码_utf-8_编译class指定_运行jar乱码解决_不依赖环境

    简述: javac时指定  编码 UTF-8   [ javac -encoding UTF-8 Test.java],运行时  java 指定编码 UTF-8 这样就不会出现乱码问题[ javac ...

  10. POJ2771_Guardian of Decency(二分图/最大独立集=N-最大匹配)

    解决报告 http://blog.csdn.net/juncoder/article/details/38159017 题目传送门 题意: 看到题目我就笑了.., 老师觉得这种两个学生不是一对: 身高 ...