AjaxPro实现机制探讨——Ajax是如何调用服务器端C#方法的?
谈起 Ajax 做过 web 开发的都非常熟悉,就是通过 xmlhttp request 与服务器端通信而避免页面刷新。关于 Ajax 是如何运作的,网上有很多帖子解释其各 js 文件的作用及调用 xmlhttp 的原理。但 Ajax 到底是怎么调用服务器端的 C# 代码的呢?怎么让后台的方法运行并将结果反馈给 xmlhttp 的呢?曾经有个同事问起我这个问题,我还真懵了!本以为象 .Net 1.1 下通过 form 传递必要的 EventName 及 EventPara 等参数传给服务器端继而解析后执行对应的事件一样来调用 C# 代码的( .net 调用事件机制也不全是这么回事,待探讨),但通过仔细研究,发现原来远不是这么回事,而网上更深入的文章却少之又少。
我们由浅到深吧,先看看相对表象的东西,即前台 Ajax 相关的 JavaScript 代码部分。之所以说相对肤浅和表象,是因为这些资料很多网友已经撰文解读过。
凡要使用 AjaxPro ,我们大致要做以下工作:
1) 在项目中引用 AjaxPro.dll (我用的是 AjaxPro.2.dll ,版本 6.6.13.2 ),并在 web.config 中 httpHandlers 配置节添加:
< add verb = " POST,GET " path = " ajaxpro/*.ashx " type = " AjaxPro.AjaxHandlerFactory, AjaxPro.2 " />
2) 在要使用 Ajax 功能的页面 .cs 文件上注册 Ajax ,例如:
protected void Page_Load( object sender, EventArgs e)
{
// 注册Ajax
AjaxPro. Utility .RegisterTypeForAjax( typeof ( Default ));
}
3) 在 .cs 文件中声明可以被 Ajax 调用的函数(或属性),如:
[AjaxPro. AjaxMethod ]
public string GetChild( string parentId)
{
return "return value from .cs file" ;
}
4) 在 .aspx 文件中用 JavaScript 调用 Ajax ,如:
< script language ="javascript">
var items = DynLoadTree.Default.GetChild( "aa" ).value; // 通过Ajax 调用后台代码
alert(items);
</ script >
做好以上四步,我们就基本实现了 Ajax 页面不刷新的功能了。那么它是怎样通过 xmlhttp 与服务器通讯的呢?运行后我们可以看到 HTML 文件的源代码多了几行 .ashx 文件的引用:
<script type="text/javascript" src="/ajaxpro/prototype.ashx"></script>
<script type="text/javascript" src="/ajaxpro/core.ashx"></script>
<script type="text/javascript" src="/ajaxpro/converter.ashx"></script>
<script type="text/javascript" src="/ajaxpro/DynLoadTree.Default,DynLoadTree.ashx"></script>
实际上这些 .ashx 就是在上面第 2 步 AjaxPro. Utility .RegisterTypeForAjax 注册Ajax 时自动将这些引用添加到Html 文档输出的。那这些文件是什么文件呢?再看第1 步中在web.config 中添加到httpHandlers 节中的配置,它告诉系统凡是收到ajaxpro 路径下已经ashx 为后缀的请求就全部交给AjaxPro.AjaxHandlerFactory 这个类来处理,而这些ashx 经过处理后返回的就是一些JavaScript 文件,和普通的js 引用没有实质区别。
我们首先看看“ DynLoadTree.Default,DynLoadTree.ashx ”的内容:
if(typeof DynLoadTree == "undefined") DynLoadTree={};
DynLoadTree.Default_class = function() {};
Object.extend(DynLoadTree.Default_class.prototype, Object.extend(new AjaxPro.AjaxClass(), { GetChild: function(parentId) {
return this.invoke("GetChild", {"parentId":parentId}, this.GetChild.getArguments().slice(1));
},url: '/ajaxpro/DynLoadTree.Default,DynLoadTree.ashx'}));
DynLoadTree.Default = new DynLoadTree.Default_class();
原来我们 DynLoadTree.Default 是在这里定义的,而这个 GetChild 方法最终是调用“ this.invoke("GetChild", {"parentId":parentId}, this.GetChild.getArguments().slice(1)); ”的,而 invoke 方法是在“ core.ashx ”中定义的。在 core.ashx 中定义了很多 Ajax 核心的 js 方法,例如 Object.extand 实现简单的继承(或阅扩展)。在 invoke 方法里,首先是 new 了一个 XmlHttp 对象,然后重点做了几件事:
this.xmlHttp.open("POST", this.url, async);
this.xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
this.xmlHttp.setRequestHeader("X-" + AjaxPro.ID + "-Method", method);
this.xmlHttp.send(json);
xmlHttp.open 说明了是向哪个服务器 url 发送请求,是同步请求还是异步请求。接下来就设置 Content-Type 的 http header ,然后再将 method 设置到 http header 中,以让服务器端知道要调用什么方法,最后 send 出去,同时参数 json 包含了调用这个方法所需的参数。至此,利用 xmlhttp 已经将请求发送给服务器了,接下来就等待服务器的反馈结果了(对于同步和异步不同的调用方式,对结果的处理是有区别的)。
但是,为什么这样一个请求给服务器后,服务器就自动调用制定的 method 呢?如果仔细一点,你可以发现 xmlHttp.open 里的 this.url 到底是什么?是要调用的页面的地址么?实际不是,这个 this.url 的值是“ /ajaxpro/DynLoadTree.Default,DynLoadTree.ashx ”。第一次看到这里的时候,我很诧异,怎么这个 xmlhttp 请求也发给一个 ashx 文件了呢?难道 ashx 文件不仅仅是用来动态生成 js 文件的么?同上,在 web.config 中已经配置了凡是 ashx 文件都交由类 AjaxPro.AjaxHandlerFactory 来处理,要想明白其中的奥秘,还得看看 AjaxHandlerFactory 里到底都干了些什么。为此,我用 Reflector 对 AjaxPro.2.dll 文件进行反编译(我的资源里提供下载),看了 AjaxHandlerFactory 的代码才大彻大悟!
原来,在 AjaxHandlerFactory 的 GetHandler 方法里是这么写的:
public IHttpHandler GetHandler ( HttpContext context, string requestType, string url, string pathTranslated)
{
……
string str2 = requestType;
if (str2 != null )
{
if (!(str2 == "GET" ))
{
if (str2 == "POST" )
{
if (!(! Utility . Settings . OnlyAllowTypesInList || flag))
{
return null ;
}
IAjaxProcessor [] processorArray = new IAjaxProcessor [] { new XmlHttpRequestProcessor (context, type), new IFrameProcessor (context, type) };
for ( int i = 0 ; i < processorArray. Length ; i++)
{
if (processorArray[i]. CanHandleRequest )
{
if (exception != null )
{
processorArray[i]. SerializeObject ( new NotSupportedException ( "This method is either not marked with an AjaxMethod or is not available." ));
return null ;
}
AjaxMethodAttribute [] customAttributes = ( AjaxMethodAttribute []) processorArray[i]. AjaxMethod . GetCustomAttributes ( typeof ( AjaxMethodAttribute ), true );
bool useAsyncProcessing = false ;
HttpSessionStateRequirement readWrite = HttpSessionStateRequirement . ReadWrite ;
if ( Utility . Settings . OldStyle . Contains ( "sessionStateDefaultNone" ))
{
readWrite = HttpSessionStateRequirement . None ;
}
if (customAttributes. Length > 0 )
{
useAsyncProcessing = customAttributes[ 0 ]. UseAsyncProcessing ;
if (customAttributes[ 0 ]. RequireSessionState != HttpSessionStateRequirement . UseDefault )
{
readWrite = customAttributes[ 0 ]. RequireSessionState ;
}
}
switch (readWrite)
{
case HttpSessionStateRequirement . ReadWrite :
if (useAsyncProcessing)
{
return new AjaxAsyncHttpHandlerSession (processorArray[i]);
}
return new AjaxSyncHttpHandlerSession (processorArray[i]);
case HttpSessionStateRequirement . Read :
if (useAsyncProcessing)
{
return new AjaxAsyncHttpHandlerSessionReadOnly (processorArray[i]);
}
return new AjaxSyncHttpHandlerSessionReadOnly (processorArray[i]);
case HttpSessionStateRequirement . None :
if (useAsyncProcessing)
{
return new AjaxAsyncHttpHandler (processorArray[i]);
}
return new AjaxSyncHttpHandler (processorArray[i]);
}
if (!useAsyncProcessing)
{
return new AjaxSyncHttpHandlerSession (processorArray[i]);
}
return new AjaxAsyncHttpHandlerSession (processorArray[i]);
}
}
}
}
else
{
switch (fileNameWithoutExtension. ToLower ())
{
case "prototype" :
return new EmbeddedJavaScriptHandler ( "prototype" );
case "core" :
return new EmbeddedJavaScriptHandler ( "core" );
……
default :
return new TypeJavaScriptHandler (type);
}
}
}
return null ;
}
它首先对requestType 进行判断,如果是“GET ”请求,则说明是html 里对被引用的ashx 文件的下载请求,则调用相应的Handler 去生成对应的JavaScript 内容输出到客户端;如果是“POST” 请求,则说明是通过XMLHTTP 发送过来的,是请求调用服务器端方法的,则返回相应的Handler 利用反射机制调用请求的方法。
首先看看“GET ”请求,对“GET ”请求的处理很简单,根据不同的文件名返回不同的Handler, 对于“core ”及“prototype ”则返回 EmbeddedJavaScriptHandler ,对于“DynLoadTree.Default,DynLoadTree.ashx ”则返回 TypeJavaScriptHandler 。在 EmbeddedJavaScriptHandler 中,构造函数的参数表示要请求的是哪个文件,然后在ProcessRequest 函数中提取指定的文件内容并输出到客户端,其实这些文件内容都是固定的,且已经放在资源里的:
internal class EmbeddedJavaScriptHandler : IHttpHandler
{
// Fields
private string fileName ; // Methods
internal EmbeddedJavaScriptHandler ( string fileName)
{
this . fileName = fileName;
} public void ProcessRequest ( HttpContext context)
{
……
string [] strArray = this . fileName . Split ( new char [] { ',' });
Assembly executingAssembly = Assembly . GetExecutingAssembly ();
for ( int i = 0 ; i < strArray. Length ; i++)
{
Stream manifestResourceStream = executingAssembly. GetManifestResourceStream ( "AjaxPro.2." + strArray[i] + ".js" );
if (manifestResourceStream != null )
{
StreamReader reader = new StreamReader (manifestResourceStream);
context. Response . Write (reader. ReadToEnd ());
context. Response . Write ( "/r/n" );
reader. Close (); if ((strArray[i] == "prototype" ) && Utility . Settings . OldStyle . Contains ( "objectExtendPrototype" ))
{
context. Response . Write ( "/r/nObject.prototype.extend = function(o, override) {/r/n/treturn Object.extend.apply(this, [this, o, override != false]);/r/n}/r/n" );
}
}
}
………
}
……
}
对于“ DynLoadTree.Default,DynLoadTree.ashx ”的请求,则交给 TypeJavaScriptHandler 处理:
internal class TypeJavaScriptHandler : IHttpHandler , IReadOnlySessionState , IRequiresSessionState
{
// Fields
// Methods
internal TypeJavaScriptHandler ( Type type);
public void ProcessRequest ( HttpContext context);
// Properties
public bool IsReusable { get ; }
}
ProcessRequest 会根据Type 动态生成JavaScript 内容并输出到客户端。 对于requestType 是“POST ”的请求,则返回相应的Handler 进行处理。以 AjaxSyncHttpHandler 为例:
internal class AjaxSyncHttpHandler : IHttpHandler
{
// Fields
private IAjaxProcessor p ; // Methods
internal AjaxSyncHttpHandler ( IAjaxProcessor p)
{
this . p = p;
} public void ProcessRequest ( HttpContext context)
{
new AjaxProcHelper ( this . p ). Run ();
} // Properties
public bool IsReusable
{
get
{
return false ;
}
}
}
其中ProcessRequest 方法就就新建一个AjaxProcHelper 对象,用该对象的Run 方法来处理实质请求。可以简略看看AjaxProcHelper.Run 的代码:
internal void Run ()
{
……
this . p . Context . Response . Expires = 0 ;
this . p . Context . Response . Cache . SetCacheability ( HttpCacheability . NoCache );
this . p . Context . Response . ContentType = this . p . ContentType ;
this . p . Context . Response . ContentEncoding = Encoding . UTF8 ;
……
object [] args = null ;
object o = null ;
args = this . p . RetreiveParameters ();
string key = string . Concat ( new object [] { this . p . Type . FullName , "|" , this . p . GetType (). Name , "|" , this . p . AjaxMethod . Name , "|" , this . p . GetHashCode () });
if ( this . p . Context . Cache [key] != null )
{
this . p . Context . Response . AddHeader ( "X-AjaxPro-Cache" , "server" );
this . p . Context . Response . Write ( this . p . Context . Cache [key]);
}
else
{
……
if ( this . p . AjaxMethod . IsStatic )
{
o = this . p . Type . InvokeMember ( this . p . AjaxMethod . Name , BindingFlags . InvokeMethod | BindingFlags . Public | BindingFlags . Static | BindingFlags . IgnoreCase , null , null , args);
}
else
{
……
object obj3 = Activator . CreateInstance ( this . p . Type , new object [ 0 ]);
o = this . p . AjaxMethod . Invoke (obj3, args);
}
……
if ((o != null ) && (o. GetType () == typeof ( XmlDocument )))
{
this . p . Context . Response . ContentType = "text/xml" ;
(( XmlDocument ) o). Save ( this . p . Context . Response . OutputStream );
}
……
}
}
可以清晰的看到,Run 中是通过反射机制调用相应的方法,再将结果写入context 输出到客户端的。
另外,我们也可以清晰的看到 Utility 中对RegisterTypeForAjax 的几个重载及实现方式:
public static void RegisterTypeForAjax ( Type type);
public static void RegisterTypeForAjax ( Type type, Page page);
同时,也可以看看AjaxMethodAttribute 的定义(有关Attribute MSDN 中有详细的描述和实例):
[ AttributeUsage ( AttributeTargets . Method , AllowMultiple = false )]
public class AjaxMethodAttribute : Attribute
{
// Fields
private HttpSessionStateRequirement requireSessionState ;
private bool useAsyncProcessing ; // Methods
public AjaxMethodAttribute ();
public AjaxMethodAttribute ( HttpSessionStateRequirement requireSessionState);
[ Obsolete ( "The use of this argument is currently in beta state, please report any problems to bug@schwarz-interactive.de." )]
public AjaxMethodAttribute ( bool useAsyncProcessing);
[ Obsolete ( "The recommended alternative is AjaxPro.AjaxServerCacheAttribute." , true )]
public AjaxMethodAttribute ( int cacheSeconds);
[ Obsolete ( "The recommended alternative is AjaxPro.AjaxNamespaceAttribute." , true )]
public AjaxMethodAttribute ( string methodName);
[ Obsolete ( "The use of this argument is currently in beta state, please report any problems to bug@schwarz-interactive.de." )]
public AjaxMethodAttribute ( HttpSessionStateRequirement requireSessionState, bool useAsyncProcessing);
[ Obsolete ( "The recommended alternative is AjaxPro.AjaxServerCacheAttribute." , true )]
public AjaxMethodAttribute ( int cacheSeconds, HttpSessionStateRequirement requireSessionState);
[ Obsolete ( "The recommended alternative for methodName is AjaxPro.AjaxNamespaceAttribute." , true )]
public AjaxMethodAttribute ( string methodName, HttpSessionStateRequirement requireSessionState);
[ Obsolete ( "The recommended alternative for methodName is AjaxPro.AjaxNamespaceAttribute." , true )]
public AjaxMethodAttribute ( string methodName, int cacheSeconds);
[ Obsolete ( "The recommended alternative for methodName is AjaxPro.AjaxNamespaceAttribute." , true )]
public AjaxMethodAttribute ( string methodName, int cacheSeconds, HttpSessionStateRequirement requireSessionState); // Properties
internal HttpSessionStateRequirement RequireSessionState { get ; }
internal bool UseAsyncProcessing { get ; }
}