KiMoGiGi 技术文集

不在乎选择什么,而在乎坚持多久……

IT博客 首页 联系 聚合 管理
  185 Posts :: 14 Stories :: 48 Comments :: 0 Trackbacks
Generate a client-side proxy for a webservice using HTTP Handlers, Mootools and JSON
By Bruno R. Figueiredo.

We will create code that will generate all the javascript necessary to call a webservice, sending and recieving Json. This will allow us to choose wich javascript library (such as Mootools, prototype, scriptaculous, etc...) to use and still be able to performe this task.

Introduction

With the release of the MSFT Ajax Extensions, calling a webservice from client-side is a kids task.

But what if you, like me, want to call a webservice but don't want to use the Ajax Extensions, using instead another library, like mootools? Well you could *just* create the soap body and send it to the webservice. That's seems easy, right?

Well, I like things that generate themselves.

In this post I will create a simple client-side proxy from a webservice, and if all ends well, we will be able to call it and get a response.

Background info

For understanding how this should be done, I went and "reflected" the MSFT Ajax Extensions assemblies to see how did they get this to work. So some of the code presented in this proof of concept is based on this. Again, the main ideia is to understand how to build a proxy similar to the used by the MSFT Ajax Extensions but without really using it.

"Why don't you use the MSFT Ajax Extensions?"

Well, first of all I wanted to learn how the whole process worked.

I also wanted to be able to call a webservice by sending and receiving Json without using the MSFT Ajax Extensions. Many small sized libraries make XHR calls. Why not used them.

Another issue, not covered here, is the usage of this code (with some slight changes) on the v1.1 of the .NET Framework.

The first thing...

... that we need to do is understand the life cycle of this:

Given a webservice (or a list of webservices), the application will validate if the webservice has the [AjaxRemoteProxy] attribute. If so, we will grab all the [WebMethod] methods that are public and generate the client-side proxy. When the client-proxy is called, on the server we need to get the correct method, invoke him, and return its results "json style". All of this server-side is done with some IHttpHandlers.

A HandlerFactory will do the work on finding out what is needed: The default webservice handler, a proxy handler, or a response handler.

The proxy file will be the asmx itself, but now we will add a "/js" to the end of the call, resulting in something like this:

<script src="http://www.codeproject.com/ClientProxyCP/teste.asmx/js" type="text/javascript"></script>

When the call is made to this, a handler will now that a javascript is needed, and generate it.

"Show me some code"

The first thing we need to have is the AjaxRemoteProxy attribute. This attribute will allow us to both mark wich webservices and web methods we will be able to call on client-side:

Collapse
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
    }
}

Now that we have our attribute, lets create a simple 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;
        }
    }
}

Notice that the webservice class is marked with our newly created attribute.

Now comes the cool code. The first thing we now need to do is to let the application know that the calls to *.asmx are now handled by us. So we need to do two things: First create the Handler and then change the web.config file.

The WebServices Handler Factory

As it was said before, the all *.asmx calls will be handled by us. Because we also want to maintain the normal functionality of the webservices, we need to create a handler factory. This factory will managed the return of the specific handler based on the following assumptions:

  1. If the context.Request.PathInfo ends with "/js", we need to generate the proxy;
  2. If the context.Request.ContentType is "application/json;" or we have a context.Request.Headers["x-request"] with "JSON" value, we need to execute a method and return its value;
  3. otherwise, we let the webservice run normally.

So lets build our factory:

Collapse
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
    }
}

Then we also need to let the application know about our factory:

<httpHandlers>
    <remove verb="*" path="*.asmx"/>
    <add verb="*" path="*.asmx" validate="false" type="CreativeMinds.Web.Proxy.RestFactoryHandler"/>
</httpHandlers>
The client-side proxy generator handler

When the context.Request.PathInfo equals "/js", we need to generate the client-side proxy. For this task the factory will return the RestClientProxyHandler.

Collapse
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
    }
}

Notice two things:

  1. the handler uses a WebServiceData object. This object contains the information about the webservice. So what we do where is get the WebServiceData object from the context.Cache and render it.
  2. the context.Cache["WS_DATA:" + ... ] holds all the WebServiceData on all webservices that are proxified. This collection is filled also on the WebServiceData object.

WebServiceData object

As said, the WebServiceData contains basic information about the webservice. It is also responsible for the render and execution of the webservice.

Collapse
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;
        }
    }
}

When initialized, the WebServiceData object will try to get a Type from the webservice path. If successful, it will check if the webservice has the AjaxRemoteProxyAttribute, and if true, will extract the WebMethods list.

The Invoke method looks at the context.Request.PathInfo to see what method to execute. It also check if arguments are passed on the context.Request.InputStream and if so, adds them to the method call. In the end the response is serialized into a Json string and sent back to the client.

The Render method looks at all the WebMethods and creates the client-side code.

The JsonResponse class is used to simplify the serialization of the Json response.

With this we have completed the first big step: Build the necessary code to generate the proxy.

Now to help up "proxifing" the webservices, we will build a simple helper to use on the webforms:

Collapse
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);                               
                                       });
        }
    }
}

The ProxyBuilder.For method recieves a string with the virtual path to the webservice. With a valid path, this method will add a new WebServiceData object to the WSProxyList property.

When no more proxies are needed, the ProxyBuilder.RenderAllIn should be called. This will register all client script generated by our proxies.

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

Browsing the page, we can now see the output for our 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});
}

Sweet! The generated javascript resembles our webservice class. We have the namespace CreativeMinds.Web.Services created, the class name teste its also there, and its webmethods. Notice that all method calls need a onCompleteHandler. This will handle all the successfully calls.

Only two step remaining: The Response Handler, and testing it all.

Response Handler

As you can see in the code generated by the proxy, the call to the webservice method doesn't change:

/CreativeMindsWebSite/teste.asmx/HelloWorld

So how can the know what to return - Json or XML?. Well, we will watch for the context.Request.ContentType and the context.Request.Headers on our RestFactoryHandler class. If one of thoose as Json on it we know what to do... :)

When a Json response is requested, the RestFactoryHandler will return the RestClientResponseHandler.

Collapse
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
    }
}

Again notice that it tries to get a WebServiceData object from the context.Cache and Invoke it passing the context as argument. The Invoke method of the WebServiceData will extract the method name form the PathInfo. Then it will create an instance from the Type, check for arguments passed on the post by checking the Request.InputStream. Using the Newtonsoft JavaScriptDeserializer we deserialize any arguments and add them to the object collection needed to invoke a method. Finally we invoke the method, serialize the response and send it back to the client.

Collapse
...

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);
        }
...

With this is are ready to test a call. So all we need to do is, first create the onCompleteHandler function to handle the response:

    function completedHandler(json)
    {
        alert(json.Result);
    }
Then add a textbox to the page:
    <input type="textbox" id="txtName" />
Finally, a caller:
    <a href="#" onclick="CreativeMinds.Web.Services.teste.HelloYou($("textbox").value, complete)">call HelloYou</a>

That's it. We have build a proxy generator.

Again this is a proof of concept, so its not tested for performance nor bug/error proof.

posted on 2007-03-23 23:24 KiMoGiGi 阅读(1014) 评论(0)  编辑 收藏 引用 所属分类: ASP.NETC# / Winforms
只有注册用户登录后才能发表评论。