Sonntag, Februar 19, 2006

Use web services with multiple parameters in the AJAX Engine

In the past moths I've got mails that all request for more functionality in the javascript webservice proxies and the AJAX engine so I've added more support for datatypes and arrays in december last year and got some more compatibility with other SOAP implementations then ASP.NET.

I've also got some requests that describe to reuse existing web services that have multiple parameters on a AJAX page and I think this is a valid request.

While it is possible to use web methods with multiple parameters when using the web service proxies layer directly, the AJAX engine level is only supporting one parameter - but there is a small trick that help you out of this situation.

The AJAX engine knows about all the asynchronous steps that have to be done to complete the whole functionality through the declaration of an action object. The details about that can be found in the documentation at http://www.mathertel.de/ajax/Aspects%20of%20AJAX_index.htm. See the chapter about the "AJAX Actions".

Now have a look how this mechanism can be used with multiple webservice parameters. You can find a sample that simply adds 2 integers at http://www.mathertel.de/AJAXEngine/S02_AJAXCoreSamples/CalcAJAX.aspx

The prepare function is normally used for retrieveing the client-side parameter from the html objects. Because we need more than one parameter we just do nothing here except returning the context object.

The call function, that is called next is not just pointing to the proxy function of the method of the web service but needs some more code. It retrieves all the parameters itself and then calls the proxy method webservice. The trick is, that the mechanism that handles asynchronous result messages is not hooked up correctly and must be patched by using 2 lines of code for the regular and the exception case:

proxies.CalcService.AddInteger.func = this.call.func;
proxies.CalcService.AddInteger.onException = this.call.onException;

Now the call will be processed asynchronously and the result value will be passed to the finish function as usual.

Here the complete code that declares the AJAX action:

// declare an AJAX action
var action1 = {
  delay: 200, // wait for multiple keystrokes from fast typing people
 
  // the prepare function just returns the context object (if any)
  // and makes it available to the call function.
  prepare: function(obj) { return (obj); },

  // the call is not pointing to the webservice proxy directly but calls it using several parameters.
  call: function (obj) {
    var n1 = document.getElementById("n1").value;
    var n2 = document.getElementById("n2").value;
    proxies.CalcService.AddInteger.func = this.call.func; // patch
    proxies.CalcService.AddInteger.onException = this.call.onException; // patch
    proxies.CalcService.AddInteger(n1, n2); // and call
  },
 
  // the result will now be processed as usual.
  finish: function (p) { document.getElementById("outputField").value = p; },
 
  onException: proxies.alertException
} // action1

Kommentare:

Wayne Lee hat gesagt…

Hi Matthias,

Thanks for the awesome job that you have done. I love it so much. Meanwhile I have done some changes in regard with the two limitations that I have found in your solution.

1) Multiple parameters
You have provided a workaround for multiple parameters, which works just fine. However, it would be much better if the feature is built-in and encapsulated. Following are my changes.
Change 1: Add a property called arguments to any action object when parameters are required. The property should return an array containing all arguments. ex.
var action = {
delay: 0,
arguments: function()
{
return new Array(document.getElementById("text4").value, document.getElementById("text5").value);
},
call: proxies.TestWS.AddInt,
finish: function (val)
{
document.getElementById("text6").innerText = val; },
onException: ajaxForms.processException
}

Change 2: Capture the arguments and pass it to web service call in ajax._next function. Code snippet is as follows.

// get the data
if (ca.prepare != null)
try {
data = ca.prepare(co);
} catch (ex) { }

// get the arguments array
if (ca.arguments != null)
try {
args = ca.arguments();
} catch (ex) { }

if (ca.call == null) {
// no call, pass prepared data to finish
ajax.Finish(data);
} else {
// start the call with parameters array if any
ca.call.func = ajax.Finish;
ca.call.onException = ajax.Exception;
ca.call(args);
// start timeout timer
if (ca.timeout != null)
ajax.timer = window.setTimeout(ajax.Cancel, ca.timeout * 1000);
} // if

Change 3: In CallSoap function, capture the passed-in parameters array and generate soap parameter string. Code snippet is as follows:

// parameters
if ( args[0] != null )
{
for (n = 0; (n < p.params.length) && (n < args[0].length); n++) {
var val = args[0][n];

var typ = p.params[n].split(':');

if ((typ.length == 1) || (typ[1] == "string")) {
val = String(args[0][n]).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");

} else if (typ[1] == "int") {
val = parseInt(args[0][n]);
} else if (typ[1] == "float") {
val = parseFloat(args[0][n]);

} else if ((typ[1] == "x") && (typeof(args[0][n]) == "string")) {
val = args[0][n];

} else if ((typ[1] == "x") && (typeof(XMLSerializer) != "undefined")) {
val = (new XMLSerializer()).serializeToString(args[0][n].firstChild);

} else if (typ[1] == "x") {
val = args[0][n].xml;

} else if ((typ[1] == "bool") && (typeof(args[0][n]) == "string")) {
val = args[0][n].toLowerCase();

} else if (typ[1] == "bool") {
val = String(args[0][n]).toLowerCase();

} else if (typ[1] == "date") {
// calculate the xml format for datetime objects from a javascript date object
var s, ret;
ret = String(val.getFullYear());
ret += "-";
s = String(val.getMonth() + 1);
ret += (s.length == 1 ? "0" + s : s);
ret += "-";
s = String(val.getDate() + 1);
ret += (s.length == 1 ? "0" + s : s);
ret += "T";
s = String(val.getHours() + 1);
ret += (s.length == 1 ? "0" + s : s);
ret += ":";
s = String(val.getMinutes() + 1);
ret += (s.length == 1 ? "0" + s : s);
ret += ":";
s = String(val.getSeconds() + 1);
ret += (s.length == 1 ? "0" + s : s);
val = ret;

} else if (typ[1] == "s[]") {
val = "<string>" + args[0][n].join("</string><string>") + "</string>";

} else if (typ[1] == "int[]") {
val = "<int>" + args[0][n].join("</int><int>") + "</int>";

} else if (typ[1] == "float[]") {
val = "<float>" + args[0][n].join("</float><float>") + "</float>";

} else if (typ[1] == "bool[]") {
val = "<boolean>" + args[0][n].join("</boolean><boolean>") + "</boolean>";

} // if

//in case of bad input for number type
if ( isNaN(val) )
val = 0;

soap += "<" + typ[0] + ">" + val + "</" + typ[0] + ">"
} // for
} // if

After all these changes, multiple parameters are enraptured and supported fully.
2) Cache strategy
Your current caching is based on one parameter scenario. It is not suitable for multiple parameters. Since multiple parameters are supported fully after implementation of changes forementioned, a perfect caching for all scenarios becomes doable. Following are my changes.

Change 1: keep _cachekey alive (never set to null) to store current web service arguments for later comparison; and keep _cache store web service return.

change 2: Add a compareArrays to Array prototype (better off putting this in a separate prototype file)
Array.prototype.compareArrays = function(arr) {
if (this.length != arr.length) return false;
for (var i = 0; i < arr.length; i++) {
if (this[i].compareArrays) { //likely nested array
if (!this[i].compareArrays(arr[i])) return false;
else continue;
}
if (this[i] != arr[i]) return false;
}
return true;
}


Change 3: check for existing cache-entry : If all argument entities match, get return from cache; otherwise, re-fetch.
if ( p._cache != null && args[0] != null && p._cachekey != null && args[0].compareArrays(p._cachekey) == true ) {
if (p.func != null) {
p.func(p._cache);
return(null);
} else {
return(p._cache);
} // if
} else {
p._cachekey = args[0];
}// if

Change 4: Always store the return to _cache; never set _cachekey to null as we need it for arguments comparison as above. Changes in _response function.

// store to _cache
cc._cache = ret;

I have tested all changes and everything works nicely. If you want, I would like to send you all files. My name is Wayne Lee and my email is wp_lee2000@hotmail.com. Thanks again for the wonderful solutions.

MatHertel hat gesagt…

1)Multiple parameters
Great solution to the multiple parameters problem in AJAX actions! Better and less complicate than mine. I'll put an array for parameters into the AJAX stack.

2)Cache strategy
That fit's too.
I just do not want to add any feature that any AJAX client needs into the aja.js file and make it bigger and bigger. Instead I think about bringing a intersection / filter mechanism into the AJAX/SOAP proxies infrastructure and make it possible to plug in a feature like caching that comes from another JavaScript include file.
... stay connected.