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






4.33/5 (7投票s)
我们将编写代码来生成调用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的正常功能,所以我们需要创建一个处理程序工厂。此工厂将根据以下假设返回特定的处理程序
- 如果
context.Request.PathInfo
以“/js”结尾,我们需要生成代理; - 如果
context.Request.ContentType
为“application/json;”或者我们有一个context.Request.Headers["x-request"]
值为“JSON”,我们需要执行一个方法并返回其值; - 否则,我们让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
}
}
请注意两点
- 处理程序使用
WebServiceData
对象。此对象包含有关WebService的信息。我们在这里所做的是从context.Cache
获取WebServiceData
对象并进行渲染。 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.ContentType
和context.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
对象,并将其作为参数传递给Invoke
。WebServiceData
的Invoke
方法将从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>
就是这样。我们构建了一个代理生成器。
再次声明,这是一个概念验证,因此它没有经过性能测试,也没有经过错误/异常测试。