基于程序集的 Web 服务和使用反射的 JavaScript 代理






4.88/5 (16投票s)
ASP.NET 和 Ajax Web 服务,不是来自 .asmx 文件,而是来自使用一点反射的已编译程序集
引言
任何构建 .NET 应用程序框架、自定义控件或只是喜欢分离关注点的人,都会遇到我曾经遇到的同样问题。我希望我能帮助你们中的一些人,而无需像我一样费力,但让我先解释一下这个问题。这是一个相当长的故事,请耐心等待;本文的水平涵盖了许多不同领域的初级到高级。
问题所在
当您想开发具有 Javascript 回调功能的自定义控件时,可以使用 ICallBack 和 IScriptable 接口。这些接口通过使用 Javascript 实现从您的类进行调用以及向您的类进行返回调用,而无需实现更新面板或页面刷新。然而,ICallback 接口有一个问题:它没有很好的 JSON(反)序列化。它限制每个组件只能有一个函数,尽管实现更多是可能的,但它不是很易于管理。
应该如何
如果您曾经在 ASP.NET 中使用过 Web 服务,您就会看到这种机制的强大功能。当与 ASP.NET Ajax 1.0 结合使用时尤其如此,因为它会为您生成 Javascript 代理。对于那些没有使用过 ASP.NET Web 服务的人,这里有一个简短的回顾:您在项目中创建一个 ASMX 文件,用一个包含几个 Web 服务属性的类填充它,就完成了!无需创建 SOAP,无需 JSON(反)序列化,无需编写代理。您唯一需要做的就是将其注册到您的 <ScriptManager> 中,以便它生成 Javascript 代理。将它放在我们的自定义控件库中是不是很棒?
又一个问题
这种方法的最大问题是,此功能仅适用于 Web 目录中尚未编译的 ASMX 文件。您没有“开箱即用”的方法可以将此功能指向已编译的程序集。为了解决这个问题,我们首先看看当前功能是如何工作的。
默认情况下它如何工作
ASMX 文件通过 HTTPHandler 转换为 Web 服务和 Javascript 代理。此 HTTPHandler 设置在 config.xml 文件中。
<httpHandlers>
<remove verb="*" path="*.asmx"/>
<add verb="*" path="*.asmx" validate="false"
type="System.Web.Script.Services.ScriptHandlerFactory"/>
</httpHandlers>
您在这里看到的处理程序是在新的“支持 ASP.NET Ajax 的 Web 应用程序”中默认创建的。当请求扩展名为 ASMX 的文件(“*.asmx
”)时,它会将该请求转发到 ScriptHandlerFactory
。然后,脚本处理程序工厂会为您创建 Web 服务、Javascript 代理或 Javascript 方法处理程序。当您将 Web 服务添加到 ScriptManager
时,它会在您的网页上生成一个 <script src="Webserver.asmx/js"/>
。此类的问题在于它会检查 ASMX 文件,仅此而已。此类有一个更大的问题,即几乎所有内容都是 Internal
和 Private
。如果您不知道这意味着什么,它意味着扩展或更改任何内容都是不可能的。
反射它
好的,也许并非不可能。一切都编译成托管代码,借助反射,我们可以进入我们喜欢的每一个小角落。即使是私有构造函数也可以通过 .NET 框架中最强大的组件之一访问。关于反射的教程有很多,所以本文我将假设您了解基础知识。
开始解决方案
我们从一个新的空“支持 ASP.NET AJAX 的 Web 应用程序”开始,它附带 ASP.NET Ajax 1.0。然后我们添加第二个空的 C# 类库项目作为第二个项目,并在第一个项目中引用第二个项目。
AutoDiscoveryWebServiceHandlerFactory
为了实现我们自己的代码,我们将在 CustomControl 库中创建我们自己的 HttpHandlerFactory。我们将其命名为 AutoDiscoveryWebServiceHandlerFactory,实现接口 IHttpHandlerFactory。
public IHttpHandler GetHandler(HttpContext context,
string requestType, string url, string pathTranslated)
和
public void ReleaseHandler
然后在 Web 应用程序中,我们修改 config.xml 文件以将 ASMX 请求转发到我们自己的处理程序工厂。
<httpHandlers>
<remove verb="*" path="*.asmx"/>
<add verb="*" path="*.asmx" validate="false"
type="Devusion. Services.AutoDiscoveryWebServiceHandlerFactory"/>
</httpHandlers>
自定义 Web 服务
接下来我们创建我们自己的 Web 服务,而不是普通的 ASMX 文件,而是一个普通的 C# 类文件。
namespace Devusion.CustomControls
{
[ScriptService]
[WebService(Namespace = "http://www.devusion.nl/xmlns/customcontrols")]
public class Service : WebService
{
[WebMethod]
[ScriptMethod(UseHttpGet = true)]
public string HelloWorldYou(string name)
{
return DateTime.Now.ToLongTimeString()+ ": Hello " + name;
}
}
}
WebServiceHandlerFactory
现在让我们编写代码,使调用 /Devusion.CustomControls.Service.asmx 成为可能。这将调用我们的 AutoDiscoveryWebServiceHandlerFactory.GetHandler 函数。在这里,我们必须将 URL 转换为类的类型。我们通过遍历已加载的程序集并检查 Web 服务属性来完成此操作。这应该进行优化以获得更好的性能。如果我们找不到匹配的类型,我们将返回 null。
private Type GetServiceType(string TypeName)
{
// TODO: Caching mechanism for assembly checks
foreach (
Assembly loadedAssembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type ClassType = loadedAssembly.GetType(TypeName);
if (ClassType != null)
return ClassType;
}
return null;
}
在 Gethandler 中,我们编写
// Try to get the type associated with the request (On a name to type basis)
Type WebServiceType =
this.GetServiceType(Path.GetFileNameWithoutExtension(pathTranslated));
// if we did not find any send it on to the
// original ajax script service handler.
if (WebServiceType == null)
{
// [REFLECTION] Get the internal class
// System.Web.Script.Services.ScriptHandlerFactory create it
IHttpHandlerFactory ScriptHandlerFactory =
(IHttpHandlerFactory)System.Activator.CreateInstance(
AjaxAssembly.GetType(
"System.Web.Script.Services.ScriptHandlerFactory"));
UsedHandlerFactory = ScriptHandlerFactory;
return ScriptHandlerFactory.GetHandler(
context, requestType, url, pathTranslated);
}
这里的代码将获取 WebServiceType。如果未找到类型,它将使用反射将请求转发到默认的 Ajax Framework ScriptHandlerFactory,因为这是一个内部类。几乎所有通常处理 Web 服务和 Javascript 代理的 ScriptHandlerFactory 功能都是私有的或内部的。请注意,您从现在开始将看到的大部分功能都是使用 Reflector.net 工具找到的。Web 服务处理程序可以通过 3 种不同的请求访问
-
正常 Web 服务方法请求 (/Devusion.CustomControls.Service.asmx?......)
-
Javascript 代理生成请求 (/Devusion.CustomControls.Service.asmx/js 或 /jsdebug)
-
Javascript 方法调用 (/Devusion.CustomControls.Service.asmx/method)
首先,我们检查是普通请求还是 Javascript 请求
// [REFLECTION] get the Handlerfactory : RestHandlerFactory
// (Handles Javascript proxy Generation and actions)
IHttpHandlerFactory JavascriptHandlerFactory =
(IHttpHandlerFactory)System.Activator.CreateInstance(
AjaxAssembly.GetType("System.Web.Script.Services.RestHandlerFactory"));
// [REFLECTION] Check if the current request is a Javasacript request
// JavascriptHandlerfactory.IsRestRequest(context);
System.Reflection.MethodInfo IsScriptRequestMethod =
JavascriptHandlerFactory.GetType().GetMethod("IsRestRequest",
BindingFlags.Static | BindingFlags.NonPublic);
if ((bool)IsScriptRequestMethod.Invoke(null, new object[] { context }))
{
// Javascript Request
}
else
{
// Normal Request
}
然后对于 Javascript 请求,我们检查是代理请求还是方法请求
// Check and see if it is a Javascript Request or a request for a
// Javascript Proxy.
bool IsJavascriptDebug =
string.Equals(context.Request.PathInfo, "/jsdebug",
StringComparison.OrdinalIgnoreCase);
bool IsJavascript = string.Equals(context.Request.PathInfo, "/js",
StringComparison.OrdinalIgnoreCase);
if (IsJavascript || IsJavascriptDebug)
{
// Proxy Request
}
else
{
// Method Request
}
现在来看这 3 种请求类型中的操作。
JavaScript 代理请求
在这里我们做了更多的反射;每个反射都在注释中进行了解释。我们使用 WebServiceType 创建一个 WebServiceData,它将包含创建 Web 服务所需的所有信息。我们直接在 GetClientProxyScript 方法中使用此对象,该方法是 WebServiceClientProxyGenerator 对象的一个方法。然后它返回 Javascript 代理。
// [REFLECTION] fetch the constructor for the WebServiceData Object
ConstructorInfo WebServiceDataConstructor = AjaxAssembly.GetType(
"System.Web.Script.Services.WebServiceData").GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance, null,
new Type[] { typeof(Type), typeof(bool) } , null);
// [REFLECTION] fetch the constructor for the WebServiceClientProxyGenerator
ConstructorInfo WebServiceClientProxyGeneratorConstructor =
AjaxAssembly.GetType(
"System.Web.Script.Services.WebServiceClientProxyGenerator").GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance, null,
new Type[] { typeof(string), typeof(bool) }, null);
// [REFLECTION] get the method from WebServiceClientProxy to
// create the Javascript : GetClientProxyScript
MethodInfo GetClientProxyScriptMethod =
AjaxAssembly.GetType(
"System.Web.Script.Services.ClientProxyGenerator").GetMethod(
"GetClientProxyScript", BindingFlags.NonPublic | BindingFlags.Instance,
null, new Type[] { AjaxAssembly.GetType(
"System.Web.Script.Services.WebServiceData") }, null);
// [REFLECTION] We invoke :
// new WebServiceClientProxyGenerator(url,false).GetClientProxyScript(
new WebServiceData(WebServiceType));
string Javascript = (string)GetClientProxyScriptMethod.Invoke(
WebServiceClientProxyGeneratorConstructor.Invoke(
new Object[] { url, IsJavascriptDebug }), new Object[]
{
WebServiceDataConstructor.Invoke(
new object[] { WebServiceType, false })
}
);
然后进行一点缓存,这几乎是直接从原始单元使用 Reflector 获取的。
// Check the assembly modified time and use it as caching http header
DateTime AssemblyModifiedDate = GetAssemblyModifiedTime(
WebServiceType.Assembly);
// See "if Modified since" was requested in the http headers,
// and check it with the assembly modified time
string s = context.Request.Headers["If-Modified-Since"];
DateTime TempDate;
if (((s != null) && DateTime.TryParse(s, out TempDate)) && (
TempDate >= AssemblyModifiedDate))
{
context.Response.StatusCode = 0x130;
return null;
}
// Add HttpCaching data to the http headers
if (!IsJavascriptDebug && (
AssemblyModifiedDate.ToUniversalTime() < DateTime.UtcNow))
{
HttpCachePolicy cache = context.Response.Cache;
cache.SetCacheability(HttpCacheability.Public);
cache.SetLastModified(AssemblyModifiedDate);
}
然后我们需要通过 HTTPHandler 将 Javascript 代理字符串返回给客户端。
internal class JavascriptProxyHandler : IHttpHandler
{
string Javascript = "";
public JavascriptProxyHandler(string _Javascript)
{
Javascript = _Javascript;
}
bool IHttpHandler.IsReusable { get { return false; } }
void IHttpHandler.ProcessRequest(HttpContext context)
{
context.Response.ContentType = "application/x-Javascript";
context.Response.Write(this.Javascript);
}
}
然后在 GetHandler 代码中创建并返回它
HttpHandler = new JavascriptProxyHandler(Javascript); return HttpHandler;
Javascript 代理就这些了。我们来看看另外两个更简单的。
JavaScript 方法请求
当您使用 Javascript 代理中生成的方法时,它将通过 /methodname 访问 Web 服务。此处理程序与普通 Web 服务处理程序 (SOAP/XML) 不同的原因是,此接口需要在 Javascript JSON 和我们的 C# 方法之间进行转换。
IHttpHandler JavascriptHandler =
(IHttpHandler)System.Activator.CreateInstance(
AjaxAssembly.GetType("System.Web.Script.Services.RestHandler"));
// [REFLECTION] fetch the constructor for the WebServiceData Object
ConstructorInfo WebServiceDataConstructor = AjaxAssembly.GetType(
"System.Web.Script.Services.WebServiceData").GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance, null,
new Type[] { typeof(Type), typeof(bool) } , null);
// [REFLECTION] get method : JavaScriptHandler.CreateHandler
MethodInfo CreateHandlerMethod = JavascriptHandler.GetType().GetMethod(
"CreateHandler", BindingFlags.NonPublic | BindingFlags.Static, null,
new Type[] { AjaxAssembly.GetType(
"System.Web.Script.Services.WebServiceData"), typeof(string) }, null);
// [REFLECTION] Invoke CreateHandlerMethod :
// HttpHandler = JavaScriptHandler.CreateHandler(WebServiceType,false);
HttpHandler = (IHttpHandler)CreateHandlerMethod.Invoke(
JavascriptHandler, new Object[]
{
WebServiceDataConstructor.Invoke(
new object[] { WebServiceType, false }),
context.Request.PathInfo.Substring(1)
}
);
return HttpHandler;
我们创建 JavascriptHandler,并且像上一个处理程序一样,创建 WebServiceData。然后我们调用 CreateHandler,将 WebServiceData 作为参数,它返回正确的 HTTPHandler。
普通 Web 服务请求
普通请求更简单。该类是公共的,只有接收 WebServiceData 而不是文件路径的函数是私有的。我们使用此函数获取另一个 HttpHandler 并返回它。
// Remember the used factory for later in ReleaseHandler
IHttpHandlerFactory WebServiceHandlerFactory = new WebServiceHandlerFactory();
UsedHandlerFactory = WebServiceHandlerFactory;
// [REFLECTION] Get the method CoreGetHandler
MethodInfo CoreGetHandlerMethod = UsedHandlerFactory.GetType().GetMethod(
"CoreGetHandler", BindingFlags.NonPublic | BindingFlags.Instance);
// [REFLECTION] Invoke the method CoreGetHandler :
// WebServiceHandlerFactory.CoreGetHandler(WebServiceType,context,
context.Request, context.Response);
HttpHandler = (IHttpHandler)CoreGetHandlerMethod.Invoke(UsedHandlerFactory,
new object[]
{
WebServiceType, context, context.Request, context.Response
}
);
return HttpHandler;
Default.aspx
现在来看看这一切的实际效果。首先,我们向 ScriptManager 添加一个 Web 服务引用
<asp:ScriptManager ID="ScriptManager1" runat="server" >
<Services>
<asp:ServiceReference Path="Devusion.CustomControls.asmx" />
</Services>
</asp:ScriptManager>
在它下面,我们放置
<script type="text/Javascript">
function RunHelloWorld(name)
{
name = prompt("Please enter your name :","John Doe");
Devusion.CustomControls.Service.HelloWorldYou(name,
HelloWorldDone,WebServiceCallFailed,'');
}
function HelloWorldDone(result,context,senderFunction)
{
alert(result);
}
function WebServiceCallFailed(error,context,senderFunction)
{
alert(error.get_message());
}
</script>
<input type="button" onclick="RunHelloWorld();" value="Click Here" />
您现在可以在 service.cs 文件中 WebService 类中的 HelloWorldYou 方法中设置断点。只需运行页面,让反射的魔力完成其余的工作!在此示例中,我没有实现实际的自定义控件或所有 Javascript 与 Silverlight 结合的首选 script# 实现。我将在未来的文章中详细介绍。
演示代码
附件中的 ZIP 是一个完整的演示解决方案。它应该可以直接打开和构建,所以如果出现任何问题请告诉我。代码是使用 Visual Studio 2005 Professional 和 ASP.NET Ajax 1.0 创建的。
历史
- 2007 年 5 月 30 日 - 发布原始版本