Faster ASP.NET Ajax Webservices through multiple sub domain calls

scaredbaconSoftware and s/w Development

Jul 4, 2012 (4 years and 11 months ago)

700 views

~ 1 ~
http://varjabedian.net
Faster ASP.NET Ajax Webservices through
multiple sub domain calls
Ralph Varjabedian
August 2009
Revision 1.0.1
Keywords: ASP.NET, Ajax, Webservices.
Abstract: An article that describes a technique to achieve faster ASP.NET Ajax Webservice calls through the use of
multiple sub domains to serve more parallel connections.
_______________________________________________________________________________________________
~ 2 ~
1.Introduction
:
Ajax is one of the most important technologies in Web 2.0. Almost every web developer nowadays
uses Ajax calls in one way or another.Webservice Ajax calls is a very nice feature in ASP.NET,when you
create a Webservice class on the server side, you can add the attribute
[System.Web.Script.Services.ScriptService] to your class and ASP.NET will automatically
generate the appropriate JavaScript file(s) to call your web services from the client side with very little
effort.
While working with Ajax calls, I discovered several ways to enhance the usage of Webservice calls
and eventually created a client side JavaScript class (WSAjaxCallsClass) that helps in this task. If you work
with ASP.NET Ajax Webservices you should not miss this article. Please note that the techniques described
here are not limited for ASP.NET itself, however the implementation given in this article is for ASP.NET.
Before deciding on the title of this article, I had several titles in mind 1. Calling Ajax Webservices
through multiple sub domains to increase throughput. 2.An Ajax Webservice calls manager, a must for
every ASP.NET Ajax developer. 3.Ajax Webservice throttling, but I decided to stick with the current one.
The (WSAjaxCallsClass) class gives you several benefits but the most important one is in the title of this
article. I will start by presenting the class and then talk about the benefits you get in details.
2.The Webservice calls manager class
:
The code presented gives you the following benefits;I will state the benefits and explain each one in
details.
a.Queuing manager
:
According to an article for Omar Al Zabir
, if you ask your browser to execute more than two Ajax
calls at once it will hang and this is worst in IE. I have tested this myself but I could not reproduce this
problem, maybe the current versions of IE have fixed this problem. However this is not the main reason why
I decided to do a queuing manager. Sometimes in your coding, without the developer paying attention to
cases like this, you initiate Ajax calls by reacting to some events or actions that the end user does. I was
developing an application that sends SMS (Short Messaging Service) where a part of the code was to
calculate the cost of the current SMS to be sent based on the message’s text length, Unicode or not, the
number of recipients chosen and the destination of each recipient. Sometimes if the user’s input is fast
enough, multiple calls are generated to the Calculate function without the first one being done yet.
Sometimes I end up with up to 5 or 6 unnecessary calls where only one is needed.By queuing the calls, we
can detect such cases where we call the same Ajax function more than once before the first call has returned,
and we can decide to ignore the multiples or we can decide to always keep the last call in the queue, which
is the more logical choice for most cases.
~ 3 ~
[WebMethod(true)]
public GenericReturnObject GetContactNameFromID(int contactid)
{
try
{
if (!IsLoggedIn)
return GenericReturnObject.CreateSessionExpired();

string contactName = Application.IBusinessLayer().IContacts.
GetContactNameFromID(AccountID, contactid);
return GenericReturnObject.CreateSuccess(“”,new { ContactName = contactName });
}
catch (Exception ex)
{
return GenericReturnObject.CreateFromException(ex);
}
}
b.Common success, failure methods
:
When you call the Initialize method of the class (WSAjaxCallsClass), it allows you to provide a
common success, failure methods for all the Ajax calls before control is passed to the original
success/failure methods of the Ajax call. Let me explain why this is important. When I use Ajax calls in a
web application, I tend to return the same object from all the Webservice calls and I do not allow my
Webservice methods on the server to throw exceptions (I will tell you why a little later). To make my idea
clearer and explain why this is important, let me show you the class that I use (GenericReturnObject).
You can find it in appendix A.
The first member of the class is an enum called “errorLevel”. errorLevel has one of the following
values: None, Warning, Fatal and SessionExpired.
i.None: If there are no errors.
ii.Warning: If the exception or error is not a fatal one, but just a warning, something like “You do
not have enough units to send this SMS”.
iii.Fatal: If a fatal error was thrown, something like a database connection error.
iv.SessionExpired: This is a special error indicating that the session on the server does not exist
(has expired).The script on the client side reacts to this error by redirecting the end user to the
login page and asking him to log in again.
The next member “ret”, is a Boolean value that indicates the success or failure of the Ajax call.If it is
false, the errorLevel value is checked for more details.
The member “message” has a description of the error.
The member “payload” is an extra object that you want passed to the client side. Thanks to JSON, this
object will be serialized and sent properly to the client side.
The member “payloadTypeName” contains the name of the type of the object that is filled in the
payload member, just in case you need it on the client side.
Along with these members you will find some static methods that help you create this object in
compact manner. Let us look at a typical Webservice method that I define:
A typical Webservice method would first check if the user is Logged in or not (session expired or not)
and if the session has expired it will create the appropriate GenericReturnObject with this errorLevel value
and return it to the client side for proper handling of this case.Next we continue to the core of our function
~ 4 ~
and pass it to the business layer and then we create a success GenericReturnObject and return an anonymous
object that has the return values that we want (ContactName).Notice how I catch all exceptions and return a
GenericReturnObject with a fatal error level and the exception in the message member of the object. If I let
exceptions to be thrown fromWebservice methods, then on the client side the “failure” function is going to
be called.So you will have to process in your failure function on the client side two types of errors, errors
that come from your Webservice calls as exceptions (they could be business logic errors or fatal errors) and
errors that come from calling Ajax Webservice methods themselves, communication errors between the
browser and the server, this is going to complicate things for us. To make our lives easier we do not allow
exceptions from our Webservice calls, this way your failure function on the client side can concentrate only
on Ajax communication problems and you can have one common failure function with one common action
to do.This means that exceptions are now returned to the success function on the client side and you will
have to handle the cases for non-successful business calls to your Webservice (when the
GenericReturnObject has an errorLevel), well this is where the common success function shines. The
common success function can now handle the cases of GenericReturnObject that are returned that have
generic errors like session expired, warning (react by popup message) or a fatal error by showing a fatal
error popup window or similar. Finally the success function of your specific Webservice call (not the
common success function) can concentrate on the business logic of the method that you just called and not
worry about the more common errors that happen.
c.Speeding up your Ajax calls
:
Last but not least, the main benefit that is the topic of this article.How does the class
(WSAjaxCallsClass) speed my Ajax calls? There is a technique that developers sometimes use to speed up
the loading of a website which is to load different resources from different domains or sub domains of the
website. For Example if our website is under example.com,then we will add the images under
images.example.com domain and not the main domain. The browser will open a limited maximum number
of connections per domain (usually 2) so if you have 10 resources, then these 10 resources will load with a
maximum of 2 connections at a time. Putting the images under a different domain will make the browser
open 2 connections for images and 2 connections for the rest of the site. You can do this for several sub
domains if you want and not just two. However adding too many sub domains will eventually drop
performance down. A good balanced method would be to use a different domain for images, scripts and css
files.
XMLHttpRequest (XHR) is also limited by the number of connections of the domain the page belongs
to, (usually 2) per domain. Now if we want to follow the same technique and divide our web service calls
into several sub domains, we will soon sadly discover that this cannot be done because the browser will stop
you and will not allow your script to access Ajax calls to domains other than the page’s domain that you are
running the script in.So why is XHR different from the rest, why do we have this restriction on it, well this
is for security reasons, so that if you have an XSS vulnerability in your website, someone will not be able to
inject code that will communicate via Ajax to a location other than the page’s domain (say the attackers own
domain) making XSS much more dangerous than it already is.Fortunately there is a way around this
limitation and we can make Ajax calls faster by allowing more parallel connections at the same time.We
will break out of the 2 connections per domain limitation.
So how do we break out of this constraint? We will use hidden iframes, where each iframe will
connect to a separate sub domain of the main site and our (WSAjaxCallsClass) class will handle the task of
dividing the Ajax requests properly on the iframes depending on which iframe is free and can accept calls.
~ 5 ~
Say we want 4 parallel connections to go at the same time (as opposed to just the default 2 of the
browser) then we create 4 hidden iframes and have each iframe connect to a separate sub domain.For
example if the main site is example.com then the sub domains would be 1.example.com,2.example.com,
3.example.com and 4.example.com.Now to setup this we need two things. First we need to setup our DNS
server to resolve these sub domains, we can easily add one record to our DNS server with a wildcard to
resolve all these sub domains (*.example.com).If adding a wildcard to your top domain like this leaves a
bad taste in your mouth, then you can pick any sub domain and add the wildcard to it (*.ajax.example.com).
Next we need to tell IIS to map these extra domains to the same website.IIS does not allow you to use
wildcards so you need to add your sub domains manually. You do that by adding the extra domains as extra
“host headers” for the website, on IIS 6 you do that by pressing the button Advanced on the ‘Web Site’ tab
of the website’s properties dialog.
Now some developers at this point are wondering about the communication with the hidden iframes.
The parent will not be able to communicate (via JavaScript) to the hidden iframe (and vice versa) if the
iframe is on a different domain than the parent (example.com and *.example.com), so how do we overcome
this obstacle? The answer is document.domain. If both your iframe and your parent set the document.domain
variable in their script to a common upper level domain then they will be able to communicate, in this case
we will set them both to example.com.
Now that we have proper communication between the parent document and the hidden iframes, it is
time to delegate the proper Ajax calls to the iframes to keep things transparent for our ASP.NET Ajax calls.
How can we achieve that? The answer is deep inside the ASP.NET Ajax library.If you do some debugging
when you do your Ajax calls, you will reach a function named: Sys$Net$WebServiceProxy$invoke and
then eventually reach a function named: Sys$Net$XMLHttpExecutor$executeRequest.These two
functions are intercepted and replaced with different versions by our class. The first function
Sys$Net$WebServiceProxy$invoke is called when a new Webservice call is requested, the new overridden
version of it will stop the Webservice call and just add it to the internal queue of the class
(WSAjaxCallsClass) for further processing. The second function
Sys$Net$XMLHttpExecutor$executeRequest is a beautiful function in that it is responsible for making the
actual Ajax call. It is the function that creates the XMLHttpRequest object and starts the actual call to the
server and luckily for us it is a very coherent function, all it needs is the “this” object that it runs in its
context.The new overridden version of this function will intercept Ajax calls just before they happen and
will send each of the intercepted calls to one of our iframes depending on which iframe is free, so that the
iframe can run its own copy of Sys$Net$XMLHttpExecutor$executeRequest that it loaded but with the
“this” context that we give it.
The next section will present a proof of concept web application that shows you the speed benefits that
we are gaining, but don’t take my word for it you can download it and run it yourself to see the results or
you can check a running version on http://testmultisubdomains.urlip.com/
. The proof of concept application
can be downloaded from my blog at http://varjabedian.net
, please read the next section for some instructions
on how to run the project locally on your machine.
3.Proof of concept and results
:
I will present in each of the following pages a screen capture with details under the image.
~ 6 ~
Screen 1:
This first screen introduces the application. The proof of concept project shows you a table with the
columns: name, start, end and Time line.
The column ‘name’ has the name of the method called, the start has the start time, the ‘end’ column
has the end time and the ‘time line’ column has a simple graphical representation of the time line. Times are
given by minutes, seconds and milliseconds. There are 20 rows in the table for 20 separate Ajax calls. In the
table’s header: you have the button that initiates the calls (here you can see it disabled as it has been already
pressed and run) and you have the magical checkbox “Use Multi sub domains”.When you choose the multi
option, the magic of iframes happen. The drop down list besides the checkbox shows the number of parallel
connections you want the queue manager to call at the same time. This first screen is running with 1 parallel
connections at a time without the multi option. See how the calls are done one after the other (ignore that
last 3 boxes, they seem to be run at the same time, but this is because the html table was not wide enough).
These calls wait 1000 milliseconds on the server side before returning, the extra milliseconds that you see
(for example the first call: 1028 ms.) is the network communication overhead time. Notice the total time,
around 20 seconds, 1 second for each call, more or less. Note that I have taken these screen caps from the
application working locally on my machine to make the results clearer.
~ 7 ~
Screen 2:
Screen two shows you running with 2 parallel connections and without multi option.Notice how each
two calls are run at the same time, also notice the total time have dropped from 20 seconds to 10 seconds,
half the time, since now we are running two at a time. At this point we are still not using iframes, we are
using the normal browser’s behavior.
~ 8 ~
Screen 3:
Screen 3 shows that we are requesting 3 parallel connections at the same time and we are still not
using the multi option. Notice the time line column;notice how even though we are sending 3 calls to the
browser at a time, yet the browser is only running 2 calls at a time and queuing the third call internally. The
total time is still 10 seconds.
~ 9 ~
Screen 4
Screen 4 shows that we are requesting 4 parallel connections at the same time and we are still not
using the multi option. Again, notice the time line column; notice how even though we are sending 4 calls to
the browser at a time, yet the browser is only running 2 calls at a time and queuing the third and fourth calls
internally. The total time is still 10 seconds.
~ 10 ~
Screen 5
Screen 5 is showing that we are requesting 1 connection at a time, but this time we are using the multi
feature (the checkbox is checked). This page now internally is using 1 hidden iframe to do its Ajax calls.
Notice each call, notice how these calls are slightly longer than Screen 1 calls, this is of the extra overhead
of delegating calls to the iframe and having the iframe return the result to us.Ignore the last calls being
under each other, this is just because the html table was not wide enough.The total time is around 20
seconds.
~ 11 ~
Screen 6
Screen 6 is showing that we are requesting 2 connection at a time, but this time we are using the multi
feature (the checkbox is checked). This page now internally is using 2 hidden iframes to do its Ajax calls.
Notice each call, notice how these calls are slightly longer than Screen 2 calls, again, this is of the extra
overhead of delegating calls to the iframes and having the iframes return the results to us. The total time is
around 10 seconds. The next screen is the interesting one.
~ 12 ~
Screen 7
Screen 7 is the first screen that starts to show the speed benefits. Now we are requesting 3 parallel
connections at the same time with the multi option. Notice now how we were able to break out of the 2
parallel connections per domain limitation of the browser. Our Ajax calls are now being served in 3 parallel
calls.Notice how the total time now drops below 10 seconds, it is around 7.5 seconds.
~ 13 ~
Screen 8
Screen 8 continues with the speed benefits, now we are able to run 4 parallel connections at the same
time using 4 hidden iframes. Notice how the total time has dropped to a little over 5 seconds, that’s 50%
increase in speed as compared to doing Ajax calls through one domain.
~ 14 ~
Screen 9
Screen 9 is using 5 parallel connections using 5 hidden iframes. Notice how the total time has dropped
even more.
~ 15 ~
Screen 10
Screen 10 is using 6 parallel connections using 6 hidden iframes. Notice how the total time has
dropped even more.
~ 16 ~
Screen 11
Screen 11 is using 7 parallel connections using 7 hidden iframes. Notice how the total time has
dropped to 3.3 seconds as compare to 10 seconds with the standard way of calling Ajax methods, that’s
around 70% increase in speed. Increasing connections further will not help much anymore, depending on the
application; a decision has to be made on the optimum number of parallel connections to be used.
During my tests, I found an Interesting find. I used Fiddler to take a closer look on how each of the
browsers supported (IE, FF, Opera, Safari) behaves with respect to the order of the connections it opens to
the server.First I will present you with 4 screen caps, each test was made with a different browser, fiddler
was used and 20 parallel connections were made using 20 hidden iframes in each of screens.Fiddler2 is an
http debugging application, it act as a proxy and gives you valuable information about your web
application’s behavior.
~ 17 ~
Screen: IE with fiddler with 20 parallel connections using 20 hidden iframes.
~ 18 ~
Screen: FF with fiddler with 20 parallel connections using 20 hidden iframes.
~ 19 ~
Screen: Opera with fiddler with 20 parallel connections using 20 hidden iframes.
~ 20 ~
Screen: Safari with fiddler with 20 parallel connections using 20 hidden iframes.
~ 21 ~
It is an interesting find to see that IE seems to send the requests to the server (for the iframes) in a
stack order (last added, first served) which does not seem logical to me and which will naturally mess up the
order of your calls!While Fire fox does the normal order (queue) first come, first served. Safari does things
differently; it seems it does not have a specific order, it might have some sort of internal queuing system that
decides on other factors which connection goes first. Opera seems to do things in order too, much like fire
fox, but the order is not perfect, it seems that it too decides on other factors as well.
If you are going to download the project and test things yourself locally, you will have to add some
virtual domains to your hosts file (C:\Windows\System32\drivers\etc\hosts). Edit this file and add some
entries to it like this:
127.0.0.1 localhost.com
127.0.0.1 1.localhost.com
127.0.0.1 2.localhost.com
All the way to 20.localhost.com, then when you run your project in Visual Studio you have to do 2
more things. 1. Open up the file default.aspx and replace the text "http://?.localt.com:54835" with the
top level domain of the entries you have added to the hosts file, following the above example, that would be
“?.localhost.com:54835”. I have configured the application’s project file to run with the above port always
54835, if for some reason you have a different port when you run your application then you have to update
the port as well. There is a helper function called GetRandomizingSubdomainFromDocument that will get
you the appropriate randomizing string needed from the current page’s URL so you won’t need to hardcode
things. The second thing you need to do is when you run the application from Visual Studio, it is going to
open your browser with the domain “localhost”, if you try to run your examples like this you will get access
denied in your JavaScript, you should replace the localhost in your browser and point it to the new top level
virtual domain you have created in your hosts file, following the above example that would be
localhost.com.I hope I made this clear enough.
~ 22 ~
4.Using the code in your applications
:
In this section I will write the steps needed to add the code in this article to your own ASP.NET Web
applications.The code works with IE7, Fire Fox 2.0, Opera 9.5 and Safari 3.0. First of all you need to add
three files to your project, you can add them anywhere but I advise you to add themunder scripts subfolder.
If you decide to add these files under a different folder, you need to update the variable
WSAjaxCallsClass_ProxyScriptLocation found in the WebServiceAjaxCallsHelper.js with the
appropriate value.
a.vnetLinkedlist.js:
A double linked list implementation. You can use this file in any project you want if you need a
double linked list.The linked list is needed by the (WSAjaxCallsClass) class to keep track of the queued
calls.
b.WebServiceAjaxCallsHelper.js:
This is the main JavaScript file that has most of the code in it. It has the class (WSAjaxCallsClass).At
the time of this writing its version was 1.0.9.
c.WebServiceAjaxCallsHelperIframe.aspx:
This form is loaded into each of the hidden iframes.It contains code that executes the delegated
ASP.NET Ajax calls.
Next you need to add a ScriptManager (or a ScriptManagerProxy) to the pages that you wish do call
ASP.NET Ajax Webservice methods. Make sure you add to your ScriptManager, two extra files in the
scripts section, vnetLinkedlist.js and WebServiceAjaxCallsHelper.js. You can take a look at the file
default.aspx in the project file attached with this article.Next you need to call Initialize function of the class
(WSAjaxCallsClass).
WSAjaxCallsClass.Initialize(GenericSuccess, GenericFailure, "http://?.localt.com:54835",
channels, true, true);
The Initialize method takes 6 parameters. The first parameter is the common success method. The
second parameter is the common failure method. The third parameter is the sub domain url, it should have a
question mark where the engine will replace it with the appropriate number depending on the number of
invisible iframes created. Note that if you pass null to the third parameter you disable the multi domains
Ajax calls feature. The fourth parameter takes the number of parallel connections you wish to have. The fifth
parameter takes a Boolean whether or not to ignore repetitive calls to the same method before the method
has returned. The sixth parameter takes a Boolean that decides how to ignore the repetitive calls, if you pass
true, it will always keep the last queued call (this is the logical choice and it works in most cases);if you
pass false, then repetitive calls will be ignored and completely discarded.Note that the public JavaScript
code is properly commented with XML comments for JavaScript intellisense in Visual Studio 2008.
Now every time you want to make an Ajax call, calls are transparently routed to the class
(WSAjaxCallsClass). And that’s about it, do not forget the steps you need to do on the IIS and DNS levels
described above in this article.
~ 23 ~
You can also use the file vnetLinkedlist.js in your own projects if you need a client side JavaScript
double linked list implementation. I will do a post on my blog later to describe how to use this file.
5.What’s next
?
The technique described here, using hidden iframes to enable multiple Ajax calls through multiple sub
domains can be used on languages other than ASP.NET however the implementation provided is for
ASP.NET.
The next article/code that I will write will implement this technique on ASP.NET Update panels to
make things faster for Update panels much in the same way, doing parallel Ajax calls through multiple sub
domains.If the reader has any comments about this idea, I would like to know about them.
Someone suggested that I add an automatic retries to timed-out Ajax calls. In my work I never had
many problems with timeouts and I did not face a situation where one or more retries would fix things. If
you, the reader,think that I should add such feature, please let me hear more about it, your comments are
very much welcomed.And also if the reader has any other comments or features that he or she would like to
see added to this class, please let me know on my blog.
I hope that this article will benefit a lot of developers and web applications out there. If this code
helped you speed up your particular site, I would like to hear your success story.
~ 24 ~
Appendix A: Code for GenericReturnObject
///<summary>
///A generic class that must be used when return things
///</summary>
public class GenericReturnObject
{
///<summary>
///When we return false, this will describe better
///</summary>
public enum ErrorLevel
{
None = 0,
Warning = 1,
Fatal = 2,
SessionExpired = 3,
}
///<summary>
///Main return value, true all is ok, false, you have to check the errorLevel and so on...
///</summary>
public bool ret { get; set; }
///<summary>
///Error message sent
///</summary>
public string message { get; set; }
///<summary>
///Return the error level
///</summary>
public ErrorLevel errorLevel { get; set; }
///<summary>
///Return an extra object if needed
///</summary>
public object payload { get; set; }
///<summary>
///Return the extra object type name
///</summary>
public string payloadTypeName { get; set; }
public static GenericReturnObject CreateSuccess()
{
return CreateSuccess(null, null);
}
public static GenericReturnObject CreateSuccess(string message)
{
return CreateSuccess(message, null);
}
public static GenericReturnObject CreateSuccess(string message, object payload)
{
GenericReturnObject obj = new GenericReturnObject();
obj.errorLevel = ErrorLevel.None;
obj.message = message;
obj.payload = payload;
if (obj.payload != null)
obj.payloadTypeName = obj.payload.GetType().ToString();
obj.ret = true;
return obj;
}
public static GenericReturnObject CreateSessionExpired()
{
GenericReturnObject obj = new GenericReturnObject();
obj.errorLevel = ErrorLevel.SessionExpired;
~ 25 ~
obj.ret = false;
return obj;
}
public static GenericReturnObject CreateFailedWarning(string message)
{
GenericReturnObject obj = new GenericReturnObject();
obj.errorLevel = ErrorLevel.Warning;
obj.message = message;
obj.payload = null;
obj.payloadTypeName = null;
obj.ret = false;
return obj;
}
public static GenericReturnObject CreateFailedFatal(string message)
{
GenericReturnObject obj = new GenericReturnObject();
obj.errorLevel = ErrorLevel.Fatal;
obj.message = message;
obj.payload = null;
obj.payloadTypeName = null;
obj.ret = false;
return obj;
}
public static GenericReturnObject CreateFromException(Exception ex)
{
if (ex is DbException) // do not show database errors for security reasons
return CreateFailedFatal("A fatal database error has occurred");
else
return CreateFailedFatal(ex.Message);
}
}
~ 26 ~
Appendix B: Code for vnetLinkedlist.js
// Copyright (c) Ralph Varjabedian http://varjabedian.net
//
// vnetLinkedlist.js v 1.0.0
// A file containing an implementation of a double linked list data structure
// July 2008
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
//The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
/*************** LinkedListClass ***************/
LinkedListClass = function()
{
/// <summary>The LinkedListClass is an implementation of a double linked list</summary>

this.IsLinkedListClass = true;
this.linkedList = null;
this.linkedListEnd = null;
this.count = 0;
}
LinkedListClass.prototype =
{
Add : function(object)
{
/// <summary>Adds a new object to the linked list at the end</summary>

if (this.linkedList == null) // empty list
{
this.linkedList = new LinkedListNodeClass(this);
this.linkedList.object = object;
this.linkedListEnd = this.linkedList;
}
else
{
this.linkedListEnd.next = new LinkedListNodeClass(this);
this.linkedListEnd.next.prev = this.linkedListEnd;
this.linkedListEnd = this.linkedListEnd.next;
this.linkedListEnd.object = object;
}
this.count++;
},
RemoveByObject : function(object)
{
/// <summary>Removes a node given the object it holds, if the node is not found, it returns
false</summary>

if (!this.HasNodes())
return false;

var traverse = this.GetListStart();

while (traverse)
{
if (traverse.object == object)
{
traverse.Remove();
return true;
}
~ 27 ~
traverse = traverse.Next();
}

return false;
},
GetListStart : function()
{
/// <summary>Gets the first Node</summary>

return this.linkedList;
},
GetListEnd : function()
{
/// <summary>Gets the last Node</summary>

return this.linkedListEnd;
},
GetSize : function()
{
/// <summary>Gets the size of the list</summary>

return this.count;
},
HasNodes : function()
{
/// <summary>Returns true if the list has nodes</summary>

return this.count > 0;
},
/***** Private ******/
_InsertAfter : function(node, object)
{
if (!node.IsLinkedListNodeClass)
throw"not a node";
if (node.linkedListObject != this)
throw"node does not belong to this list";
var oldNextNode = node.next;

node.next = new LinkedListNodeClass(this);
node.next.prev = node;
node = node.next;
node.object = object;
node.next = oldNextNode;
if (oldNextNode != null)
oldNextNode.prev = node;

if (this.linkedListEnd == node.prev)
this.linkedListEnd = node;

this.count++;
},
_InsertBefore : function(node, object)
{
if (!node.IsLinkedListNodeClass)
throw"not a node";
if (node.linkedListObject != this)
throw"node does not belong to this list";
var oldPrevNode = node.prev;

node.prev = new LinkedListNodeClass(this);
node.prev.next = node;
node = node.prev;
node.object = object;
node.prev = oldPrevNode;
if (oldPrevNode != null)
oldPrevNode.next = node;

if (this.linkedList == node.next)
this.linkedList = node;

this.count++;
},
_Remove : function(node)
~ 28 ~
{
if (!node.IsLinkedListNodeClass)
throw"not a node";
if (node.linkedListObject != this)
throw"node does not belong to this list";

if (node.prev == null) // first node
{
this.linkedList = node.next;
if (this.linkedListEnd == node)
this.linkedListEnd = this.linkedList;
if (this.linkedList)
this.linkedList.prev = null;
}
else if (node.next == null) // last node
{
node.prev.next = null;
this.linkedListEnd = node.prev;
}
else// in the middle
{
node.prev.next = node.next;
node.next.prev = node.prev;
}

this.count--;
node.next = null;
node.prev = null;
node = null;
},
/***** Private ******/

_end : null
}
/*************** LinkedListClass ***************/
/*************** LinkedListNodeClass ***************/
LinkedListNodeClass = function(linkedListObject)
{
/// <summary>The LinkedListNodeClass is the node of a double linked list</summary>
if (!linkedListObject.IsLinkedListClass)
throw"not a linked list class";
this.IsLinkedListNodeClass = true;

this.object = null;
this.next = null;
this.prev = null;
this.linkedListObject = linkedListObject;
}
LinkedListNodeClass.prototype =
{
GetObject : function()
{
/// <summary>Returns the object that the Node is attached to</summary>

return this.object;
},

Next : function()
{
/// <summary>Returns the next node or null if there is no more</summary>

return this.next;
},
Previous : function()
{
/// <summary>Returns the previous node or null if there is no more</summary>

return this.prev;
},
Remove : function()
{
/// <summary>Removes the Node from its containing list</summary>

~ 29 ~
this.linkedListObject._Remove(this);
},
InsertAfter : function(object)
{
/// <summary>Inserts an Object after this Node</summary>

this.linkedListObject._InsertAfter(this, object);
},
InsertBefore : function(object)
{
/// <summary>Inserts an Object before this Node</summary>

this.linkedListObject._InsertBefore(this, object);
},

_end : null
}
/*************** LinkedListNodeClass ***************/
~ 30 ~
Appendix C: Code for WebServiceAjaxCallsHelper.js
/// <reference name="MicrosoftAjax.js"/>
/// <reference name="vnetLinkedlist.js"/>
// Copyright (c) Ralph Varjabedian http://varjabedian.net
//
// WebServiceAjaxCallsHelper.js v 1.0.9
//
// A file containing a manager for WebService Ajax calls, offering several benefits
// 1. Limit the number of active calls at once (queuing manager)
// 2. Multiple calls to the same webservice are ignored (latest/first call considered, other ignored) as
long as the call is not done
// 3. Ability to provide common implementation for success/failure ajax calls
// 4. Ajax calls through multiple subdomains to increase performance, break out of the 2 connections per
domain limitation
//
// July 2008
//
// Tested on: IE7, FF 2.0, Opera 9.5, Win Safari 3
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
/******* Switches *******/
var WSAjaxCallsClass_ProxyScriptLocation = "/scripts/WebServiceAjaxCallsHelperIframe.aspx";
var WSAjaxCallsClass_CallsPerIFrame = 1; // IE has problems with 2 per iframe, which it should not. sticking
to 1 for now
/******* Switches *******/
WSAjaxCallsClass = new Object();
WSAjaxCallsClass.Initialize = function(commonSuccessFunction, commonFailureFunction, randomizingSubdomain,
maxCalls, ignoreMultipleCalls, ignoreMultipleCallsFavorLast)
{
/// <summary>Creates an instance of the class WSAjaxCallsClass. You can only create one instance.</summary>
/// <param name="commonSuccessFunction">A common success function to callback</param>
/// <param name="commonFailureFunction">A common failure function to callback</param>
/// <param name="randomizingSubdomain">A randomizing subdomain if you want to use multiple subdomains, else
pass null</param>
/// <param name="ignoreMultipleCalls">If the class should ignore multiple calls to the same function before
the call is done</param>
/// <param name="ignoreMultipleCallsFavorLast">If the previous parameter is false, then you choose how to
ignore the multiple calls. Favor the last call or the first call</param>
if (this.Initialize.arguments.length != 6)
throw"You must pass to WSAjaxCallsClass 6 arguments";

if (typeof(this.initialized) != "undefined")
throw"Already initialized";
this.initialized = true;

this.IsWSAjaxCallsClass = true;
this.commonSuccessFunction = commonSuccessFunction;
this.commonFailureFunction = commonFailureFunction;
this.randomizingSubdomain = randomizingSubdomain;
this.ignoreMultipleCalls = ignoreMultipleCalls;
this.ignoreMultipleCallsFavorLast = ignoreMultipleCallsFavorLast;
this.AllIFramesReady = false;

this.activeCallsNow = 0;
this.maximumActiveCalls = maxCalls;

this.linkedList = new LinkedListClass();
~ 31 ~
this.original_Sys_Net_WebRequestManager_executeRequest = Sys$Net$XMLHttpExecutor$executeRequest;
Sys$Net$XMLHttpExecutor$executeRequest = Sys.Net.XMLHttpExecutor.prototype.executeRequest =
this.Sys_Net_WebRequestManager_executeRequest;

this.original_Sys_Net_WebServiceProxy_invoke = Sys.Net.WebServiceProxy.invoke;
Sys$Net$WebServiceProxy$invoke = Sys.Net.WebServiceProxy.invoke = this.Sys_Net_WebServiceProxy_invoke;
this.SetDocumentDomain();

if (this.randomizingSubdomain != null)
this.PreCreateIFrames();
}
//WSAjaxCallsClass.trace = function(text) { }
WSAjaxCallsClass.trace = function(text) { Sys.Debug.trace(text); }
WSAjaxCallsClass.Sys_Net_WebRequestManager_executeRequest = function()
{
var This = this;
var url = this._webRequest._url;
var space = url.indexOf(" ");
if (space != -1)
{
var token = parseInt(url.substring(0, space));
this._webRequest._url = WSAjaxCallsClass.randomizingSubdomain.replace("?", token+1) +
url.substring(space + 1);
return WSAjaxCallsClass.DelegateCallToIframe.call(WSAjaxCallsClass, token, This);
}
return WSAjaxCallsClass.original_Sys_Net_WebRequestManager_executeRequest.call(This);
}
WSAjaxCallsClass.Sys_Net_WebServiceProxy_invoke = function(servicePath, methodName, useGet, params,
onSuccess, onFailure, userContext, timeout)
{
WSAjaxCallsClass.QueueCall.apply(WSAjaxCallsClass,
WSAjaxCallsClass.Sys_Net_WebServiceProxy_invoke.arguments);
}
WSAjaxCallsClass.SetDocumentDomain = function()
{
document.initialDomain = document.domain;

var x = window.location.href.toLowerCase().replace("http://", "").replace("https://", "");
var a = x.indexOf("/");
if (a != -1)
x = x.substring(0, a);
a = x.indexOf(":");
if (a != -1)
x = x.substring(0, a);
a = x.lastIndexOf(".");
if (a != -1)
{
a = x.lastIndexOf(".", a - 1);
if (a != -1)
x = x.substring(a + 1);
}
document.domain = x;
}
WSAjaxCallsClass.QueueCall = function()
{
WSAjaxCallsClass.trace("WSAjaxCallsClass.QueueCall: queuing call for WS:["+ this.QueueCall.arguments[0]
+ "] method: ["+ this.QueueCall.arguments[1] + "]");
this.AddToQueue(this.QueueCall.arguments);
this.SignalQueue();
}
WSAjaxCallsClass.signalQueueTimeoutID = 0;
WSAjaxCallsClass.SignalQueue = function()
{
var thisObject = this;
clearTimeout(this.signalQueueTimeoutID);
this.signalQueueTimeoutID = setTimeout(function() { thisObject._SignalQueue(); }, 0);
}
WSAjaxCallsClass.AddToQueue = function(args)
{
if (this.ignoreMultipleCalls && this.linkedList.HasNodes())
{
~ 32 ~
var traverse = this.linkedList.GetListStart();
while (traverse)
{
if (traverse.object[0] == args[0] && traverse.object[1] == args[1]) // servicePath and methodName
{
WSAjaxCallsClass.trace("WSAjaxCallsClass.AddToQueue replaced function "+
args[1].toString());
if (this.ignoreMultipleCallsFavorLast)
traverse.object = args;
return;
}
traverse = traverse.next;
}
}
WSAjaxCallsClass.trace("WSAjaxCallsClass.AddToQueue added function "+ args[1].toString());
this.linkedList.Add(args);
}
WSAjaxCallsClass._SignalQueueActive = false;
WSAjaxCallsClass._SignalQueue = function()
{
if (this._SignalQueueActive)
return;
this._SignalQueueActive = true;

while (this.CanDoCallNow() && this.HasWaitingCalls())
{
var node = this.linkedList.GetListStart();
if (this.CallNow.apply(this, node.object))
node.Remove();
}

this._SignalQueueActive = false;
}
WSAjaxCallsClass.HasWaitingCalls = function()
{
return this.linkedList.HasNodes();
}
WSAjaxCallsClass.CanDoCallNow = function()
{
return (this.activeCallsNow < this.maximumActiveCalls);
}
WSAjaxCallsClass.PreCreateIFrames = function()
{
if (typeof(WSAjaxCallsClass.IframeCollection) == "undefined")
WSAjaxCallsClass.IframeCollection = new Object();
var i;
for (i = 1; i <= Math.ceil(WSAjaxCallsClass.maximumActiveCalls/WSAjaxCallsClass_CallsPerIFrame); i++)
{
var domain = this.randomizingSubdomain.replace("?", i);
WSAjaxCallsClass.CreateIFrame(domain);
}
}
WSAjaxCallsClass.CreateIFrame = function(domain)
{
if (WSAjaxCallsClass.IframeCollection[domain] == null)
{
var subframe = document.createElement("iframe");
var atr = document.createAttribute('height');
atr.value = "0";
subframe.setAttributeNode(atr);

atr = document.createAttribute('width');
atr.value = "0";
subframe.setAttributeNode(atr);

try
{
atr = document.createAttribute('style');
atr.value = "display:none; visibility:hidden;";
subframe.setAttributeNode(atr);
} catch (e) {}

try { subframe.setAttribute("style", "display:none; visibility:hidden;"); }
catch (e) {}
~ 33 ~
subframe.setAttribute("width", 0);
subframe.setAttribute("height", 0);

document.body.appendChild(subframe);
subframe.src = domain + WSAjaxCallsClass_ProxyScriptLocation;
if (!WSAjaxCallsClass.IframeCollection.count)
WSAjaxCallsClass.IframeCollection.count = 0;
WSAjaxCallsClass.IframeCollection[WSAjaxCallsClass.IframeCollection.count++] =
WSAjaxCallsClass.IframeCollection[domain] =
{ subframe : subframe, window : subframe.contentWindow, isBusyCount : 0, isReady : false } ;
}
}
WSAjaxCallsClass.OneIFrameIsReady = function(iframeWindow)
{
var i;
for (i = 0; i < WSAjaxCallsClass.IframeCollection.count; i++)
{
if (WSAjaxCallsClass.IframeCollection[i].window == iframeWindow)
{
WSAjaxCallsClass.IframeCollection[i].isReady = true;
WSAjaxCallsClass.trace("WSAjaxCallsClass.OneIFrameIsReady: new iframe is ready src: "+
WSAjaxCallsClass.IframeCollection[i].subframe.src);
break;
}
}
this.SignalQueue();
var allReady = Math.ceil(WSAjaxCallsClass.maximumActiveCalls/WSAjaxCallsClass_CallsPerIFrame) ==
WSAjaxCallsClass.IframeCollection.count;
if (allReady)
{
for (i = 0; i < WSAjaxCallsClass.IframeCollection.count; i++)
{
if (WSAjaxCallsClass.IframeCollection[i].isReady == false)
{
allReady = false;
break;
}
}
}
if (allReady)
{
WSAjaxCallsClass.AllIFramesReady = true;
WSAjaxCallsClass.trace("WSAjaxCallsClass.OneIFrameIsReady: All iframes is ready");
if (WSAjaxCallsClass.AllIFramesAreReady)
WSAjaxCallsClass.AllIFramesAreReady();
}
if (WSAjaxCallsClass.NewIFrameReady)
WSAjaxCallsClass.NewIFrameReady(allReady);
}
WSAjaxCallsClass.IframeTriggerMethod = 0;
WSAjaxCallsClass.DelegateCallToIframe = function(token, executeThis)
{
if (typeof(this.IframeCollection) == "undefined")
throw"Iframes not created!";

if (this.IframeCollection[token] == null)
throw"Iframe for token does not exist";
WSAjaxCallsClass.trace("WSAjaxCallsClass.DelegateCallToIframe: delegating call to ["+
executeThis._webRequest._url + "] by using token "+ token);
var iframe = this.IframeCollection[token];
var x = function() { iframe.window.docall(executeThis); }
try
{
if (WSAjaxCallsClass.IframeTriggerMethod == 0)
x(); // opera gives access violation here.
else
iframe.window.setTimeout(x, 0);
}
catch (e)
{
WSAjaxCallsClass.trace("WSAjaxCallsClass.DelegateCallToIframe: error calling x(), switching to second
method");
WSAjaxCallsClass.IframeTriggerMethod = 1;
iframe.window.setTimeout(x, 0);
}
}
~ 34 ~
WSAjaxCallsClass.CallNow = function(servicePath, methodName, useGet, params, onSuccess, onFailure,
userContext, timeout)
{
this.activeCallsNow++;

var token = -1;

WSAjaxCallsClass.trace("WSAjaxCallsClass.CallNow: servicePath: ["+ servicePath + "] methodName: ["+
methodName + "]");
if (this.randomizingSubdomain)
{
var weHaveFree = false;
for (token = 0; token < WSAjaxCallsClass.IframeCollection.count; token++)
{
if (WSAjaxCallsClass.IframeCollection[token].isReady == false)
continue;
if (WSAjaxCallsClass.IframeCollection[token].isBusyCount < WSAjaxCallsClass_CallsPerIFrame)
{
weHaveFree = true;
break;
}
}
if (!weHaveFree)
return false;

WSAjaxCallsClass.IframeCollection[token].isBusyCount++; // reserve now
servicePath = token + " "+ servicePath; // a hack for passing the token so that the invoke can know
to delegate this
WSAjaxCallsClass.trace("WSAjaxCallsClass.CallNow: delegating call to servicePath: ["+ servicePath +
"] token: ["+ token + "]");
}
if (this.BeforeAjaxCallStarts != null)
this.BeforeAjaxCallStarts(servicePath, methodName, params, userContext);

var newContext = { userContext : userContext, onSuccess : onSuccess, onFailure : onFailure, servicePath :
servicePath, params : params, token : token };

this.original_Sys_Net_WebServiceProxy_invoke.call(Sys.Net.WebServiceProxy,
servicePath, methodName, useGet, params, WSAjaxCallsClass.WebserviceCallSuccess,
WSAjaxCallsClass.WebserviceCallFailed, newContext, timeout);

return true;
}
WSAjaxCallsClass.WebserviceCallSuccess = function(result, context, methodName)
{
WSAjaxCallsClass.trace("WSAjaxCallsClass.WebserviceCallSuccess: result ["+ result + "] methodName: ["+
methodName + "]");
WSAjaxCallsClass.activeCallsNow--;
if (context.token != -1)
WSAjaxCallsClass.IframeCollection[context.token].isBusyCount--;
WSAjaxCallsClass.SignalQueue();

if (WSAjaxCallsClass.AfterAjaxCallEnds != null)
WSAjaxCallsClass.AfterAjaxCallEnds(context.servicePath, methodName, context.params,
context.userContext, true, result);
if (WSAjaxCallsClass.commonSuccessFunction != null)
{
if (!WSAjaxCallsClass.commonSuccessFunction(result, context.userContext, methodName))
return;
}
context.onSuccess(result, context.userContext, methodName);
}
WSAjaxCallsClass.WebserviceCallFailed = function(result, context, methodName)
{
WSAjaxCallsClass.trace("WSAjaxCallsClass.WebserviceCallFailed: result ["+ result + "] methodName: ["+
methodName + "]");
WSAjaxCallsClass.activeCallsNow--;
if (context.token != -1)
WSAjaxCallsClass.IframeCollection[context.token].isBusyCount--;
WSAjaxCallsClass.SignalQueue();

if (WSAjaxCallsClass.AfterAjaxCallEnds != null)
WSAjaxCallsClass.AfterAjaxCallEnds(context.servicePath, methodName, context.params,
context.userContext, false, result);
if (WSAjaxCallsClass.commonFailureFunction != null)
{
if (!WSAjaxCallsClass.commonFailureFunction(result, context.userContext, methodName))
~ 35 ~
return;
}
context.onFailure(result, context.userContext, methodName);
}
WSAjaxCallsClass.GetRandomizingSubdomainFromDocument = function()
{
/// <summary>A helper function to return a randomizing subdomain string. It uses the location of the current
document. If the top level domain is http://www.example.com then the returned string would be
http://?.example.com </summary>
var x = window.location.href.toLowerCase();
x = x.replace("www.", "");
var a = x.indexOf("://");
a += 3;
x = x.substring(0, a) + "?."+ x.substring(a, x.indexOf("/", a));
return x;
}
~ 36 ~
Appendix D: Code for WebServiceAjaxCallsHelperIframe.aspx
<%@ Page Language="C#"AutoEventWireup="true"CodeBehind="WebServiceAjaxCallsHelperIframe.aspx.cs"
Inherits="ASPNETMultipleSubDomainsAjaxCalls.scripts.WebServiceAjaxCallsHelperIframe"%>
<!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>
<meta http-equiv="expires"content="Thu, 1 Jan 2015 1:1:1 GMT">
</head>
<body onload="onloaddone();">
<!-- Used by WebServiceAjaxCallsHelper.js -->
<!-- For details visit http://varjabedian.net -->
<!-- Please do not remove this notice -->
<form id="form1"runat="server">
<asp:ScriptManager ID="ScriptManager1"runat="server">
<Services>
</Services>
<Scripts>
<asp:ScriptReference Path="~/Scripts/vnetLinkedlist.js"/>
<asp:ScriptReference Path="~/Scripts/WebServiceAjaxCallsHelper.js"/>
</Scripts>
</asp:ScriptManager>
<script type="text/javascript">
function onloaddone()
{
WSAjaxCallsClass.SetDocumentDomain();
window.parent.WSAjaxCallsClass.OneIFrameIsReady(window);
}
function docall(executeThis)
{
Sys$Net$XMLHttpExecutor$executeRequest.call(executeThis);
}
</script>
</form>
</body>
</html>