从 Asp.Net MVC 到 Web Form

2016/12/03 20:57
阅读数 36

从 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,顺便设置 clientIDModeStatic,免得 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 来实现 JsonResultSystem.Web.Http.Results.JsonResult<T>,在 System.Web.Http.dll 中)。而普通 Controller 是用微软自己的 JavaScriptSerializer 来实现的的 JsonResultSystem.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); }

不过重要的不是 IActionResultActionResult,而是具体的实现。从原有的程序功能来看,至少需要实现:

  • 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)

幸好我们原来的路由定义得并不复杂,所以这里的解析函数也可以写得很简单,只是按分隔符 / 拆成几段分别赋值给新对象的 ControllerActionId 属性就好。

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 反射工具类

虽然没有路由系统,但是上面得到了 ControllerAction 这两个名称之后,还需要找到对应的 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 中进行处理。但如果你想做这件事情,一定要谨慎,因为这涉及到一个相对复杂的生命周期,搞不好就可能刨个坑把自个儿埋了。


点击〔↙阅读原文〕 发表你的意见!

本文分享自微信公众号 - 边城客栈(fancyidea-full)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部