65.9K
CodeProject 正在变化。 阅读更多。
Home

使用HTTP Handler、Mootools和JSON生成WebService的客户端代理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (7投票s)

2007年2月19日

CPOL

6分钟阅读

viewsIcon

64654

downloadIcon

248

我们将编写代码来生成调用WebService所需的所有JavaScript,以发送和接收JSON。这将允许我们选择使用哪个JavaScript库(如Mootools、prototype、scriptaculous等),同时仍然能够执行此任务。

引言

随着MSFT AJAX Extensions的发布,从客户端调用WebService变得非常容易。

但是,如果您像我一样,想要调用WebService,但又不想使用AJAX Extensions,而是使用其他库,比如MooTools呢?好吧,您可以*直接*创建SOAP主体并将其发送到WebService。这似乎很简单,对吧?

我喜欢那些能自我生成的东西。

在这篇文章中,我将创建一个简单的WebService客户端代理,如果一切顺利,我们将能够调用它并获得响应。

背景信息

为了理解如何做到这一点,我“反射”了MSFT AJAX Extensions的程序集,看看它们是如何实现这一点的。因此,本文中提供的部分概念验证代码就是基于此的。同样,主要思想是理解如何在不真正使用MSFT AJAX Extensions的情况下,构建一个类似于其使用的代理。

为什么不使用MSFT AJAX Extensions

嗯,首先,我想学习整个过程是如何工作的。

我还想能够通过发送和接收JSON来调用WebService,而无需使用MSFT AJAX Extensions。许多小型库都会进行XHR调用。为什么不使用它们呢?

另一个问题,这里没有涵盖,是在.NET Framework的v1.1版本中使用此代码(经过一些轻微修改)。

首先...

...我们需要做的是理解这个的生命周期

给定一个WebService(或一系列WebService),应用程序将验证WebService是否具有[AjaxRemoteProxy]属性。如果是,我们将获取所有公共的[WebMethod]方法并生成客户端代理。当客户端代理被调用时,在服务器端,我们需要获取正确的方法,调用它,然后以“JSON样式”返回其结果。所有这些服务器端工作都是通过一些IHttpHandler完成的。

一个HandlerFactory将负责找出需要什么:默认的WebService处理程序、代理处理程序或响应处理程序。

代理文件将是ASMX本身,但现在,我们将在调用的末尾添加“/js”,得到类似这样的结果

<script src="https://codeproject.org.cn/ClientProxyCP/teste.asmx/js" 
       type="text/javascript"></script>

当对此发出调用时,一个处理程序将知道需要JavaScript,并生成它。

给我看看代码

我们需要做的第一件事是拥有AjaxRemoteProxy属性。此属性允许我们标记哪些WebService和Web方法可以在客户端调用

using System;

namespace CreativeMinds.Web.Proxy
{
    [AttributeUsage(AttributeTargets.Class | 
     AttributeTargets.Method, AllowMultiple = false)]
    public class AjaxRemoteProxyAttribute : Attribute
    {
        #region Private Declarations
        private bool  _ignore = false;
        #endregion Private Declarations

        #region Properties
        /// <summary>
        /// Gets or sets a value indicating whether the target is ignored.
        /// </summary>
        /// <value><c>true</c> if ignore;
        /// otherwise, <c>false</c>.</value>
        public bool  Ignore
        {
            get { return _ignore; }
            set { _ignore = value; }
        }
        #endregion Properties

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the
        /// <see cref="AjaxRemoteProxyAttribute"/> class.
        /// </summary>
        public AjaxRemoteProxyAttribute()
        {
        }
        /// <summary>
        /// Initializes a new instance of the
        /// <see cref="AjaxRemoteProxyAttribute"/> class.
        /// </summary>
        /// <param name="_ignore">if set
        /// to <c>true</c> if we wish to ignore this target.</param>
        public AjaxRemoteProxyAttribute(bool _ignore)
        {
            this._ignore = _ignore;
        }
        #endregion Constructor
    }
}

现在我们有了自己的属性,让我们创建一个简单的WebService

using System.Web.Services;
using CreativeMinds.Web.Proxy;

namespace CreativeMinds.Web.Services{
    [AjaxRemoteProxy()]
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class MyWebService : WebService
    {
        [WebMethod]
        public string HelloWorld()
        {
            return "Hello World";
        }
        [WebMethod]
        public string HelloYou(string name)
        {
            return "Hello " + name;
        }
    }
}

请注意,WebService类已使用我们新创建的属性进行了标记。

现在是酷炫的代码。我们需要做的第一件事是让应用程序知道对*.asmx的调用现在由我们处理。所以我们需要做两件事:首先,创建处理程序,然后修改web.config文件。

WebService处理程序工厂

如前所述,所有*.asmx调用都将由我们处理。因为我们也希望维护WebService的正常功能,所以我们需要创建一个处理程序工厂。此工厂将根据以下假设返回特定的处理程序

  1. 如果context.Request.PathInfo以“/js”结尾,我们需要生成代理;
  2. 如果context.Request.ContentType为“application/json;”或者我们有一个context.Request.Headers["x-request"]值为“JSON”,我们需要执行一个方法并返回其值;
  3. 否则,我们让WebService正常运行。

所以,让我们构建我们的工厂

using System;
using System.Web;
using System.Web.Services.Protocols;

namespace CreativeMinds.Web.Proxy
{
    public class RestFactoryHandler:IHttpHandlerFactory
    {
        #region IHttpHandlerFactory Members

        public IHttpHandler GetHandler(HttpContext context, 
               string requestType, string url, string pathTranslated)
        {
            if (string.Equals(context.Request.PathInfo, "/js", 
                              StringComparison.OrdinalIgnoreCase))
            {
                return new RestClientProxyHandler();
            }
            else
            {
                if (context.Request.ContentType.StartsWith("application/json;", 
                            StringComparison.OrdinalIgnoreCase) || 
                    (context.Request.Headers["x-request"] != null && 
                    context.Request.Headers["x-request"].Equals("json", 
                            StringComparison.OrdinalIgnoreCase)))
                {
                    return new RestClientResponseHandler();
                }
            }
            return new WebServiceHandlerFactory().GetHandler(context, 
                       requestType, url, pathTranslated);
        }

        public void ReleaseHandler(IHttpHandler handler)
        {
            
        }

        #endregion
    }
}

然后,我们还需要让应用程序知道我们的工厂

<httpHandlers>
    <remove verb="*" path="*.asmx"/>
    <add verb="*" path="*.asmx" validate="false" 
               type="CreativeMinds.Web.Proxy.RestFactoryHandler"/>
</httpHandlers>

客户端代理生成器处理程序

context.Request.PathInfo等于“/js”时,我们需要生成客户端代理。为此,工厂将返回RestClientProxyHandler

using System.Web;

namespace CreativeMinds.Web.Proxy
{
    class RestClientProxyHandler : IHttpHandler
    {
        private bool isReusable = true;

        #region IHttpHandler Members

        ///<summary>
        ///Enables processing of HTTP Web requests by a custom
        ///HttpHandler that implements the 
        ///<see cref="T:System.Web.IHttpHandler"></see> interface.
        ///</summary>
        ///
        ///<param name="context">An <see 
        ///cref="T:System.Web.HttpContext"></see> 
        ///object that provides references to the intrinsic server objects 
        ///(for example, Request, Response, Session, and Server)
        ///used to service HTTP requests. </param>
        public void ProcessRequest(HttpContext context)
        {
            WebServiceData wsd = context.Cache["WS_DATA:" + 
                    context.Request.FilePath] as WebServiceData;
            if (wsd != null)
            {
                wsd.Render(context);
            }
        }

        ///<summary>
        ///Gets a value indicating whether another request can use 
        ///the <see cref="T:System.Web.IHttpHandler"></see> instance.
        ///</summary>
        ///
        ///<returns>
        ///true if the <see cref="T:System.Web.IHttpHandler">
        ///        </see> instance is reusable; otherwise, false.
        ///</returns>
        ///
        public bool IsReusable
        {
            get { return isReusable; }
        }

        #endregion
    }
}

请注意两点

  1. 处理程序使用WebServiceData对象。此对象包含有关WebService的信息。我们在这里所做的是从context.Cache获取WebServiceData对象并进行渲染。
  2. context.Cache["WS_DATA:" + ... ]保存了所有被代理的WebService的WebServiceData。此集合由WebServiceData对象填充。

WebServiceData对象

如前所述,WebServiceData包含有关WebService的基本信息。它还负责WebService的渲染和执行。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security;
using System.Text;
using System.Web;
using System.Web.Compilation;
using System.Web.Hosting;
using System.Web.Services;
using System.Web.UI;
using Newtonsoft.Json;

namespace CreativeMinds.Web.Proxy
{
    internal class WebServiceData
    {
        #region Private Declarations

        private List<MethodInfo> _methods;
        private Type _type;
        private string _wsPath;
        private object _typeInstance;

        #endregion Private Declarations

        #region Constructor

        public WebServiceData(string wsPath)
        {
            _wsPath = wsPath;
            _methods = new List<MethodInfo>();
            Process();
        }

        #endregion Constructor

        #region Process

        /// <summary>
        /// Processes this instance.
        /// </summary>
        private void Process()
        {
            //Verifies if the path to the webservice is valid
            if (HostingEnvironment.VirtualPathProvider.FileExists(_wsPath))
            {
                Type type1 = null;
                try
                {
                    // Lets try and get the Type from the Virtual Path
                    type1 = BuildManager.GetCompiledType(_wsPath);
                    if (type1 == null)
                    {
                        type1 = BuildManager.CreateInstanceFromVirtualPath(
                                                  _wsPath, typeof (Page)).GetType();
                    }

                    if (type1 != null)
                    {
                        // Good. We have a Type. Now lets check if this is to Profixy.
                        object[] objArray1 = type1.GetCustomAttributes(
                                   typeof (AjaxRemoteProxyAttribute), true);
                        if (objArray1.Length == 0)
                        {
                            throw new InvalidOperationException(
                                   "No AjaxRemoteProxyAttribute found on webservice");
                        }

                        // Well. So far so good.
                        // Let's get all the methods.
                        BindingFlags flags1 = BindingFlags.Public | 
                               BindingFlags.DeclaredOnly | BindingFlags.Instance;
                        MethodInfo[] infoArray1 = type1.GetMethods(flags1);

                        foreach (MethodInfo info1 in infoArray1)
                        {
                            // we only need the WebMethods
                            object[] metArray1 = 
                              info1.GetCustomAttributes(typeof (WebMethodAttribute), true);
                            if (metArray1.Length != 0)
                            {
                                _methods.Add(info1);
                            }
                        }
                        
                        // keep locally the Type
                        _type = type1;

                        // Add this WS to the Cache, for later use.
                        if (HttpContext.Current.Cache["WS_DATA:" + 
                                 VirtualPathUtility.ToAbsolute(_wsPath)] == null)
                        {
                            HttpContext.Current.Cache["WS_DATA:" + 
                                  VirtualPathUtility.ToAbsolute(_wsPath)] = this;
                        }
                    }
                    else
                    {
                        throw new ApplicationException("Couldn't proxify webservice!!!!");
                    }
                }
                catch (SecurityException)
                {
                }
            }
        }

        #endregion

        #region Render

        /// <summary>
        /// Renders the Proxy to the specified <see cref="HttpContext"/>.
        /// </summary>
        /// <param name="context">The
        ///    <see cref="HttpContext"/>.</param>
        public void Render(HttpContext context)
        {
            // Set the ContentType to Javascript
            context.Response.ContentType = "application/x-javascript";

            StringBuilder aux = new StringBuilder();
            if (_type == null) return;

            // Register the namespace
            aux.AppendLine(string.Format(
                "RegisterNamespace(\"{0}\");", _type.Namespace));

            // Create the Class for this Type
            string nsClass = string.Format("{0}.{1}", _type.Namespace, _type.Name);
            aux.AppendLine(string.Format("{0} = function(){{}};", nsClass));

            _methods.ForEach(delegate (MethodInfo method)
             {
                 // Create a static Method on the class
                 aux.AppendFormat("{0}.{1} = function(", nsClass, method.Name);

                 // Set the method arguments
                 StringBuilder argumentsObject = new StringBuilder();
                 foreach (ParameterInfo info2 in method.GetParameters())
                 {
                     aux.AppendFormat("{0}, ", info2.Name);
                     argumentsObject.AppendFormat("\"{0}\":{0}, ", info2.Name);
                 }

                 if (argumentsObject.Length > 0)
                 {
                     argumentsObject = 
                       argumentsObject.Remove(argumentsObject.Length - 2, 2);
                     argumentsObject.Insert(0, "{").Append("}");
                 }

                 // Add the CompleteHandler argument in last
                 aux.Append("onCompleteHandler){\n");

                 // Render the method Body with the XHR call
                 aux.AppendLine(string.Format("new Json.Remote(\"{1}\", 
                       {{onComplete: onCompleteHandler, method:'post'}}).send({0});", 
                       argumentsObject.ToString(), 
                       VirtualPathUtility.ToAbsolute(_wsPath + "/" + method.Name)));
                 aux.Append("}\n");
             });
            context.Response.Write(aux.ToString());
        }

        #endregion

        #region Invoke

        /// <summary>
        /// Invokes the requested WebMethod specified in the <see cref="HttpContext"/>.
        /// </summary>
        /// <param name="context">The <see cref="HttpContext"/>.</param>
        public void Invoke(HttpContext context)
        {
            // Method name to invoke
            string methodName = context.Request.PathInfo.Substring(1);

            // We need an Instance of the Type
            if (_typeInstance == null)
                _typeInstance = Activator.CreateInstance(_type);

            // Get the Posted arguments (format "json={JSON Object}")
            string requestBody = 
              new StreamReader(context.Request.InputStream).ReadToEnd();
            string[] param = requestBody.Split('=');
            // JSON Deserializer @ http://www.newtonsoft.com/products/json/
            object a = JavaScriptConvert.DeserializeObject(param[1]);
            //object a = JavaScriptDeserializer.DeserializeFromJson<object>(param[1]);
            Dictionary<string, object> dic = a as Dictionary<string, object>;
            int paramCount = 0;
            if (dic != null)
            {
                paramCount = dic.Count;
            }
            object[] parms = new object[paramCount];

            if (dic != null)
            {
                int count = 0;
                foreach (KeyValuePair<string, object> kvp in dic)
                {
                    Debug.WriteLine(string.Format("Key = {0}, Value = {1}", 
                                    kvp.Key, kvp.Value));
                    parms[count] = kvp.Value;
                    count++;
                }
            }

            // Get the method to invoke and invoke it
            MethodInfo minfo = _type.GetMethod(methodName);
            object resp = minfo.Invoke(_typeInstance, parms);

            // Serialize the response
            // JSON Serializer @ http://www.newtonsoft.com/products/json/
            string JSONResp = 
              JavaScriptConvert.SerializeObject(new JsonResponse(resp));

            // Render the output to the context
            context.Response.ContentType = "application/json";
            context.Response.AddHeader("X-JSON", JSONResp);
            context.Response.Write(JSONResp);
        }

        #endregion
    }

    /// <summary>
    /// Wrapper for the JSON response
    /// </summary>
    public class JsonResponse
    {
        private object  _result = null;

        /// <summary>
        /// Gets or sets the result output.
        /// </summary>
        /// <value>The result.</value>
        public object  Result
        {
            get { return _result; }
            set { _result = value; }
        }


        /// <summary>
        /// Initializes a new instance of the 
        /// <see cref="JsonResponse"/> class.
        /// </summary>
        /// <param name="_result">The _result.</param>
        public JsonResponse(object _result)
        {
            this._result = _result;
        }
    }
}

初始化时,WebServiceData对象将尝试从WebService路径获取Type。如果成功,它将检查WebService是否具有AjaxRemoteProxyAttribute,如果为true,则提取WebMethods列表。

Invoke方法查看context.Request.PathInfo以确定要执行哪个方法。它还检查参数是否在context.Request.InputStream中传递,如果是,则将它们添加到方法调用中。最后,响应被序列化为JSON字符串并发送回客户端。

Render方法查看所有WebMethods并创建客户端代码。

JsonResponse类用于简化JSON响应的序列化。

至此,我们完成了第一大步:构建生成代理所需的代码。

现在,为了帮助我们“代理”WebService,我们将构建一个简单的助手类供WebForms使用

using System.Collections.Generic;
using System.Web;
using System.Web.UI;

namespace CreativeMinds.Web.Proxy
{
    public static class ProxyBuilder
    {
        #region Properties

        /// <summary>
        /// Gets or sets the get WS proxy list.
        /// </summary>
        /// <value>The get WS proxy list.</value>
        public static List<string> WSProxyList
        {
            get
            {
                List<string> aux = 
                  HttpContext.Current.Cache["WS_PROXIES_URL"] as List<string>;
                HttpContext.Current.Cache["WS_PROXIES_URL"] = 
                  aux ?? new List<string>();
                return HttpContext.Current.Cache["WS_PROXIES_URL"] as List<string>;
            }
            set
            {
                HttpContext.Current.Cache["WS_PROXIES_URL"] = value;
            }
        }

        #endregion Properties


        public static void For(string wsPath)
        {
            if (!WSProxyList.Exists(delegate(string s) { return s == wsPath; }))
            {
                new WebServiceData(wsPath);
                WSProxyList.Add(wsPath);
            }
        }

        /// <summary>
        /// Renders all Webservice Proxies in the <see cref="Page"/>.
        /// </summary>
        /// <param name="page">The <see 
        /// cref="Page"/> where the proxies will be generated and sused.</param>
        public static void RenderAllIn(Page page)
        {
            WSProxyList.ForEach(delegate(string virtualPath)
               {
                   string FullPath = 
                     VirtualPathUtility.ToAbsolute(virtualPath + "/js");
                   page.ClientScript.RegisterClientScriptInclude(
                       string.Format("WSPROXY:{0}", FullPath), FullPath);
               });
        }
    }
}

ProxyBuilder.For方法接收一个包含WebService虚拟路径的字符串。对于有效路径,此方法将向WSProxyList属性添加一个新的WebServiceData对象。

当不再需要代理时,应调用ProxyBuilder.RenderAllIn。这将注册由我们的代理生成的所有客户端脚本。

protected void Page_Load(object sender, EventArgs e)
{
    ProxyBuilder.For("~/teste.asmx");
    ProxyBuilder.RenderAllIn(this);
}

浏览页面,我们现在可以看到我们WebService的输出

RegisterNamespace("CreativeMinds.Web.Services");
CreativeMinds.Web.Services.teste = function(){};
CreativeMinds.Web.Services.teste.HelloWorld = function(onCompleteHandler){
    new Json.Remote("/CreativeMindsWebSite/teste.asmx/HelloWorld", 
                    {onComplete: onCompleteHandler, method:'post'}).send();
}
CreativeMinds.Web.Services.teste.HelloYou = function(name, onCompleteHandler){
new Json.Remote("/CreativeMindsWebSite/teste.asmx/HelloYou", 
   {onComplete: onCompleteHandler, method:'post'}).send({"name":name});
}

太棒了!生成的JavaScript类似于我们的WebService类。我们创建了命名空间CreativeMinds.Web.Services,类名teste也在那里,还有它的Web Methods。请注意,所有方法调用都需要一个onCompleteHandler。这将处理所有成功的调用。

只剩下两个步骤:响应处理程序和全部测试。

响应处理程序

正如您在代理生成的代码中看到的,对WebService方法的调用没有改变

/CreativeMindsWebSite/teste.asmx/HelloWorld

那么,我们怎么知道返回JSON还是XML呢?好吧,我们将在RestFactoryHandler类中观察context.Request.ContentTypecontext.Request.Headers。如果其中任何一个包含JSON,我们就知道该怎么做了……:)

当请求JSON响应时,RestFactoryHandler将返回RestClientResponseHandler

using System.Web;

namespace CreativeMinds.Web.Proxy
{
    public class RestClientResponseHandler : IHttpHandler
    {
        #region IHttpHandler Members

        ///<summary>
        ///Enables processing of HTTP Web requests by a custom HttpHandler 
        ///that implements the <see cref="T:System.Web.IHttpHandler"></see> interface.
        ///</summary>
        ///
        ///<param name="context">An 
        ///  <see cref="T:System.Web.HttpContext"></see> object that
        /// provides references to the intrinsic server objects
        /// (for example, Request, Response, Session, and Server)
        /// used to service HTTP requests. </param>
        public void ProcessRequest(HttpContext context)
        {
            WebServiceData wsd = context.Cache["WS_DATA:" + 
                       context.Request.FilePath] as WebServiceData;
            if (wsd != null)
            {
                wsd.Invoke(context);
            }
        }

        ///<summary>
        ///Gets a value indicating whether another request can use 
        ////the <see cref="T:System.Web.IHttpHandler"></see> instance.
        ///</summary>
        ///
        ///<returns>
        ///true if the <see cref="T:System.Web.IHttpHandler"></see>
        ///instance is reusable; otherwise, false.
        ///</returns>
        ///
        public bool IsReusable
        {
            get { return true; }
        }

        #endregion
    }
}

同样,请注意它尝试从context.Cache获取WebServiceData对象,并将其作为参数传递给InvokeWebServiceDataInvoke方法将从PathInfo中提取方法名。然后,它将从Type创建实例,并通过检查Request.InputStream来查找POST传递的参数。使用Newtonsoft的JavaScriptDeserializer,我们反序列化任何参数,并将它们添加到调用方法所需的集合中。最后,我们调用方法,序列化响应,并将其发送回客户端。

...

namespace CreativeMinds.Web.Proxy
{
    internal class WebServiceData
    {
        ...
        /// <summary>
        /// Invokes the requested WebMethod
        /// specified in the <see cref="HttpContext"/>.
        /// </summary>
        /// <param name="context">The <see cref="HttpContext"/>.</param>
        public void Invoke(HttpContext context)
        {
            // Method name to invoke
            string methodName = context.Request.PathInfo.Substring(1);

            // We need an Instance of the Type
            if (_typeInstance == null)
                _typeInstance = Activator.CreateInstance(_type);

            // Get the Posted arguments (format "json={JSON Object}")
            string requestBody = 
              new StreamReader(context.Request.InputStream).ReadToEnd();
            string[] param = requestBody.Split('=');
            // JSON Deserializer @ http://www.newtonsoft.com/products/json/
            object a = JavaScriptConvert.DeserializeObject(param[1]);
            //object a = JavaScriptDeserializer.DeserializeFromJson<object>(param[1]);
            Dictionary<string, object> dic = a as Dictionary<string, object>;
            int paramCount = 0;
            if (dic != null)
            {
                paramCount = dic.Count;
            }
            object[] parms = new object[paramCount];

            if (dic != null)
            {
                int count = 0;
                foreach (KeyValuePair<string, object> kvp in dic)
                {
                    Debug.WriteLine(string.Format("Key = {0}, Value = {1}", 
                                    kvp.Key, kvp.Value));
                    parms[count] = kvp.Value;
                    count++;
                }
            }

            // Get the method to invoke and invoke it
            MethodInfo minfo = _type.GetMethod(methodName);
            object resp = minfo.Invoke(_typeInstance, parms);

            // Serialize the response
            // JSON Serializer @ http://www.newtonsoft.com/products/json/
            string JSONResp = 
              JavaScriptConvert.SerializeObject(new JsonResponse(resp));

            // Render the output to the context
            context.Response.ContentType = "application/json";
            context.Response.AddHeader("X-JSON", JSONResp);
            context.Response.Write(JSONResp);
        }
        ...

至此,我们准备好测试一次调用了。我们只需要做的就是,首先创建onCompleteHandler函数来处理响应

function completedHandler(json)
{
    alert(json.Result);
}

然后向页面添加一个文本框

<input type="textbox" id="txtName" />

最后,一个调用者

<a href="#" 
   onclick="CreativeMinds.Web.Services.teste.HelloYou(
            $("textbox").value, complete)">call HelloYou</a>

就是这样。我们构建了一个代理生成器。

再次声明,这是一个概念验证,因此它没有经过性能测试,也没有经过错误/异常测试。

使用HTTP Handler、Mootools和JSON生成WebService的客户端代理 - CodeProject - 代码之家
© . All rights reserved.