从 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 名称。
<system.web>
<pages clientIDMode="Static"
enableSessionState="true"
enableViewState="false"
enableViewStateMac="false">
</pages>
</system.web>
说起来轻松,但这部分工作需要大量的人工操作,所以其实是最累也最容易出错的。
移植 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<T>
,在 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
中进行处理。但如果你想做这件事情,一定要谨慎,因为这涉及到一个相对复杂的生命周期,搞不好就可能刨个坑把自个儿埋了。