.net 3.0+, Ajax calls allow for true asynchronous communication through a browser. You may not notice it until you have a server call that takes awhile, but just because AJAX is allowing it, that doesn't mean the server is acting in a truly asynchronous manner. By default aspx pages will asynchronously allow multiple calls to the same session, however, a thread-lock is placed on each subsequent call so that each incoming request must wait for the previous to finish before the response stream can be accessed. This is a nice protection measure to keep a page programmer from ripping their hair out because of multi-threading data access issues and dead-lock scenarios; but it is inversely annoying to one trying to allow asynchronicity. The solution to allow true multi-threaded asynchronous behavior isn't an easy one, but the following code is a step in the right direction. The first thing you should know is that to make a page act asynchronously is to set the "Async" attribute in the page directive of the aspx page being called <%@ Page Async="true"... This will force the page to implement the IHttpAsyncHandler interface, as well as build in some asynchronous request behavior that wasn't included in .net version 1.0 framework. At first you may think, "Hey, the page is now asynchronous, so it is simple right?" Wrong. Just because the page is now asynchronous, how do you handle the responses back to the client that called the page? The Page.AddOnPreRenderCompleteAsync() function allows you accomplish this feat, but it is very vanilla plain when it comes to features.
/* !Important! Make sure you have a Web Garden ("Maximum Worker Processes" in IIS7) value greater than 1 set up in IIS for the web application. The website will need multiple threads of the site in order to run multi-threaded and asynchronously. Also, this seemed to lock-up in IIS6 occasionally, causing it to act synchronously. I deleted the application pool and created a new one to fix this. Main Class (AsyncCall.cs), place in App_Code directory or compile as a dll and place in the bin dirctory of the site: ------------------------------------------------------------------------------- */ using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Net; using System.Threading; using System.IO; using System.Collections.Specialized; namespace AsyncStuff { public static class AsyncCall { public sealed class AsyncState { internal const string QVARNAME = "ASYNCSession"; public readonly Page CallingPage; internal readonly Uri Url; private HttpWebRequest _request; private HttpWebResponse _response; internal readonly SpecificFunction SpecificFunction; public readonly object[] Parameters; private string _text; internal byte[] FormContent; //cascaded constructor only (private) private AsyncState(Page page) { this.CallingPage = page; this.Url = GetAsyncUrl(page.Request.Url); } //off-page url internal AsyncState(Uri url, Page page, SpecificFunction specificFunction) { this.CallingPage = page; this.Url = GetAsyncUrl(url); this.SpecificFunction = specificFunction; } internal AsyncState(Page page, SpecificFunction specificFunction) : this(page) { this.SpecificFunction = specificFunction; } internal AsyncState(Page page, SpecificFunction specificFunction, params object[] parameters) : this(page, specificFunction) { this.Parameters = parameters; } private Uri GetAsyncUrl(Uri uri) { string url = uri.OriginalString; int start = url.IndexOf("?"); if (start < 0) { start = url.Length; url = url.Insert(start++, "?"); } else { start++; url = url.Insert(start, "&"); } //timestamped query variable inserted to indicate that a page is running asychronously url = url.Insert(start, QVARNAME + "=" + DateTime.Now.ToString("MMddyyyyHHmmssffff")); } public HttpWebRequest Request { get { return this._request; } internal set { this._request = value; } } public HttpWebResponse Response { get { return this._response; } internal set { this._response = value; } } public string GetResponseText() { if (this._text == null) { this._text = ""; if (this._response != null) } if (this._text == "") return null; return this._text; } } /// <summary> /// A delegate function used to create a callback for the RunPageAsynchronously() function /// </summary> /// <param name="state">The state object that will be returned from the results of the asychronous call</param> public delegate void SpecificFunction(AsyncState state); /// <summary> /// Run an off-page url request as an asychronous request. (Note: this is a seperate session, so don't expect session variables to persist) /// </summary> /// <param name="url">Off-page URL which contents will be retreived asychronously</param> /// <param name="callingPage">The current System.Web.UI.Page object which is making the asychronous call (must implement IHttpAsyncHandler or use the [%@ Page Async="true"] directive)</param> /// <returns>true, if the page was able to run asynchronously (a value of false may indicate the %@ Page directive is not using Async="true"</returns> public static bool RunAsynchronously(string url, Page callingPage) { { if (!absUrl.IsAbsoluteUri || absUrl.OriginalString.StartsWith("file")) { string resolve = callingPage.ResolveUrl(url); Uri pageUrl = callingPage.Request.Url; string newPath = pageUrl.OriginalString.Replace(pageUrl.PathAndQuery, resolve); string filePath = callingPage.MapPath(absUrl.AbsolutePath); if (!fi.Exists) throw new Exception("File Not Found, requestsed relative path does not exist. Check the filename for spelling errors."); } return true; } return false; } private static void AfterOffPage(AsyncState state) { if (state==null || state.Response == null || state.CallingPage==null || state.CallingPage.Response==null) return; //content rewrite (make calling page same as the page called) //requires IIS content rewrite pipline mode, so if exception then ignore try { state.CallingPage.Response.Headers.Clear(); state.CallingPage.Response.Headers.Add(state.Response.Headers); } catch { } state.CallingPage.Response.ContentType = state.Response.ContentType; int len = (int)state.Response.ContentLength; byte[] data = br.ReadBytes(len); state.CallingPage.Response.OutputStream.Write(data, 0, len); state.CallingPage.Response.End(); } /// <summary> /// Run the current page request as an asychronous request. (Note: this is a seperate session, so don't expect session variables to persist) /// </summary> /// <param name="callingPage">The current System.Web.UI.Page object to be processed (must implement IHttpAsyncHandler or use the [%@ Page Async="true"] directive)</param> /// <param name="specificFunction">A parameterless function to call on the async request page</param> /// <returns>true, if the page was able to run asynchronously (a value of false may indicate the %@ Page directive is not using Async="true"</returns> public static bool RunAsynchronously(Page callingPage, SpecificFunction specificFunction) { { return true; } return false; } /// <summary> /// Run the current page request as an asychronous request. (Note: this is a seperate session, so don't expect session variables to persist) /// </summary> /// <param name="callingPage">The current System.Web.UI.Page object to be processed (must implement IHttpAsyncHandler or use the [%@ Page Async="true"] directive)</param> /// <param name="specificFunction">A function to call on the async request page</param> /// <param name="parameters">The parameters to pass to the specific function</param> /// <returns>true, if the page was able to run asynchronously (a value of false may indicate the %@ Page directive is not using Async="true"</returns> public static bool RunAsynchronously(Page callingPage, SpecificFunction specificFunction, params object[] parameters) { { return true; } return false; } /// <summary> /// Tests to see if the page is in is asynchronous cycle of an asynchronous request made from the RunPageAsynchronously function /// </summary> /// <param name="page">The page to check for sychronicity</param> /// <returns>true if in the asychronous cycle</returns> public static bool IsRunningAsync(Page page) { return (page.Request.QueryString[AsyncState.QVARNAME] != null); } private static IAsyncResult BeginAsyncOperation(object sender, EventArgs e, AsyncCallback cb, object stateObj) { { AsyncState state = (AsyncState)stateObj; state.Request = (HttpWebRequest)HttpWebRequest.Create(state.Url); //copy relevant header information state.Request.Accept = GetTextList<string>(state.CallingPage.Request.AcceptTypes, ", ", true); state.Request.AllowAutoRedirect = true; state.Request.AllowWriteStreamBuffering = true; state.Request.ContentType = state.CallingPage.Request.ContentType + "; " + state.CallingPage.Request.ContentEncoding.WebName; state.Request.Method = state.CallingPage.Request.RequestType; state.Request.Referer = state.CallingPage.Request.UrlReferrer.OriginalString; //copy cookies if (state.CallingPage.Request.Cookies.Count > 0) { foreach (string key in state.CallingPage.Request.Cookies.Keys) { if (!excludedCookies.Contains(key, StringComparer.CurrentCultureIgnoreCase)) { HttpCookie cookie = state.CallingPage.Request.Cookies[key]; state.Request.CookieContainer.Add(copy); } } } //copy form variables if (state.CallingPage.Request.Form.Count > 0 && state.Request.Method.Equals("POST", StringComparison.CurrentCultureIgnoreCase)) { string pairs = GetTextDictionary(state.CallingPage.Request.Form, "=", "&", true); state.FormContent = state.CallingPage.Request.ContentEncoding.GetBytes(pairs); state.Request.ContentLength = state.FormContent.Length; state.Request.BeginGetRequestStream(EndRequestStreamCallback, state); } return state.Request.BeginGetResponse(cb, stateObj); } return null; } private static void EndRequestStreamCallback(IAsyncResult ar) { { AsyncState state = (AsyncState)ar.AsyncState; sw.Write(state.FormContent, 0, state.FormContent.Length); sw.Close(); } } private static void EndAsyncOperation(IAsyncResult ar) { { AsyncState state = (AsyncState)ar.AsyncState; if (state.Request != null) { try { state.Response = (HttpWebResponse)state.Request.EndGetResponse(ar); } catch (Exception ex) { throw new Exception("WebRequest Error. Remember that asynchronous calls are made from the server and not the client, so any routing done on the client (say...to a testing server) will not be in effect with this request.", ex); }; if (state.SpecificFunction != null) { object target = state.SpecificFunction.Target; if (target == null) target = state.CallingPage; catch (Exception ex) { throw new Exception("An error occured in the SpecificFunction supplied, the debug thread is not attached to the function invoked, therefore further debug information is unavailable.", ex); } } } } } private static string GetTextList<I>(IEnumerable<I> list, string separator, bool urlEncode) { int index = 0; foreach (I item in list) { if (index > 0) sb.Append(separator); if (urlEncode) sb.Append(HttpUtility.UrlPathEncode(item.ToString())); else sb.Append(item.ToString()); index++; } string ret = sb.ToString(); sb.Length = 0; //destroy memory return ret; } private static string GetTextDictionary<K, V>(IDictionary<K, V> dictionary, string equality, string separator, bool urlEncode) { int index = 0; foreach (K key in dictionary.Keys) { V value = dictionary[key]; if (index > 0) sb.Append(separator); if (urlEncode) sb.Append(HttpUtility.UrlPathEncode(key.ToString())); else sb.Append(key.ToString()); sb.Append(equality); if (urlEncode) sb.Append(HttpUtility.UrlPathEncode(value.ToString())); else sb.Append(value.ToString()); index++; } string ret = sb.ToString(); sb.Length = 0; //destroy memory return ret; } private static string GetTextDictionary(NameValueCollection dictionary, string equality, string separator, bool urlEncode) { int index = 0; foreach (string key in dictionary.Keys) { string value = dictionary[key]; if (index > 0) sb.Append(separator); if (urlEncode) sb.Append(HttpUtility.UrlPathEncode(key.ToString())); else sb.Append(key.ToString()); sb.Append(equality); if (urlEncode) sb.Append(HttpUtility.UrlPathEncode(value.ToString())); else sb.Append(value.ToString()); index++; } string ret = sb.ToString(); sb.Length = 0; //destroy memory return ret; } } } /* Page Example (AsyncPage.aspx): ------------------------------------------------------------------------------- */ <%@ Page Async="true" Language="C#" AutoEventWireup="true" CodeFile="AsyncPage.aspx.cs" Inherits="AsyncPage" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <script type="text/javascript"> function AJAXCall(url, callBack, postData) { //setup the callback var out = callBack; if (!out) { out = function(text) { return; } } //setup the request var request = null; if (window.XMLHttpRequest) else if (window.ActiveXObject) else return false; //true for async.. request.open("POST", url, true); //setup the handle of the request when the status changes request.onreadystatechange = function() { if (request && request.readyState == 4) { //if (request.status == 200) out(request.responseText); } } //setup the request headers request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); //send the request if (postData) request.send(postData); else request.send(""); } function callback(data) { alert('got here: ' + data); } function button1() { AJAXCall("AsyncPage.aspx?LongFunction=1", callback, "testing=1"); } function button2() { AJAXCall("AsyncPage.aspx?ShortFunction=1", callback, "testing=1"); } function button3() { AJAXCall("AsyncPage.aspx?UrlFunction=1", callback, "testing=1"); } </script> </head> <body> <form id="form1" runat="server"> <div> <asp:Label ID="lbl1" runat="server" /> <input type="button" id="testbutton1" value="TestLong" onclick="javascript:button1();" style="width:100px;Height:25px;" /> <input type="button" id="testbutton2" value="TestShort" onclick="javascript:button2();" style="width:100px;Height:25px;" /> <input type="button" id="testbutton3" value="TestUrl" onclick="javascript:button3();" style="width:100px;Height:25px;" /> </div> </form> </body> </html> /* Code Behind (AsyncPage.aspx.cs): ------------------------------------------------------------------------------- */ using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Net; using System.IO; using System.Threading; using AsyncStuff; public partial class AsyncPage : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (AsyncCall.IsRunningAsync(Page)) CalledAsync(); //function is run when a page calls itself asychronously //a query variable triggers this page to run a different async function (use AJAX Call to this same url with this query variable) //these are single-run checked already, so you don't have to worry about running them more than once if (Request.QueryString["LongFunction"] != null) AsyncCall.RunAsynchronously(Page, AfterLong, "Long call"); if (Request.QueryString["ShortFunction"] != null) AsyncCall.RunAsynchronously(Page, AfterShort); if (Request.QueryString["UrlFunction"] != null) AsyncCall.RunAsynchronously("/AsyncTest.aspx?test=1", Page); //Normal Page load lbl1.Text = "Just sitting here"; } //this stuff called on the async thread protected void CalledAsync() { //stuff done during async if (Request.QueryString["LongFunction"]!=null) { //crunch some numbers to waste some time for(int i = 1; i<565535; i++) { decimal num = (decimal)DateTime.Now.Millisecond * (decimal)(new Random(DateTime.Now.Millisecond)).NextDouble(); num += num; } Response.Write("Success"); } else if (Request.QueryString["ShortFunction"] != null) { Response.Write("Short Call Was Run"); } Response.End(); //do not show regular page contents } //this stuff called on the page thread after the async call returns protected void AfterLong(AsyncCall.AsyncState state) { //this is an example of using the response stream directly, you can use state.GetResponseText() to accomplish this same thing string pageContent = sr.ReadToEnd(); if (pageContent.Contains("Success")) Response.Write(state.Parameters[0].ToString()); else Response.Write("Failure"); Response.End(); } //this stuff called on the page thread after the async call returns protected void AfterShort(AsyncCall.AsyncState state) { string text = state.GetResponseText(); if (text!=null) Response.Write(text); Response.End(); } }
You need to login to post a comment.
