• 关于ASP.NET WebForm和ASP.NET MVC框架在使用ajax时的区别
  • 波涛中的小船 发表于 2016/3/28 12:41:00 | 分类标签: MVC ajax webForm
  •  前言

      Asp.net WebForm 和 Asp.net MVC(简称MVC) 都是基于Asp.net的web开发框架,两者有很大的区别,其中一个就是MVC更加注重http本质,而WebForm试图屏蔽http,为此提供了大量的服务器控件和ViewState机制,让开发人员可以像开发Windows Form应用程序一样,基于事件模型去编程。两者各有优缺点和适用情景,但MVC现在是许多Asp.net开发者的首选。

      WebForm是建立在Asp.net的基础上的,Asp.net提供了足够的扩展性,我们也可以利用这些在WebForm下编写像MVC一样的框架,这个有机会再写。说到WebForm很多人就会联想到服务器控件(拖控件!!!),其实不然,我们也可以完全不使用服务器控件,像MVC那样关注html。WebForm要抛弃服务器控件,集中关注html,首先就要将<form runat="server"></form>标签去掉,这个runat server 的form 是其PostBack机制的基础。既然我们要回归到html+css+js,那么意味着许多东西都要自己实现,例如处理Ajax请求。不像MVC那样,WebForm开始的设计就将服务器控件作为主要组成部分,如果不使用它,那么只能利用它的扩展性去实现。

      本系列就是实现一个基于WebForm平台的轻量级ajax组件,主要分为三个部分:

      1. 介绍WebForm下各种实现方式。

      2. 分析ajaxpro组件。

      3. 编写自己的ajax组件。

    一、Ajax简介

      异步允许我们在不刷新整个页面的情况下,像服务器请求或提交数据。对于复杂的页面,为了请求一点数据而重载整个页面显然是很低效的,ajax就是为了解决这个问题的。ajax的核心是XmlHttpRequest对象,通过该对象,以文本的形式向服务器提交请求。XmlHttpRequest2.0后,还支持提交二进制数据。

      ajax安全:出于安全考虑,ajax受同源策略限制;也就是只能访问同一个域、同一个端口的请求,跨域请求会被拒绝。当然有时候需求需要跨域发送请求,常用的跨域处理方法有CORS(跨域资源共享)和JSONP(参数式JSON)。

      ajax数据交互格式:虽然Ajax核心对象XmlHttpRequest有"XML"字眼,但客户端与服务器数据交换格式不局限于xml,例如现在更多是使用json格式。  

      ajax 也是有缺点的。例如对搜索引擎的支持不太好;有时候也会违背url资源定位的初衷。

    二、Asp.net MVC 平台下使用ajax

      在MVC里,ajax调用后台方法非常方便,只需要指定Action的名称即可。

      前台代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <body>
        <h1>index</h1>
        <input type="button" value="GetData" onclick="getData()" />
        <span id="result"></span>
    </body>
    <script type="text/javascript">
        function getData() {
            $.get("GetData", function (data) {
                $("#result").text(data);
            });
        }
    </script>

      后台代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class AjaxController : Controller
    {
        public ActionResult GetData()
        {
            if(Request.IsAjaxRequest())
            {
                return Content("data");
            }
            return View();
        }
    }

    三、WebForm 平台下使用ajax

      3.1 基于服务器控件包或者第三方组件

      这是基于服务器控件的,例如ajax toolkit工具包,或者像FineUI这样的组件。web前端始终是由html+css+js组成的,只不过如何去生成的问题。原生的我们可以自己编写,或者用一些前端插件;基于服务器控件的,都是在后台生成的,通常效率也低一点。服务器组件会在前台生成一系列代理,本质还是一样的,只不过控件封装了这个过程,不需要我们自己编写。基于控件或者第三方组件的模式,在一些管理系统还是挺有用的,访问量不是很大,可以快速开发。

      3.2 基于ICallbackEventHandler接口

      .net 提供了ICallbackEventHandler接口,用于处理回调请求。该接口需要用ClientScriptManager在前台生成代理脚本,用于发送和接收请求,所以需要<form runat="server">标签。

      前台代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <body>
        <form id="form1" runat="server">
        <div>       
            <input type="button" value="获取回调结果" onclick="callServer()" />
            <span id="result" style="color:Red;"></span>
        </div>
        </form>
    </body>
    <script type="text/javascript">
        function getCallbackResult(result){
            document.getElementById("result").innerHTML = result;
        }
    </script>

      后台代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public partial class Test1 : System.Web.UI.Page, ICallbackEventHandler
    {       
        protected void Page_Load(object sender, EventArgs e)
        {
            //客户端脚本Manager
            ClientScriptManager scriptMgr = this.ClientScript;
     
            //获取回调函数,getCallbackResult就是回调函数
            string functionName = scriptMgr.GetCallbackEventReference(this"""getCallbackResult""");
     
            //发起请求的脚本,callServer就是点击按钮事件的执行函数
            string scriptExecutor = "function callServer(){" + functionName + ";}";
     
            //注册脚本
            scriptMgr.RegisterClientScriptBlock(this.GetType(), "callServer", scriptExecutor, true);
        }
     
        //接口方法
        public string GetCallbackResult()
        {
            return "callback result";
        }
     
        //接口方法
        public void RaiseCallbackEvent(string eventArgument)
        {
        }
    }

      这种方式有以下缺点:

      1. 实现起来较复杂,每个页面Load事件都要去注册相应的脚本。

      2. 前台会生成一个用于代理的脚本文件。

      3. 对于页面交互复杂的,实现起来非常麻烦。

      4. 虽然是回调,但是此时页面对象还是生成了。

      3.3 使用一般处理程序

      一般处理程序其实是一个实现了IHttpHandler接口类,与页面类一样,它也可以用于处理请求。一般处理程序通常不用于生成html,也没有复杂的事件机制,只有一个ProcessRequest入口用于处理请求。我们可以将ajax请求地址写成.ashx文件的路径,这样就可以处理了,而且效率比较高。

      要输出文本内容只需要Response.Write(data)即可,例如,从数据库获取数据后,序列化为json格式字符串,然后输出。前面说到,一般处理程序不像页面一样原来生成html,如果要生成html,可以通过加载用户控件生成。如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public void ProcessRequest(HttpContext context)
    {
        Page page = new Page();
        Control control = page.LoadControl("~/PageOrAshx/UserInfo.ascx");
        if (control != null)
        {
            StringWriter sw = new StringWriter();
            HtmlTextWriter writer = new HtmlTextWriter(sw);
            control.RenderControl(writer);
            string html = sw.ToString();
            context.Response.Write(html);               
        }
    }

      这种方式的优点是轻量、高效;缺点是对于交互多的需要定义许多ashx文件,加大了管理和维护成本。

      3.4 页面基类

      将处理ajax请求的方法定义在页面对象内,这样每个页面就可以专注处理本页面相关的请求了。这里有点需要注意。

      1.如何知道这个请求是ajax请求?

        通过请求X-Requested-With:XMLHttlRequest 可以判断,大部份浏览器的异步请求都会包含这个请求头;也可以通过自定义请求头实现,例如:AjaxFlag:XHR

      2.在哪里统一处理?

        如果在每个页面类里判断和调用是很麻烦的,所以将这个处理过程转到一个页面基类里处理。

      3.如何知道调用的是哪个方法?

        通过传参或者定义在请求头都可以,例如:MethodName:GetData。

      4.知道方法名称了,如何动态调用?

        反射

      5.如何知道该方法可以被外部调用?

        可以认为public类型的就可以被外部调用,也可以通过标记属性标记。

      通过上面的分析,简单实现如下  

      页面基类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class PageBase : Page
    {
        public override void ProcessRequest(HttpContext context)
        {
            HttpRequest request = context.Request;
            if (string.Compare(request.Headers["AjaxFlag"],"AjaxFlag",0) == 0)
            {
                string methodName = request.Headers["MethodName"];
                if (string.IsNullOrEmpty(methodName))
                {
                    EndRequest("MethodName标记不能为空!");
                }
                Type type = this.GetType().BaseType;
                MethodInfo info = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
                if (info == null)
                {
                    EndRequest("找不到合适的方法调用!");
                }               
                string data = info.Invoke(thisnullas string;
                EndRequest(data);
            }
            base.ProcessRequest(context);
        }
     
        private void EndRequest(string msg)
        {
            HttpResponse response = this.Context.Response;
            response.Write(msg);
            response.End();
        }
    }

      页面类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public partial class Test1 : PageBase
    {
        protected void Page_Load(object sender, EventArgs e)
        {
     
        }
     
        public string GetData()
        {
            return "213";
        }
    }  

      前台代码:

    1
    2
    3
    4
    5
    6
    7
    8
    function getData(){
        $.ajax({
            headers:{"AjaxFlag":"XHR","MethodName":"GetData"},
            success:function(data){
                $("#result").text(data);
            }
        });
    }

    四、优化版页面基类

      上面的页面基类功能很少,而且通过反射这样调用的效率很低。这里优化一下:

      1.可以支持简单类型的参数。

        例如上面的GetData可以是:GetData(string name),通过函数元数据可以获取相关的参数,再根据请求的参数,就可以设置参数了。

      2.加入标记属性。

        只有被AjaxMethodAttribute标记的属性才能被外部调用。

      3.优化反射。

        利用缓存,避免每次都根据函数名称去搜索函数信息。

      标记属性:

    1
    2
    3
    public class AjaxMethodAttribute : Attribute
    {
    }

      缓存对象:  

    1
    2
    3
    4
    5
    6
    public class CacheMethodInfo
    {
        public string MethodName { getset; }
        public MethodInfo MethodInfo { getset; }
        public ParameterInfo[] Parameters { getset; }
    }

      基类代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    public class PageBase : Page
    {
        private static Hashtable _ajaxTable = Hashtable.Synchronized(new Hashtable());
     
        public override void ProcessRequest(HttpContext context)
        {           
            HttpRequest request = context.Request;
            if (string.Compare(request.Headers["AjaxFlag"],"XHR",true) == 0)
            {
                InvokeMethod(request.Headers["MethodName"]);
            }
            base.ProcessRequest(context);
        }
     
        /// <summary>
        /// 反射执行函数
        /// </summary>
        /// <param name="methodName"></param>
        private void InvokeMethod(string methodName)
        {
            if (string.IsNullOrEmpty(methodName))
            {
                EndRequest("MethodName标记不能为空!");
            }
            CacheMethodInfo targetInfo = TryGetMethodInfo(methodName);
            if (targetInfo == null)
            {
                EndRequest("找不到合适的方法调用!");
            }
            try
            {
                object[] parameters = GetParameters(targetInfo.Parameters);
                string data = targetInfo.MethodInfo.Invoke(this, parameters) as string;
                EndRequest(data);
            }
            catch (FormatException)
            {
                EndRequest("参数类型匹配发生错误!");
            }
            catch (InvalidCastException)
            {
                EndRequest("参数类型转换发生错误!");
            }
            catch (ThreadAbortException)
            {
            }
            catch (Exception e)
            {
                EndRequest(e.Message);
            }
        }
     
        /// <summary>
        /// 获取函数元数据并缓存
        /// </summary>
        /// <param name="methodName"></param>
        /// <returns></returns>
        private CacheMethodInfo TryGetMethodInfo(string methodName)
        {
            Type type = this.GetType().BaseType;
            string cacheKey = type.AssemblyQualifiedName;
            Dictionary<string, CacheMethodInfo> dic = _ajaxTable[cacheKey] as Dictionary<string, CacheMethodInfo>;
            if (dic == null)
            {
                dic = new Dictionary<string, CacheMethodInfo>();
                MethodInfo[] methodInfos = (from in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
                                            let ma = m.GetCustomAttributes(typeof(AjaxMethodAttribute), false)
                                            where ma.Length > 0
                                            select