自定义 MVC 框架





5.00/5 (3投票s)
在本文中,我将展示如何将一个 ASP.NET Web Forms 应用程序转换为 MVC 框架。
引言
本文旨在提供关于如何从 .NET Web 应用程序开发 MVC 框架的教育性见解。它概述了开发 MVC 框架时需要考虑的一些关键因素以及我遇到的一些挑战。
通过引入 System.Web.Routing
命名空间,.NET Web Forms 中的路由已成为可能。此路由引擎允许开发人员将传入的 HTTP 请求与其物理文件解耦,从而可以使用更简洁的 URL。正是这个命名空间让我能够尝试将 Web Forms 应用程序转换为自定义 MVC 框架的想法。
路由
第一个挑战是将传入的 HTTP 请求路由到一个处理程序,并由该处理程序启动 MVC 分派过程。路由是在 Global.asax 中位于 Application_Start()
方法中完成的,该方法在应用程序首次启动时触发。下面的列表 1.1 显示了 Global.asax 的摘录。
.........
void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.Add(new Route("{*url}", new RouteHandler()));
}
如您在上面的代码中可以看到,已将单个路由添加到 RouteTable
集合中。路由 {*url} 表示所有 HTTP 请求都将由 RouteHandler
类处理。RouteHandler
实现 IRouteHandler
接口,并通过这样做实现 GetHttpHandler
方法。此方法必须返回一个 IHttpHandler
实例,该实例知道如何实际处理 HTTP 请求。下面的列表 1.2 显示了 RouteHandler
类中 GetHttpHandler
方法的摘录。
.........
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var page = new Mvc.MvcHandler();
return page as IHttpHandler;
}
请注意,正在创建一个新的 MvcHandler
实例并返回。现在我有了处理请求的方法,下一步是决定如何配置框架的使用。例如,我将如何告诉框架我想要使用的模板引擎类型。我曾考虑将这些可配置项放在 Web.config 文件中的 appSettings
部分,但在进一步考虑后,我决定创建一个 Bootstrap 类,该类将位于应用程序的根目录。
引导
我需要一种方法来配置我的应用程序,而无需使用 Web.config 文件。我决定使用一个 Bootstrap 类,该类将从 MVC 应用程序中始终存在的 MvcHandler
类实例化。您可以将其视为应用程序的主入口点。让我们看一下示例应用程序中提供的 Bootstrap 类。
using System;
using System.Web;
using Mvc;
using Mvc.Route;
public class Bootstrap
{
public void Load(HttpContext context)
{
Mvc.Application app = new Mvc.Application();
app.Module.Add("admin");
app.Route.Add(new StaticRoute("training.html", "default", "index", "training"));
app.Route.Add(new StaticRoute("prices.html", "default", "index", "prices"));
app.Route.Add(new StaticRoute("about.html", "default", "index", "about"));
app.Route.Add(new StaticRoute("contactus.html", "default", "index", "contactus"));
app.TemplateEngine = new Mvc.View.TemplateView(context.Server.MapPath("Application/"));
app.DebugController = false;
app.Dispatch(context);
}
}
上面的列表 1.3 显示了示例应用程序中完整的 Bootstrap 类。请注意,它有一个 Load()
方法,该方法接受一个参数。Load 方法由 MvcHandler
类调用,如前所述,该类负责实例化 Bootstrap 类。MvcHandler
类将其 HttpContext
对象传递给 Load 方法,从而允许我在 Bootstrap 类中使用存储在上下文中的 Request
和 Response
对象。
现在我有了入口点,我就可以开始加载应用程序,并且可以根据需要进行配置。
Application
引导阶段完成后,我必须就我的 MVC 框架的运行方式以及我想要实现的关键功能做出一些决定。对于第一个版本,我希望保持简单,因此我决定了以下主要功能。
- 静态/动态路由
- 模板化
- 按模块配置
- 自定义请求对象
静态/动态路由
在某些情况下,我可能希望路由一个 URL 并将其指向我选择的控制器/操作。例如,我可能希望路由以下 URL http://www.domain.com/aboutus.html,以便它映射到带有 Aboutus 操作的 Index 控制器。至少我需要三种类型的路由。一种是路由静态 URL,其中路径/查询精确匹配我指定的路由。
示例
http://www.domain.com/aboutus.html
路由:匹配“aboutus.html”,成功则映射到 **Index** 控制器,**About** 操作。
我还想要一个动态路由,以便我可以匹配 URL 路径/查询的一部分。匹配将基于正则表达式。任何匹配该模式的 URL 都将被映射到预定义的控制器和操作。
最后,我想要一个可以动态提取 URL 中的控制器/操作名称的路由。我还希望路由具有灵活性,以便在后续版本中可以添加其他类型的路由。为了实现这一点,我创建了如下所示的抽象 Route
类(列表 1.4)。
using System;
using Mvc.Http;
namespace Mvc.Route
{
public abstract class Route
{
private HttpRequest _request = null;
public HttpRequest Request
{
get { return this._request; }
set { this._request = value; }
}
public abstract bool ExecuteRoute();
}
}
上面的抽象 Route 类定义了一个抽象方法 ExecuteRoute
。当被派生类实现时,它必须返回一个布尔值,指示路由是否成功。它不需要知道 Controller/Action 类/方法是否存在。例如,我可以创建一个自定义路由,并在 ExecuteRoute()
方法中为 HttpRequest
_request
(一个自定义 HttpRequest 对象,稍后将详细介绍)对象设置 ModuleName
、ControllerName
和 ActonName
属性,然后返回 true,如下面的示例所示。
public override bool ExecuteRoute()
{
this.Request.Module = "Default";
this.Request.Controller = "Index";
this.Request.Action = "Index";
return true;
}
因为这个路由返回 true,我的 MVC 框架将始终尝试加载一个带有 Index 方法的 Index 类。这种方法的优点在于,我可以控制如何路由 URL。如果我正在开发一个博客,我可能希望将所有以 /Blog 开头的 URL 路由到预定义的控制器。
模板化
创建模板引擎本身不是一个小项目。最快的模板引擎是替换占位符变量与集合中存储的项目。这没什么新鲜的,并且该技术已在各种应用程序中使用,例如电子邮件模板。这种方法的缺点是我无法在模板中使用 C# 代码,因此控制语句和循环都不可能。
在进一步研究后,我发现存在各种模板引擎。这意味着我不必将模板引擎绑定到我的 MVC 框架,而是开发人员可以选择他们想要使用的模板引擎。为了提供这种灵活性,我需要一个模板引擎抽象类。当继承此类时,它将提供我的 MVC 框架理解如何加载和将变量传递给 View 所需的所有方法。下面的列表 1.5 显示了 MVC 框架中 TemplateEngine
类的摘录。
public abstract class TemplateEngine
{
private Mvc.Http.HttpRequest _request;
private System.Web.HttpResponse _response;
private string _module;
private string _controller;
private string _action;
private Dictionary<string,> _viewData = new Dictionary<string,object>();
public Mvc.Http.HttpRequest Request
{
get { return this._request; }
set { this._request = value; }
}
public System.Web.HttpResponse Response
{
get { return this._response; }
set { this._response = value; }
}
public string Module
{
get { return this._module; }
set { this._module = value; }
}
public string Controller
{
get { return this._controller; }
set { this._controller = value; }
}
public string Action
{
get { return this._action; }
set { this._action = value; }
}
public Dictionary<string,object> ViewData
{
get { return this._viewData; }
set { this._viewData = value; }
}
public abstract string Render();
}
只需要设置几个公共属性。ViewData 是一个键/值集合,它将被传递给 TemplateEngine
。大多数时候,这些数据将从操作方法内部传递。另外请注意,有一个抽象方法 Render()
。此方法必须返回一个发送到浏览器的输出字符串。例如,如果我使用第三方模板引擎,我将创建一个继承自抽象 TemplateEngine
类的自定义模板引擎类,并在 Render 方法中实现渲染第三方模板引擎所需的代码,以便它返回 View 输出。
分派控制器
到目前为止,我讨论了 MVC 框架所需的一些关键组件。现在是时候解释 Controller Dispatch 过程了。那么,让我们从 BootStrapping 阶段开始。在引导时,会创建一个 Mvc.Application
实例。在 Mvc.Application
实例配置完成后,必须调用 Mvc.Application
类的 Dispatch()
方法。它接受一个参数,即 HttpConext
的实例。调用 Dispatch 方法时,会发生以下情况。
将创建一个新的 Mvc.HttpRequest
实例。
Http.HttpRequest mvcRequest = new Http.HttpRequest(context);
虽然从上面的代码中不明显,但 HttpRequest
类实际上是 Mvc.Http
命名空间中的一个自定义类。它是自定义类的原因是,它可以提供额外的属性,例如 IsPost
、IsGet
来确定 HTTP 请求方法,以及获取 post 和 query 值的方法。
接下来,通过迭代路由集合来确定路由。请注意,在下面的示例代码中,将一个 UriRoute
实例添加到路由集合中。这是默认路由,它被添加到集合的末尾,因此它是最后被检查的路由,并且该路由始终返回 true。它返回 true 是因为,如果 URL 中不存在 module/controller/action 名称,则会使用默认的 module/controller/action 名称。
this.Route.Add(new UriRoute(this._modules));
foreach(Mvc.Route.Route route in this.Route)
{
route.Request = mvcRequest;
if (route.ExecuteRoute())
{
break;
}
}
确定路由后,mvcRequest
对象将保存 module/controller/action 名称。使用这些名称,可以动态创建类实例,其中类名是控制器名称,以及要调用的类上的方法是操作名称。
在尝试实例化控制器类之前,需要准备备用控制器。毕竟,如果一个 URL 指向一个不存在的控制器,会发生什么?请看下面的代码。
// Initalize controller meta data for three controllers.
// The front, custom error and default error controllers.
ControllerMetaData frontController = new ControllerMetaData();
frontController.Namespace = exeAssembly + ".Application." +
mvcRequest.Module + ".Controllers." + mvcRequest.Controller + "Controller";
frontController.Module = mvcRequest.Module;
frontController.Controller = mvcRequest.Controller;
frontController.Action = mvcRequest.Action;
ControllerMetaData customErrorController = new ControllerMetaData();
customErrorController.Namespace = exeAssembly + ".Application." +
mvcRequest.Module + ".Controllers.ErrorController";
customErrorController.Module = mvcRequest.Module;
customErrorController.Controller = "Error";
customErrorController.Action = "NotFound";
ControllerMetaData errorController = new ControllerMetaData();
errorController.Namespace = "Mvc.ErrorController";
errorController.Controller = "Error";
errorController.Action = "NotFound";
// Add The three controllers to a collection.
List<controllermetadata> controllers = new List<controllermetadata>();
controllers.Add(frontController);
controllers.Add(customErrorController);
controllers.Add(errorController);
请注意,在上面的代码中,我正在使用一个 ControllerMetaData
类来保存关于特定控制器的信息。第一个元对象保存有关前端控制器的详细信息,而后两个元对象保存有关错误控制器(自定义错误和系统错误)的详细信息。然后将元对象添加到 List
集合并进行迭代。如果找不到前端控制器,则会检查列表中的下一个控制器,这是一个自定义 ErrorController
,需要与前端控制器位于同一模块中。使用此自定义 ErrorController
,我可以自定义找不到前端控制器时出现的 404 响应。最后,如果没有自定义 ErrorController
,则使用我 MVC 框架中的默认 ErrorController
。
该过程的最后一部分涉及使用反射和 dynamic
类型尝试动态创建控制器实例。控制器必须继承自 Mvc.Controller
,这是一个抽象类,它为派生控制器类提供属性和方法。
抽象类 Mvc.Controller
有三个虚方法,它们从 Dispatch
方法调用。第一个被调用的方法是 Init()
方法,用于初始化控制器。接下来调用 Load()
方法。此方法很有用,如果您想为控制器中的每个操作运行一些代码,例如身份验证。在调用 Load()
方法之后,将调用 Action 方法,并将该方法的结果存储在一个变量中,该变量稍后将被发送到浏览器。如果 Action 方法想要向浏览器发送输出,它需要返回输出,或者使用 Response.Write()
方法。最后,在调用 Action 方法之后,将调用 Unload 方法,此方法对于处置对象和清理资源很有用。
Dispatch
方法还尝试为每个模块加载一个 Module.cs 类并调用一个名为 Load()
的方法。如果 Module.cs 文件存在并且有 Load()
方法,则在调用属于该模块的任何控制器时都会调用它。例如,我可以通过在该模块的 Load()
方法中添加代码来对模块进行身份验证,这样我就不必对该模块中的每个控制器进行身份验证。
此项目仍在进行中,我欢迎任何反馈。