CURRENT PROJECTS
loading
CATEGORIES AND POSTS
loading
overset
DEVELOPMENT LOG FOR JIM PALMER
Posted 07/16/2008 in C#.NET


GOAL
To build a robust and fast AJAX handler facility utilizing Reflection to access remote methods in classes without using ASP.NET's AJAX library and the [ScriptService] decorator and work with jQuery's $.ajax() or $.post() facilities.

I must disclaim that the [ScriptService] [WebMethod] decorators allows for an awesomely simple way to expose methods as web services both as SOAP and JSON for AJAX calls, but the goal of this writeup is not to use it.

What I came up with is a simple ASP Page_Load that can handing incoming AJAX requests with simple function name and argument list variables embedded in a normal POST as follows:
_a=%5B%27first%27%2C%27second%27%5D&_f=com.overset.Util.echoArgs
_a serves as the JSON serialized argument array of strings, i.e. ['arg1','arg2']
_f serves as the fully qualified namespace+class+function and directs reflection to look in the correct place.

In the event that a public method within a public class within the namespace specified with the appropriate arguments - the function will be called and the response mapped to a custom Hashtable which is then returned as a JSON string through a reponse with the "Content-Type: text/javascript" header. This custom Hashtable JSON string is of the format:
{"_s":"","_m":"","_d":""}
_s Is the status of the function response; ["OK","ERR"]
_m Is a string containing any custom error messages returned from the reflected function
_d Contains the data returned from the reflected function as an object in the form of a Hashtable/IDictionary Type, but not required to be those.
Here is the C#.ASP ah.aspx file that contains the Page_Load handler as well as a global Page_Error handler to catch uncaught exceptions and present them back to the user's browser in an AJAX friendly mannor (no code-behind here):
<%@ Page Language="C#" AutoEventWireup="true" Trace="false" Debug="false" EnableSessionState="false" ValidateRequest="false" %>

<%@ Assembly	Name		= "LitJson" %>
<%@ Import		Namespace	= "LitJson" %>
<%@ Import		Namespace	= "System.Globalization" %>
<%@ Import		Namespace	= "System.Reflection" %>

<script runat="server">

	/// <summary>
	///		container class to instantiate the core return structure
	/// </summary>
	public class ajaxReturnStruct {

		public Hashtable retStr = new Hashtable();

		public ajaxReturnStruct (string ajaxStatus, object ajaxData) {
			this.retStr["_s"] = ajaxStatus;
			this.retStr["_d"] = ajaxData;
			this.retStr["_m"] = "";
		}
		public ajaxReturnStruct (string ajaxStatus, object ajaxData, string ajaxMessage) {
			this.retStr["_s"] = ajaxStatus;
			this.retStr["_d"] = ajaxData;
			this.retStr["_m"] = ajaxMessage;
		}
	}
	
	/// <summary>
	///		new custom Exception class so that we can properly handle exceptions from remote reflection method calls
	/// </summary>
	public class ajaxException : Exception {
		public string ajaxStatus;
		public string stackTrace;
		public ajaxException() { 
			this.ajaxStatus	= "err";
			this.stackTrace = "";
		}
		public ajaxException(string ajaxMessage) : base(ajaxMessage) {
			this.ajaxStatus	= "err";
			this.stackTrace = "";
		}
		public ajaxException(string ajaxExStatus, string ajaxExMessage) : base(ajaxExMessage) {
			this.ajaxStatus	= ajaxExStatus;
			this.stackTrace = "";
		}
		public ajaxException(string ajaxExStatus, string ajaxExData, string ajaxExMessage) : base(ajaxExMessage) {
			this.ajaxStatus	= ajaxExStatus;
			this.stackTrace = ajaxExData;
		}
	}


	/// <summary>
	///		This is the core Page_Load function that drives the AJAX handling of remote methods via reflection
	/// </summary>
	protected void Page_Load(object sender, System.EventArgs e) {

		if ( Request.Form["_f"] != null && Request.Form["_a"] != null ) {

			string[] assmQNameArray;
			string assmQName;
			JsonData parseData;
			Type dynType;
			object foundObj;
			Hashtable invokeMeth;
			string catchMethName;
			object[] invArgs = new object[1];

			// instantiate the correct [namespace.assembly.method,assembly] via reflection
			try {
				// now parse actual data from JSON to native .net object
				parseData = JsonMapper.ToObject(Request.Form["_a"]);
				// fetch the AssemblyQualifiedName from the function string in the POST vars
				assmQNameArray = Request.Form["_f"].Split(".".ToCharArray());
				assmQName = String.Join(".", assmQNameArray, 0, (assmQNameArray.Length - 1) );
				// attempt to find function - throw on error and ignore case
				dynType = Type.GetType(assmQName + "," + assmQName, true, true);
				// create instance of Type found - nonPublic=true
				foundObj = Activator.CreateInstance(dynType, true);
			} catch (Exception cEx) {
				throw(new ajaxException("err", "Cannot find dynamic function -- " + cEx.Message));
			}

			// build the arguments array and assume the arguments will always be a string
			if ( parseData.IsArray ) {
				invArgs = new object[parseData.Count];
				for ( int pdI=0; pdI < parseData.Count; pdI++ ) {
					if ( parseData[pdI].IsBoolean ) 
						invArgs[pdI] = (bool)parseData[pdI];
					if ( parseData[pdI].IsDouble ) 
						invArgs[pdI] = (double)parseData[pdI];
					if ( parseData[pdI].IsInt ) 
						invArgs[pdI] = (int)parseData[pdI];
					if ( parseData[pdI].IsLong ) 
						invArgs[pdI] = (long)parseData[pdI];
					if ( parseData[pdI].IsString ) 
						invArgs[pdI] = (string)parseData[pdI];
				}
			} else {
				throw(new ajaxException("err", "Data must be sent as 1D array of simple data types."));
			}

			// attempt to execute the remote method via reflection
			try {
				invokeMeth = (Hashtable) dynType.InvokeMember( assmQNameArray[assmQNameArray.Length - 1], BindingFlags.InvokeMethod, null, foundObj, invArgs );
			} catch (TargetInvocationException tie) {
				// To discover the real cause of a remote Exception you must catch TargetInvocationException and examine the inner exception. 
				throw(new ajaxException("err", tie.InnerException.StackTrace.ToString(),  "Unexpected error calling dynamic function -- " + tie.InnerException.Message.ToString()));
			}

			Response.AddHeader("Content-Type", "text/javascript");
			// prepend the escaping comments
			Response.Write("/****/\n");
			// build response envelope
			ajaxReturnStruct aRet = new ajaxReturnStruct((string)invokeMeth["_s"], (string)invokeMeth["_d"], (string)invokeMeth["_m"]);
			// serialize response envelope and write to the response
			Response.Write( JsonMapper.ToJson( aRet.retStr ) );

		} else {
		
			throw(new ajaxException("err", "You must include the _f (function) and _a (arguments) arguments in the POST data."));
		
		}

	}
	
	/// <summary>
	///		This will allow for the Page directive to handle errors and return them to the client in the AJAX+JSON format
	/// </summary>
	public void Page_Error(object sender, System.EventArgs e) {
		Response.Clear();
		// ensure we escape any prior sent javascript comments (paranoid)
		Response.Write("/****/\n");
		ajaxReturnStruct retStr;
		// handle the display of our custom ajaxEception object
		if ( Server.GetLastError().GetBaseException().GetType() == typeof(ajaxException) ) {
			ajaxException objErr = (ajaxException)Server.GetLastError().GetBaseException();
			retStr = new ajaxReturnStruct(objErr.ajaxStatus, objErr.stackTrace, objErr.Message.ToString());
		} else {
			Exception objErr = Server.GetLastError().GetBaseException();
			retStr = new ajaxReturnStruct("err", objErr.StackTrace.ToString(), objErr.Message.ToString());
		}
		Response.AddHeader("Content-Type", "text/javascript");
		Response.Write( (string)JsonMapper.ToJson( retStr.retStr ) );
		Server.ClearError();
	}

</script>

Here is the actual C# class that contains the method we want to invoke through reflection:
// csc.exe /nologo /t:library /out:Bin\com.overset.Util.dll /r:Bin\LitJson.dll com.overset.Util.cs
namespace com.overset {

	using System;
	using System.Collections;
	using LitJson;

	public class Util {
	
		public Hashtable retStr;

		/// <summary>
		///		constructor creates return structure object
		/// </summary>
		public Util () { 
			this.retStr = new Hashtable();
		}

		/// <summary>
		///		sets the data in the RetStruct to a string 
		///		of the 2 arguments appended together
		/// </summary>
		public object echoArgs (string arg1, string arg2) {
			this.retStr["_s"] = "OK";
			this.retStr["_d"] = arg1 + arg2;
			this.retStr["_m"] = "";
			return this.retStr;
		}

	}

}

And here's a sample jQuery $.ajax() example on how to format the POST request envelope to instantiate the remote function:
<html>
<head>
	<script language="JavaScript" type="text/javascript" src="jquery.js"></script>
	<script>
		$(document).ready( function () {
				$.ajax({
					data:		'&_f=com.overset.Util.echoArgs&_a=[\'first\',\'second\']',
					dataType:	'json',
					cache:		false,
					success: 	function(result, textStatus) {
						alert(	'status: ' + result._s + '\n' +
								'data: ' + result._d + '\n' +
								'message: ' + result._m);
					},
					type:		'POST',
					url:		'ah.aspx'
				});
			});
	</script>
</head>
</html>

Instructions to build simple test environment:
When you then access the index.html from your wwroot the jQuery.ajax() function should then attempt an AJAX call with the following POST data:
&_f=com.overset.Util.echoArgs&_a=%5B%27first%27%2C%27second%27%5D
And should get the following response:
Content-Type: text/javascript; charset=utf-8
/****/
{"_s":"OK","_m":"","_d":"firstsecond"}
comments
loading
new comment
NAME
EMAIL ME ON UPDATES
EMAIL (hidden)
URL
MESSAGE TAGS ALLOWED: <code> <a> <pre class="code [tab4|tabX|inline|bash]"> <br>
PREVIEW COMMENT
TURING TEST
gravatar