Asp.Net Ajax: How do you know all of your Ajax calls have completed?
Some Background
Here is a situation I ran into recently: I have an Asp.Net Ajax service (.asmx) which has a method used to create rows in a database similiar to:
[WebMethod]
public int CreateItem(string description, double value)
This method basically created a row in the database and returns the id of the item created. It is called from javascript like this:
function MyButtonClicked() {
MyNamespace.Service.CreateItem("My Item", 5.3, OnSucceeded, OnFailed);
}
function OnSucceeded(result, userContext, methodName) {
}
function OnFailed(error, userContext, methodName) {
}
If you are not familiar with calling Ajax web services from javascript, you can read more from the Calling Web Services from Client Script in ASP.NET AJAX article on asp.net. Basically, I am just calling my CreateItem method (which is part of a .asmx service) from javascript - how this magic happens isn't really too important to understand the problem I was facing.
Notice that the last two parameters I passed into my CreateItem call in javascript are references to callback methods that should be called in the event that my call succeeded or failed. A successful call will end up calling back my OnSucceeded method which has 3 parameters:
- result - this contains any values returned from my method. In this case it will be the id of the item that was inserted.
- userContext - this is an optional item that can be passed into the original call to the method and will just be passed on to the callback method. I am not using this parameter in this situation
- methodName - this is the name of the method that was called which resulted in the OnSucceeded method being called. In my case, this will be the string "CreateItem"
Now for the Problem
Ok, now with all that background I can actually spell out the problem I had to solve. In many cases, I make calls to the CreateItem method many times in a row. So if I need to create three items in the database, I may do something like this in javascript:
MyNamespace.Service.CreateItem("My first Item", 1, OnSucceeded, OnFailed);
MyNamespace.Service.CreateItem("My second Item", 2, OnSucceeded, OnFailed);
MyNamespace.Service.CreateItem("My third Item", 3, OnSucceeded, OnFailed);
Notice that all three calls have the same callback on success. Now lets say I want to create these three items and then as soon as all three calls are completed successfully I want to forward the user to another page. At first you may think I can just do something like this:
function OnSucceeded(result, userContext, methodName) {
if (methodName == "CreateItem") {
window.location = "NextPage.aspx";
}
}
This would forward the user to a new page as soon as our call to CreateItem has completed successfully. There is one huge problem though: how do we know which call to CreateItem succeeded? The first one? The Second one? The third one?
Obviously we only want to forward the user in the case that our final call has succeeded. As you can see, this problem only gets worse if we have other calls on the same page that could be pending at the time we want to forward the user to the next page.
My Solution - Count Your Calls
So here is how I solved the problem: I made a variable which I would use to keep track of how many pending calls there were at any given time. At the top of my javascript page I first declare:
var pendingCalls = 0;
Then, each time I make a call, I increment the counter:
++pendingCalls;
MyNamespace.Service.CreateItem("My first Item", 1, OnSucceeded, OnFailed);
++pendingCalls;
MyNamespace.Service.CreateItem("My second Item", 2, OnSucceeded, OnFailed);
++pendingCalls;
MyNamespace.Service.CreateItem("My third Item", 3, OnSucceeded, OnFailed);
Then I need to make sure I decrement the counter each time a call succeeds or fails:
function OnSucceeded(result, userContext, methodName) {
--pendingCalls;
}
function OnFailed(error, userContext, methodName) {
--pendingCalls;
}
This way, we know how many pending calls we have at any given time. Then to forward the user to a new page once all calls are completed, I did the following:
function OnSucceeded(result, userContext, methodName) {
--pendingCalls;
if (methodName == "CreateItem" && pendingCalls == 0) {
window.location = "NextPage.aspx";
}
}
As you can see, asynchronous programming can be very difficult. Has anybody else had to solve this problem? If so, how did you do it?