从 Asp.Net MVC 到 Web Form 这看起来有点奇怪,大家都研究如何从 Web Form 到 MVC 的时候,为什么会出现一个相反的声音?从研究的角度来说,对反向过程进行研究有助于理解正向过程。通过对 MVC 转 Web Form 的研究,可以推导出:如果想把一个 Web Form 应用转换为 MVC 应用,可能需要进行怎么样的准备,应该从哪些方面去考虑重构?
当然研究不是我们最真实的目的,项目需要才是非常有力的理由——在我们用 MVC 框架已经初步完成项目第一阶段的时候准备试运行的时候,客户要求必须使用 Web Form——这不是客户的原因,只是我们前期调研得不够仔细。
产生这样的需求有很多历史原因,这不是今天要讨论的范围。我们要讨论的是如何快速的把 MVC 框架改回 Web Form 框架。要完成这个任务,需要做哪些事情?
- 在 Web Form 中 渲染 Razor 模板……如果不行,就得按 Razor 重写 Aspx
- 所有 Ajax 调用的 Controller 都必须改用 Ashx 来实现
- MVC 的路由配置得取消,URL 与原始的目录路径结构强相关
- 前端变化不大,但是要小心 Web Form 对元素 ID 和控件名称(name)的强制处理
Razor 框架 → Aspx 框架
很不幸,没找到现成的工具在 Web Form 框架中渲染 Razor 模板。所以这部分工作只是能手工完成了。幸好 Aspx 框架可以定义 Master 页面,而且 Master 可以嵌套,其它一些框架元素也可以在 aspx 框架中找到对应的元素来解决:
- layout 布局页 → Master 母板页
- cshtml 模板页 → aspx 页面
- @section → asp:ContentPlaceHolder
- @helper → ascx 控件
基于前后端分享的 MVC 框架没有用到 aspx 的事件机制,可以直接在 web.config 里禁用 ViewState,顺便设置 clientIDMode
为 Static
,免得 Web Form 乱改 ID 名称。
说起来轻松,但这部分工作需要大量的人工操作,所以其实是最累也最容易出错的。
移植 Controller
Controller 是 MVC 中的概念,但实际上可以把 Controller 看作是一个 Action 的集合,而 Action 在 RPC 的概念中对应于过程(Procedure)名称以及对应的参数定义。
由于前面对 Razor 的移植,所有返回 View()
的 Action 都被换成了 .aspx
页面访问。所以先把这部分 Action 从 Controller 中剔除掉。剩下的大部分是返回 JsonNetResult
的 Action,用于 Ajax 调用。现在不得不庆幸没有使用 RESTful 风格,完全不用担心 HTTP Method 的处理。
RESTful 很好,但不要迷信它,这种风格并不适应所有场景,有兴趣可以看看 oschina 上的一篇协同翻译文章 理解面向 HTTP API 的 REST 和 RPC
可能有些人能猜测到 JsonNetResult
是个什么东西,不过我觉得还是有必要说一下
介绍 JsonNetResult
MVC API Controller 使用了 Newtonsoft Json.Net 来实现 JsonResult
(System.Web.Http.Results.JsonResult
,在 System.Web.Http.dll 中)。而普通 Controller 是用微软自己的 JavaScriptSerializer 来实现的的 JsonResult
(System.Web.Mvc.JsonResult
,在 System.Web.Mvc.dll 中)。因为 JavaScriptSerializer 不如 Json.Net 好用,所以在写普通的 MVC Controller 的时候,会用 Json.Net 自己实现一个 JsonNetResult
,在网上有很多实现,下面也会有一段类似的代码,所以就不贴了。
入口
在 MVC 中,路由系统可以找到指定的 Controller 和 Action,但在 Web Form 中没有路由系统,自己写个 HttpModule 是可以实现,不过工作量不小。既然剩下的几乎都是请求数据的 HTTP API,比较合适的选择是 IHttpHandler,即 ashx 页面。
只需要定义一个 Do.ashx,通过参数指定 Controller 和 Action,把 Do.ashx 作为所有 Ajax 及类似请求的入口。
有了入口,还得模拟 MVC 对 Controller 和 Action 的处理。这里有几个关键点需要注意:
- 所有 Action 返回的是一个
ActionResult
,由框架处理ActionResult
对象来向 Response 进行输出。 - Action 的参数会由 MVC 框架根据名称来解析
如果这些要点没处理好,Controller 就得进行结构上的变更。下面会根据这两个要点来介绍 ActionResult 、Controller 和 Do.ashx 的实现,它们也是本文的重点。
Controller 基类
所有的 Controller 都从基类 Controller
继承,看起来它很重要。但实际上 Controller
基类只是提供了一些工作方法,为所有 Controller 提供了统一扩展的基础。而所有重要的事情,都不是在这里面完成的。
参数的解析和自动赋值是在 Do.ashx 中完成的,当然,这个功能很重要,所以写了一些类来实现;业务过程是在它的子类中完成的;结果处理则是在 ActionResult 中完成的。把它们组合在一起,这才是 Controller 干的事情,而它必须要做的,就是提供一个基类,仅此而已。
IActionResult 和 ActionResult
从网上找到的 JsonNetResult 实现代码,基本上可以了解到,ActionResult 最终会通过 ExecuteResult(HttpContext)
方法将自身保存的参数或者数据,进行一定的处理之后,输出到 HttpContext.Response
对象。所以 IActionResult
接口比如简单,而 ActionResult
就是一个默认实现。
public interface IActionResult
{
void ExecuteResult(HttpContext context);
}
不过重要的不是 IActionResult
和 ActionResult
,而是具体的实现。从原有的程序功能来看,至少需要实现:
-
JsonNetResult
,用于输出 JSON 结果 -
HttpStatsResult
,用于输出指定的 Http 状态,比如 403 -
HttpNotFoundResult
,用于输出 404 状态 -
FileResult
,这是下载文件要用到的
JsonNetResult
这是最主要使用的一个 Result。它主要是设置 ContentType 为 "application/json"
,默认编码 UTF-8
,然后就是用 Json.Net 将数据对象处理成 JSON 输出到 Response。
public class JsonNetResult : IActionResult
{
private const string DEFAULT_CONTENT_TYPE = "application/json";
// 指定 Response 的编码,未指定则使用全局指定的那个(UTF-8)
public Encoding ContentEncoding { get; set; }
// ContentType,未设置则使用 DEFAULT_CONTENT_TYPE
public string ContentType { get; set; }
// 保存要序列化成 JSON 的数据对象
public object Data { get; set; }
public JsonNetResult()
{
Settings = JsonConvert.DefaultSettings();
}
// 为当前的 Json 序列化准备一个配置对象,
// 如果有特殊需要,可以修改其配置项,不会影响全局配置
public JsonSerializerSettings Settings { get; private set; }
public void ExecuteResult(HttpContext context)
{
HttpResponse response = context.Response;
if (ContentEncoding != null)
{
response.ContentEncoding = ContentEncoding;
}
if (Data == null)
{
return;
}
response.ContentType = string.IsNullOrEmpty(ContentType)
? DEFAULT_CONTENT_TYPE
: ContentType;
var scriptSerializer = JsonSerializer.Create(Settings);
// Serialize the data to the Output stream of the response
scriptSerializer.Serialize(response.Output, Data);
response.Flush();
// response.End() 加了会在后台抛一个异常,所以把它注释掉了
// response.End();
}
}
HttpStatusResult 和 HttpNotFoundResult
HttpNotFoundResult
其实就是 HttpStatusResult
的一个特例,所以只需要实现 HttpStatusResult
再继承一个 HttpNotFoundResult
出来就好
HttpStatusResult
最主要的是需要一个代码,StatusCode,像 404 啊,403 啊,505 啊之类的。另外 IIS 实现了子状态,所以还有一个子状态码 SubStatusCode。剩下的就是一个消息了,都不是必须的属性。实现起来非常简单
public class HttpStatusResult : IActionResult
{
public int StatusCode;
public int SubStatusCode;
public string Status;
public string StatusDescription { get; set; }
public HttpStatusResult(int statusCode, string status = null)
{
StatusCode = statusCode;
Status = status;
}
public void ExecuteResult(HttpContext context)
{
var response = context.Response;
response.StatusCode = StatusCode;
response.SubStatusCode = SubStatusCode;
response.Status = Status ?? response.Status;
response.StatusDescription = StatusDescription ?? response.StatusDescription;
response.End();
}
}
public sealed class HttpNotFoundResult : HttpStatusResult, IActionResult
{
public HttpNotFoundResult()
: base(404, "404 Resource not found")
{
}
}
FileResult
对于文件来说,有三个主要的属性:MIME、文件流和文件名。配置好 Response 的头之后,简单的把文件流拷贝到 Response 的输出流就解决问题
public class FileResult : IActionResult
{
const string DEFAULT_CONTENT_TYPE = "application/octet-stream";
public string ContentType { get; set; }
readonly string filename;
readonly Stream stream;
public FileResult(Stream stream, string filename = null)
{
this.filename = filename;
this.stream = stream;
}
public void ExecuteResult(HttpContext context)
{
var response = context.Response;
response.ContentType = string.IsNullOrEmpty(ContentType)
? DEFAULT_CONTENT_TYPE
: ContentType;
if (!string.IsNullOrEmpty(filename))
{
response.AddHeader("Content-Disposition",
string.Format("attachment; filename=\"{0}\"", filename));
}
response.AddHeader("Content-Length", stream.Length.ToString());
stream.CopyTo(response.OutputStream);
stream.Dispose();
response.End();
}
}
Do.ashx
上面已经提到了 Do.ashx 是一个入口,它的首要工作是选择正确的 Controller 和 Action。Action 的指定是通过参数实现的,我们得定义一个特别的参数,思考再三,将参数名定义为 $
,因为它够特殊,而且比 action
或者 _action
短。而这个参数的值,就延用 MVC 中路由的结构 /controller/action/id
。
幸好原来路由结构就不复杂,不然解析函数就难写了。
MVC 框架中有一个 ActionDescriptor
类保存了 Controller 和 Action 的信息。所以我们模拟一个 ActoinDescriptor
,然后 Do.ashx 就只需要对每次请求生成一个 ActionDescriptor
对象,让它来解析参数,选择 Controller 和 Action,再调用找到的 Action,处理结果……明白了吧,它才是真正的调度中心!
ActionDescriptor
要干的第一件事就是解析 $
参数。因为在 Controller 和 Action 不明确之后,ActionDescriptor
对象就没必要存在,所以我们定义了一个静态方法:
static ActionDescriptor Parse(string action)
幸好我们原来的路由定义得并不复杂,所以这里的解析函数也可以写得很简单,只是按分隔符 /
拆成几段分别赋值给新对象的 Controller
、Action
和 Id
属性就好。
internal static ActionDescriptor Parse(string action)
{
if (string.IsNullOrWhiteSpace(action))
{
return null;
}
var parts = action
.Trim('/', ' ')
.Split(SPLITERS, StringSplitOptions.RemoveEmptyEntries);
return new ActionDescriptor {
Controller = parts[0],
Action = parts.Length > 1 ? parts[1] : "index",
Id = parts.Length > 2 ? parts[2] : null
};
}
Router 反射工具类
虽然没有路由系统,但是上面得到了 Controller
和 Action
这两个名称之后,还需要找到对应的 Controller 类,以及对应于 Action 的方法——这一些都需要用反射来完成。
Router
就是定义来干这个事情,所以它是一个反射工具类。它所做的事情,只是把类和方法找出来,即一个 Type
对象,一个 MethodInfo
对象。
Router
类有 60 多行代码,不算大也不算小。限于篇幅,代码我就不准备贴了,因为它干的事情实在很简单,只要有反射的基础知识,写出来也就是分分钟的事情。
ActionDescriptor.Do(HttpContext)
Router
把 Controller 的类,一个 Type
对象,以及 Action 对应的方法,一个 MethodInfo
对象找出来之后,还需要实例化并对实例调用方法,得到一个 IActionResult
,再调用它的 ExecuteResult(HttpContext)
方法将结果输出到 Response。
这一整个过程就是 ActionDescriptor.Do()
干的事情,非常清晰也非常简单。用伪代码描述出来就是
var tuple = Router.Get(controllerName, actionName);
// tuple.Item1 是 Type 对象
// tuple.Item2 是 MethodInfo 对象
var instance = Activator.CreateInstance(tuple.Item1);
var result = method.Invoke(c, GetArguments(method, context.Request));
if (typeof(IActionResult).IsAssignableFrom(result.GetType()))
{
((IActionResult)result).ExecuteResult(context);
}
else
{
// 如果返回的不是 IActionResult,当作 JsonNetResult 的数据来处理
// 这样相当于扩展了 Action,可以直接返回需要序列化成 JSON 的数据对象
new JsonNetResult
{
Data = result
}.ExecuteResult(context);
}
等一等,发现身份不明的东东——GetArguments()
这是干啥用的?
object[] GetArguments(MethodInfo, HttpRequest)
从签名就可以猜测 GetArguments()
要分析 Action 对应方法的参数定义,然后从 Reqeust 中取值,返回一个与 Action 方法参数定义一一对应的参数值列表(数组)……也就是 MethodInfo.Invoke()
方法的第二个参数。
GetArguments()
内部使用 ReqeustParser
来实现对每一个参数进行取值,它的主要过程只是对传入的 MethodInfo
对象的参数列表进行遍历
object[] GetArguments(MethodInfo method, HttpRequest request)
{
var parser = new RequestParser(request);
// 通过 Linq 的 Select 扩展来遍历参数列表,并依次通过 RequestParser 来取值
return method.GetParameters()
.Select(p => parser.ParseValue(p.Name, p.ParameterType))
.ToArray();
}
这么一来,取值的重任就交给 RequestParser
了——你觉得任务不够重吗?如果只是对简单的数据类型,比如 int、string 取值,当然不重,但如果是一个数据模型呢?
RequestParser
ReqeustParser
首要实现的就是对简单类型取值,这是在 ParseValue()
方法中实现的,进行简单的分析之后调用 Convert.ChangeType()
就能解决问题。
但如果遇到一个数据模型,就需要用 ParseObject()
来处理了,它会遍历模型对象的所有属性,并依次递归调用 ParseValue()
来进行处理——这里偷懒了,只处理了属性,没有去处理字段——如果你需要,自己实现也不是难事
class RequestParser
{
static bool IsConvertableType(Type type)
{
switch (type.FullName)
{
case "System.DateTime":
case "System.Decimal":
return true;
default:
return false;
}
}
readonly HttpRequest request;
internal RequestParser(HttpRequest request)
{
this.request = request;
}
internal object ParseValue(string name, Type type)
{
string value = request[name];
if (type == typeof(string))
{
return value;
}
if (string.IsNullOrWhiteSpace(value))
{
value = null;
}
var vType = Nullable.GetUnderlyingType(type) ?? type;
if (vType.IsEnum)
{
return value == null
? null
: Enum.ToObject(
vType,
Convert.ChangeType(value, Enum.GetUnderlyingType(vType)));
}
if (vType.IsPrimitive || IsConvertableType(vType))
{
return value == null ? null : Convert.ChangeType(value, vType);
}
return ParseObject(vType);
}
internal object ParseObject(Type type)
{
const BindingFlags flags
= BindingFlags.Instance
| BindingFlags.SetProperty
| BindingFlags.Public;
object obj;
try
{
obj = Activator.CreateInstance(type);
}
catch
{
return null;
}
foreach (var p in type.GetProperties(flags)
.Where(p => p.GetIndexParameters().Length == 0))
{
var value = ParseValue(p.Name, p.PropertyType);
if (value != null)
{
p.SetValue(obj, value, null);
}
}
return obj;
}
}
虽然一句注释都没有,但我相信你看得懂。如果实在不明白,请留言。
结束语
到此,从 MVC 转为 Web Form 的主要技术问题都已经解决了。其中一些处理方式是借鉴了 MVC 框架的实现思路。因此这个项目在切换框架的时候还不是特别复杂,所以要处理的事情也相对较少。对于一个成熟的 MVC 框架实现的项目来说,转换绝不是一件轻松的事情——相当于你得自己在 Web Form 中实现 MVC 框架,工作量大不说,稳定性也堪忧。
MVC 框架还有很重要的一个部分就是 Filter,对于 Filter 的简单实现,可以在 ActionDescriptor
中进行处理。但如果你想做这件事情,一定要谨慎,因为这涉及到一个相对复杂的生命周期,搞不好就可能刨个坑把自个儿埋了。