Posted By

bryanlyman on 02/10/10


Tagged

url ajax session page server browser Net ASP thread true off ServerSide multi Asynchronous HttpWebRequest aspx multithread off-page webrequest IHttpAsyncHandler mltithread AddOnPreRenderCompleteAsync bryan lyman


Versions (?)

Asynchronous ASP.Net Pages, AJAX Helper, WebRequest Passthrough


 / Published in: C#
 

.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.

  1. /*
  2. !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.
  3.  
  4. Main Class (AsyncCall.cs), place in App_Code directory or compile as a dll and place in the bin dirctory of the site:
  5. -------------------------------------------------------------------------------
  6. */
  7.  
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Linq;
  11. using System.Web;
  12. using System.Web.UI;
  13. using System.Net;
  14. using System.Threading;
  15. using System.IO;
  16. using System.Collections.Specialized;
  17.  
  18. namespace AsyncStuff
  19. {
  20. public static class AsyncCall
  21. {
  22. public sealed class AsyncState
  23. {
  24. internal const string QVARNAME = "ASYNCSession";
  25.  
  26. public readonly Page CallingPage;
  27. internal readonly Uri Url;
  28. private HttpWebRequest _request;
  29. private HttpWebResponse _response;
  30. internal readonly SpecificFunction SpecificFunction;
  31. public readonly object[] Parameters;
  32. private string _text;
  33. internal byte[] FormContent;
  34.  
  35. //cascaded constructor only (private)
  36. private AsyncState(Page page)
  37. {
  38. this.CallingPage = page;
  39. this.Url = GetAsyncUrl(page.Request.Url);
  40. }
  41.  
  42. //off-page url
  43. internal AsyncState(Uri url, Page page, SpecificFunction specificFunction)
  44. {
  45. this.CallingPage = page;
  46. this.Url = GetAsyncUrl(url);
  47. this.SpecificFunction = specificFunction;
  48. }
  49.  
  50. internal AsyncState(Page page, SpecificFunction specificFunction)
  51. : this(page)
  52. {
  53. this.SpecificFunction = specificFunction;
  54. }
  55.  
  56. internal AsyncState(Page page, SpecificFunction specificFunction, params object[] parameters)
  57. : this(page, specificFunction)
  58. {
  59. this.Parameters = parameters;
  60. }
  61.  
  62. private Uri GetAsyncUrl(Uri uri)
  63. {
  64. string url = uri.OriginalString;
  65. int start = url.IndexOf("?");
  66. if (start < 0)
  67. {
  68. start = url.Length;
  69. url = url.Insert(start++, "?");
  70. }
  71. else
  72. {
  73. start++;
  74. url = url.Insert(start, "&");
  75. }
  76.  
  77. //timestamped query variable inserted to indicate that a page is running asychronously
  78. url = url.Insert(start, QVARNAME + "=" + DateTime.Now.ToString("MMddyyyyHHmmssffff"));
  79. return new Uri(url);
  80. }
  81.  
  82. public HttpWebRequest Request
  83. {
  84. get { return this._request; }
  85. internal set { this._request = value; }
  86. }
  87.  
  88. public HttpWebResponse Response
  89. {
  90. get { return this._response; }
  91. internal set { this._response = value; }
  92. }
  93.  
  94. public string GetResponseText()
  95. {
  96. if (this._text == null)
  97. {
  98. this._text = "";
  99. if (this._response != null)
  100. this._text = (new StreamReader(this._response.GetResponseStream())).ReadToEnd();
  101. }
  102. if (this._text == "")
  103. return null;
  104.  
  105. return this._text;
  106. }
  107. }
  108.  
  109. /// <summary>
  110. /// A delegate function used to create a callback for the RunPageAsynchronously() function
  111. /// </summary>
  112. /// <param name="state">The state object that will be returned from the results of the asychronous call</param>
  113. public delegate void SpecificFunction(AsyncState state);
  114.  
  115. /// <summary>
  116. /// Run an off-page url request as an asychronous request. (Note: this is a seperate session, so don't expect session variables to persist)
  117. /// </summary>
  118. /// <param name="url">Off-page URL which contents will be retreived asychronously</param>
  119. /// <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>
  120. /// <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>
  121. public static bool RunAsynchronously(string url, Page callingPage)
  122. {
  123. if (callingPage is IHttpAsyncHandler)
  124. {
  125. Uri absUrl = new Uri(url, UriKind.RelativeOrAbsolute);
  126. if (!absUrl.IsAbsoluteUri || absUrl.OriginalString.StartsWith("file"))
  127. {
  128. string resolve = callingPage.ResolveUrl(url);
  129. Uri pageUrl = callingPage.Request.Url;
  130. string newPath = pageUrl.OriginalString.Replace(pageUrl.PathAndQuery, resolve);
  131. absUrl = new Uri(newPath, UriKind.Absolute);
  132.  
  133. string filePath = callingPage.MapPath(absUrl.AbsolutePath);
  134. FileInfo fi = new FileInfo(filePath);
  135. if (!fi.Exists)
  136. throw new Exception("File Not Found, requestsed relative path does not exist. Check the filename for spelling errors.");
  137. }
  138. AsyncState state = new AsyncState(absUrl, callingPage, new SpecificFunction(AfterOffPage));
  139. callingPage.AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginAsyncOperation), new EndEventHandler(EndAsyncOperation), state);
  140. return true;
  141. }
  142. return false;
  143. }
  144.  
  145. private static void AfterOffPage(AsyncState state)
  146. {
  147. if (state==null || state.Response == null || state.CallingPage==null || state.CallingPage.Response==null)
  148. return;
  149.  
  150. //content rewrite (make calling page same as the page called)
  151. //requires IIS content rewrite pipline mode, so if exception then ignore
  152. try
  153. {
  154. state.CallingPage.Response.Headers.Clear();
  155. state.CallingPage.Response.Headers.Add(state.Response.Headers);
  156. }
  157. catch { }
  158. state.CallingPage.Response.ContentType = state.Response.ContentType;
  159. BinaryReader br = new BinaryReader(state.Response.GetResponseStream());
  160. int len = (int)state.Response.ContentLength;
  161. byte[] data = br.ReadBytes(len);
  162. state.CallingPage.Response.OutputStream.Write(data, 0, len);
  163.  
  164. state.CallingPage.Response.End();
  165. }
  166.  
  167. /// <summary>
  168. /// Run the current page request as an asychronous request. (Note: this is a seperate session, so don't expect session variables to persist)
  169. /// </summary>
  170. /// <param name="callingPage">The current System.Web.UI.Page object to be processed (must implement IHttpAsyncHandler or use the [%@ Page Async="true"] directive)</param>
  171. /// <param name="specificFunction">A parameterless function to call on the async request page</param>
  172. /// <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>
  173. public static bool RunAsynchronously(Page callingPage, SpecificFunction specificFunction)
  174. {
  175. if ((callingPage is IHttpAsyncHandler) && !IsRunningAsync(callingPage))
  176. {
  177. AsyncState state = new AsyncState(callingPage, specificFunction);
  178. callingPage.AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginAsyncOperation), new EndEventHandler(EndAsyncOperation), state);
  179. return true;
  180. }
  181. return false;
  182. }
  183.  
  184. /// <summary>
  185. /// Run the current page request as an asychronous request. (Note: this is a seperate session, so don't expect session variables to persist)
  186. /// </summary>
  187. /// <param name="callingPage">The current System.Web.UI.Page object to be processed (must implement IHttpAsyncHandler or use the [%@ Page Async="true"] directive)</param>
  188. /// <param name="specificFunction">A function to call on the async request page</param>
  189. /// <param name="parameters">The parameters to pass to the specific function</param>
  190. /// <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>
  191. public static bool RunAsynchronously(Page callingPage, SpecificFunction specificFunction, params object[] parameters)
  192. {
  193. if ((callingPage is IHttpAsyncHandler) && !IsRunningAsync(callingPage))
  194. {
  195. AsyncState state = new AsyncState(callingPage, specificFunction, parameters);
  196. callingPage.AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginAsyncOperation), new EndEventHandler(EndAsyncOperation), state);
  197. return true;
  198. }
  199. return false;
  200. }
  201.  
  202. /// <summary>
  203. /// Tests to see if the page is in is asynchronous cycle of an asynchronous request made from the RunPageAsynchronously function
  204. /// </summary>
  205. /// <param name="page">The page to check for sychronicity</param>
  206. /// <returns>true if in the asychronous cycle</returns>
  207. public static bool IsRunningAsync(Page page)
  208. {
  209. return (page.Request.QueryString[AsyncState.QVARNAME] != null);
  210. }
  211.  
  212. private static IAsyncResult BeginAsyncOperation(object sender, EventArgs e, AsyncCallback cb, object stateObj)
  213. {
  214. if (stateObj is AsyncState)
  215. {
  216. AsyncState state = (AsyncState)stateObj;
  217. state.Request = (HttpWebRequest)HttpWebRequest.Create(state.Url);
  218.  
  219. //copy relevant header information
  220. state.Request.Accept = GetTextList<string>(state.CallingPage.Request.AcceptTypes, ", ", true);
  221. state.Request.AllowAutoRedirect = true;
  222. state.Request.AllowWriteStreamBuffering = true;
  223. state.Request.ContentType = state.CallingPage.Request.ContentType + "; " + state.CallingPage.Request.ContentEncoding.WebName;
  224. state.Request.Method = state.CallingPage.Request.RequestType;
  225. state.Request.Referer = state.CallingPage.Request.UrlReferrer.OriginalString;
  226.  
  227. //copy cookies
  228. if (state.CallingPage.Request.Cookies.Count > 0)
  229. {
  230. state.Request.CookieContainer = new CookieContainer();
  231. List<string> excludedCookies = new List<string> { "ASP.NET_SessionId" };
  232. foreach (string key in state.CallingPage.Request.Cookies.Keys)
  233. {
  234. if (!excludedCookies.Contains(key, StringComparer.CurrentCultureIgnoreCase))
  235. {
  236. HttpCookie cookie = state.CallingPage.Request.Cookies[key];
  237. Cookie copy = new Cookie(cookie.Name, cookie.Value, state.Url.AbsolutePath, state.Url.Host);
  238. state.Request.CookieContainer.Add(copy);
  239. }
  240. }
  241. }
  242.  
  243. //copy form variables
  244. if (state.CallingPage.Request.Form.Count > 0 && state.Request.Method.Equals("POST", StringComparison.CurrentCultureIgnoreCase))
  245. {
  246. string pairs = GetTextDictionary(state.CallingPage.Request.Form, "=", "&", true);
  247. state.FormContent = state.CallingPage.Request.ContentEncoding.GetBytes(pairs);
  248. state.Request.ContentLength = state.FormContent.Length;
  249. state.Request.BeginGetRequestStream(EndRequestStreamCallback, state);
  250. }
  251.  
  252. return state.Request.BeginGetResponse(cb, stateObj);
  253. }
  254. return null;
  255. }
  256.  
  257. private static void EndRequestStreamCallback(IAsyncResult ar)
  258. {
  259. if (ar.AsyncState is AsyncState)
  260. {
  261. AsyncState state = (AsyncState)ar.AsyncState;
  262. BinaryWriter sw = new BinaryWriter(state.Request.EndGetRequestStream(ar));
  263. sw.Write(state.FormContent, 0, state.FormContent.Length);
  264. sw.Close();
  265. }
  266. }
  267.  
  268. private static void EndAsyncOperation(IAsyncResult ar)
  269. {
  270. if (ar.AsyncState is AsyncState)
  271. {
  272. AsyncState state = (AsyncState)ar.AsyncState;
  273. if (state.Request != null)
  274. {
  275. try { state.Response = (HttpWebResponse)state.Request.EndGetResponse(ar); }
  276. 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); };
  277.  
  278. if (state.SpecificFunction != null)
  279. {
  280. object target = state.SpecificFunction.Target;
  281. if (target == null)
  282. target = state.CallingPage;
  283. try { state.SpecificFunction.Method.Invoke(target, new object[] { state }); }
  284. 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); }
  285. }
  286. }
  287. }
  288. }
  289.  
  290. private static string GetTextList<I>(IEnumerable<I> list, string separator, bool urlEncode)
  291. {
  292. System.Text.StringBuilder sb = new System.Text.StringBuilder();
  293. int index = 0;
  294. foreach (I item in list)
  295. {
  296. if (index > 0)
  297. sb.Append(separator);
  298. if (urlEncode)
  299. sb.Append(HttpUtility.UrlPathEncode(item.ToString()));
  300. else
  301. sb.Append(item.ToString());
  302. index++;
  303. }
  304. string ret = sb.ToString();
  305. sb.Length = 0; //destroy memory
  306. return ret;
  307. }
  308.  
  309. private static string GetTextDictionary<K, V>(IDictionary<K, V> dictionary, string equality, string separator, bool urlEncode)
  310. {
  311. System.Text.StringBuilder sb = new System.Text.StringBuilder();
  312. int index = 0;
  313. foreach (K key in dictionary.Keys)
  314. {
  315. V value = dictionary[key];
  316. if (index > 0)
  317. sb.Append(separator);
  318. if (urlEncode)
  319. sb.Append(HttpUtility.UrlPathEncode(key.ToString()));
  320. else
  321. sb.Append(key.ToString());
  322. sb.Append(equality);
  323. if (urlEncode)
  324. sb.Append(HttpUtility.UrlPathEncode(value.ToString()));
  325. else
  326. sb.Append(value.ToString());
  327.  
  328. index++;
  329. }
  330. string ret = sb.ToString();
  331. sb.Length = 0; //destroy memory
  332. return ret;
  333. }
  334.  
  335. private static string GetTextDictionary(NameValueCollection dictionary, string equality, string separator, bool urlEncode)
  336. {
  337. System.Text.StringBuilder sb = new System.Text.StringBuilder();
  338. int index = 0;
  339. foreach (string key in dictionary.Keys)
  340. {
  341. string value = dictionary[key];
  342. if (index > 0)
  343. sb.Append(separator);
  344. if (urlEncode)
  345. sb.Append(HttpUtility.UrlPathEncode(key.ToString()));
  346. else
  347. sb.Append(key.ToString());
  348. sb.Append(equality);
  349. if (urlEncode)
  350. sb.Append(HttpUtility.UrlPathEncode(value.ToString()));
  351. else
  352. sb.Append(value.ToString());
  353. index++;
  354. }
  355. string ret = sb.ToString();
  356. sb.Length = 0; //destroy memory
  357. return ret;
  358. }
  359.  
  360.  
  361.  
  362. }
  363. }
  364.  
  365. /*
  366. Page Example (AsyncPage.aspx):
  367. -------------------------------------------------------------------------------
  368. */
  369.  
  370. <%@ Page Async="true" Language="C#" AutoEventWireup="true" CodeFile="AsyncPage.aspx.cs" Inherits="AsyncPage" %>
  371.  
  372. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  373.  
  374. <html xmlns="http://www.w3.org/1999/xhtml">
  375. <head runat="server">
  376. <title></title>
  377. <script type="text/javascript">
  378. function AJAXCall(url, callBack, postData) {
  379. //setup the callback
  380. var out = callBack;
  381. if (!out) {
  382. out = function(text) { return; }
  383. }
  384.  
  385. //setup the request
  386. var request = null;
  387. if (window.XMLHttpRequest)
  388. request = new XMLHttpRequest();
  389. else if (window.ActiveXObject)
  390. request = new ActiveXObject("Microsoft.XMLHTTP");
  391. else
  392. return false;
  393.  
  394. //true for async..
  395. request.open("POST", url, true);
  396.  
  397. //setup the handle of the request when the status changes
  398. request.onreadystatechange = function() {
  399. if (request && request.readyState == 4) {
  400. //if (request.status == 200)
  401. out(request.responseText);
  402.  
  403. }
  404. }
  405. //setup the request headers
  406. request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  407.  
  408. //send the request
  409. if (postData)
  410. request.send(postData);
  411. else
  412. request.send("");
  413. }
  414.  
  415. function callback(data) {
  416. alert('got here: ' + data);
  417. }
  418.  
  419. function button1() {
  420. AJAXCall("AsyncPage.aspx?LongFunction=1", callback, "testing=1");
  421. }
  422.  
  423. function button2() {
  424. AJAXCall("AsyncPage.aspx?ShortFunction=1", callback, "testing=1");
  425. }
  426.  
  427. function button3() {
  428. AJAXCall("AsyncPage.aspx?UrlFunction=1", callback, "testing=1");
  429. }
  430.  
  431. </script>
  432. </head>
  433. <body>
  434. <form id="form1" runat="server">
  435. <div>
  436. <asp:Label ID="lbl1" runat="server" />
  437. <input type="button" id="testbutton1" value="TestLong" onclick="javascript:button1();" style="width:100px;Height:25px;" />
  438. <input type="button" id="testbutton2" value="TestShort" onclick="javascript:button2();" style="width:100px;Height:25px;" />
  439. <input type="button" id="testbutton3" value="TestUrl" onclick="javascript:button3();" style="width:100px;Height:25px;" />
  440. </div>
  441. </form>
  442. </body>
  443. </html>
  444.  
  445. /*
  446. Code Behind (AsyncPage.aspx.cs):
  447. -------------------------------------------------------------------------------
  448. */
  449.  
  450. using System;
  451. using System.Collections.Generic;
  452. using System.Linq;
  453. using System.Web;
  454. using System.Web.UI;
  455. using System.Web.UI.WebControls;
  456. using System.Net;
  457. using System.IO;
  458. using System.Threading;
  459. using AsyncStuff;
  460.  
  461. public partial class AsyncPage : System.Web.UI.Page
  462. {
  463. protected void Page_Load(object sender, EventArgs e)
  464. {
  465. if (AsyncCall.IsRunningAsync(Page))
  466. CalledAsync(); //function is run when a page calls itself asychronously
  467.  
  468. //a query variable triggers this page to run a different async function (use AJAX Call to this same url with this query variable)
  469. //these are single-run checked already, so you don't have to worry about running them more than once
  470. if (Request.QueryString["LongFunction"] != null)
  471. AsyncCall.RunAsynchronously(Page, AfterLong, "Long call");
  472.  
  473. if (Request.QueryString["ShortFunction"] != null)
  474. AsyncCall.RunAsynchronously(Page, AfterShort);
  475.  
  476. if (Request.QueryString["UrlFunction"] != null)
  477. AsyncCall.RunAsynchronously("/AsyncTest.aspx?test=1", Page);
  478.  
  479. //Normal Page load
  480. lbl1.Text = "Just sitting here";
  481. }
  482.  
  483. //this stuff called on the async thread
  484. protected void CalledAsync()
  485. {
  486. //stuff done during async
  487.  
  488. if (Request.QueryString["LongFunction"]!=null)
  489. {
  490. //crunch some numbers to waste some time
  491. for(int i = 1; i<565535; i++)
  492. {
  493. decimal num = (decimal)DateTime.Now.Millisecond * (decimal)(new Random(DateTime.Now.Millisecond)).NextDouble();
  494. num += num;
  495. }
  496. Response.Write("Success");
  497. }
  498. else if (Request.QueryString["ShortFunction"] != null)
  499. {
  500. Response.Write("Short Call Was Run");
  501. }
  502.  
  503. Response.End(); //do not show regular page contents
  504. }
  505.  
  506. //this stuff called on the page thread after the async call returns
  507. protected void AfterLong(AsyncCall.AsyncState state)
  508. {
  509. //this is an example of using the response stream directly, you can use state.GetResponseText() to accomplish this same thing
  510. StreamReader sr = new StreamReader(state.Response.GetResponseStream());
  511. string pageContent = sr.ReadToEnd();
  512. if (pageContent.Contains("Success"))
  513. Response.Write(state.Parameters[0].ToString());
  514. else
  515. Response.Write("Failure");
  516.  
  517. Response.End();
  518. }
  519.  
  520. //this stuff called on the page thread after the async call returns
  521. protected void AfterShort(AsyncCall.AsyncState state)
  522. {
  523. string text = state.GetResponseText();
  524. if (text!=null)
  525. Response.Write(text);
  526. Response.End();
  527. }
  528. }

Report this snippet  

You need to login to post a comment.