|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionWith 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 infoFor 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 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 FactoryAs 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:
So lets build our factory: 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 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:
WebServiceData object As said, the WebServiceData contains basic information about the webservice. It is also responsible for the render and execution of the 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;
}
}
}
When initialized, the WebServiceData object will try to get a
The The Render method looks at all the WebMethods and creates the client-side code. The 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: 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
When no more proxies are needed, the 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 Only two step remaining: The Response Handler, and testing it all. Response HandlerAs 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 thecontext.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 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 ...
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 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.
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||