Implementing a SOAP client in Javascript
Calling a server from JavaScript is a fundamental part of AJAX applications. Using WebServices with SOAP and WSDL is easy if proxy objects and methods are available in the browser.
Introduction
From the languages and programming environments like C, the .NET CLR and Java we are know proxy generation mechanisms based on IDL and RPC for a long time. These generated classes and files enable the programmer to call a server-side method by calling a local method with the same name. The implementation of the network transfer is taken off your application code.
If you want to implement a communication from JavaScript to WebServices using SOAP it is very important to use an approach that needs only a small amount of code. Complex and long scripts tend to be buggy.
This Proxy generator can be used on its own but is also part of an AJAX framework available through my blog side http://ajaxaspects.blogspot.com/ that is still under development.
Some AJAX implementations use their own way to transport the information between the client and the server. This implementation uses the standard SOAP protocol and works on Internet Explorer and the Firefox browser.
How it works - in short
Webservices can be described by using the formal description standard for WebServices called WSDL (WebService Description Language). Everything we need to know for calling a WebService is available in this XML formatted information and all we need to do is to transform this information into a JavaScript source code syntax that can be directly executed by using a XSLT based translation. A common include file is used for bringing the core implementations of the SOAP protocol.
Using the proxy
To make these proxy functions work a common JavaScript include (ajax.js) file and a file that generates the WebService specific code must included.
<script type="text/javascript" src="ajax.js"></script>
<script type="text/javascript" src="GetJavaScriptProxy.aspx?service=calcservice.asmx"></script>
The complete source of ajax.js can be viewed here: view ajax.js.
The implementation of the real communication details are implemented in the ajax.js file. A variable named proxies is created as an empty JavaScript Object and this is the only one global variable that we need. The individual proxys are then attached to this Object to minimize the naming conflicts that may occur.
The second script include now retrieves the WSDL description of the WebService and generates the specific JavaScript for this service containing local proxy methods that just can be called to execute the corresponding method on the server.
Ansynchronous calls
Calling a server-side method may look like this:
proxies.CalcService.CalcPrimeFactors.func = displayFactors; // hook up a method that gets the response
proxies.CalcService.CalcPrimeFactors(12); // now call the server
// The return value is passed to this function as a parameter
function displayFactors (retVal) {
document.getElementById("outputField").value = retVal;
} // displayFactors
Here you see an asynchronous call. The function CalcPrimeFactors() returns immediately and the client side scripting continues. After some milliseconds (or longer) the server will send back the result of the called method of the WebService and the value will be passed to the hooked up method as a parameter.
Synchronous calls
There is also a synchronous version that can be used. In this case the func attribute must remain unset or null and the result of the server-side method is directly returned from the client side method call. This kind of calling the server may block for some milliseconds because no user-events like typing or clicking are processed during the call.
proxies.CalcService.func = null; // no hook up function !
var f = proxies.CalcService.CalcPrimeFactors(12); // call the server and return the result.
Implementation details
Here is a sample extract of the code that is generated for the client to shows how the mechanism works.
The include file ajax.js generates the global object named ajax:
var proxies = new Object();
Per WebService an object named like the WebService is attached to the ajax object to hold the service specific information like the url and the namespace of the WebService:
// JavaScript proxy for webservices
// A WebService for the calculation of prime factors.
proxies.CalcService = {
url: "http://localhost:1049/CalcFactors/CalcService.asmx",
ns: "http://www.mathertel.de/CalcFactorsService/"
} // proxies.CalcService
For each WebService method a function on the client is created that mirrors the method on the server. The information we need to build up the full SOAP message is attached to the function object as attributes.
// Add 2 numbers.
proxies.CalcService.AddInteger = function () { return(proxies.callSoap(arguments)); }
proxies.CalcService.AddInteger.fname = "AddInteger";
proxies.CalcService.AddInteger.service = proxies.CalcService;
proxies.CalcService.AddInteger.action = "http://www.mathertel.de/CalcFactors/AddInteger";
proxies.CalcService.AddInteger.params = ["number1:int","number2:int"];
proxies.CalcService.AddInteger.rtype = ["AddIntegerResult:int"];
Caching
The proxy implementation also offers a client-side caching feature. An approach that leads to less traffic on the net because repeating the same calls can be prevented.
The Http caching features, instrumented by using HTTP headers do not help in these situations because the request is not an http-get request and there is always a payload in the http body. Caching must therefore be realized by some scripting on the client.
The caching feature in the JavaScript WebService proxy implementation can be enabled by calling the method proxies.EnableCache and passing the function that should further use caching. There is a button in the CalcFactorsAJAX.htm sample to show how to enable this:
proxies.EnableCache(proxies.CalcService.CalcPrimeFactors)
By calling this method a JavaScript object is added that stores all results and is used to prevent a call to the server if an entry for the parameter already exists inside this object. This is not a perfect solution, but it works under the following circumstances:
- The parameter must be a string or number that can be used for indexing the properties of a JavaScript object.
- The cache doesn't clear itself. It can be cleared by calling EnableCache once again.
- Only methods with a single parameter are supported.
Proxies Object Reference
This is the map of the objects and properties that are used for the proxy functions to work:
Public usable members
Property | Usage |
---|---|
proxies.service.function() | Calling a server-side method. |
proxies.service.function.func | This property is used to assign the function that receives the result of the server call when commincating asynchronously. |
proxies.service.function.onException | This property is used to assign the function that receives the exceptions of the communication and server site execution of the call. |
proxies.service.function.corefunc | Debugging helper function that should normally be null. See below. |
proxies.EnableCache(func) | The function for enabling the caching feature of a specific server-side function. |
proxies.IsActive() | This function returns true when a asynchronous call is started and waiting for the response. |
proxies.cancel(raise) | This function can be called to cancel the running asynchronous WebService call. the parameter raise can be set to false to prevent the raising an exception. |
proxies.alertResult | A function that can be used to show the result of a SOAP response as text. See below. |
proxies.alertResponseText | A function that can be used to show the real text of a SOAP response. See below. |
proxies.alertException | A function that can be used to show eventually thrown exceptions. This is the simples case of passing exceptions to the user. |
Members set by the proxy generator
Property | Usage |
---|---|
proxies.service.url | URL of the WebServices. |
proxies.service.ns | Namespace of the WebServices. |
proxies.service.function() | A small function is generated thet starts the communication by calling proxies.callSoap(). |
proxies.service.function.fname | Name of the method. |
proxies.service.function.action | Soapaction of the method, used in the http header. |
proxies.service.function.params | Array with the names and types of the parameters. |
proxies.service.function.service | A link back to the service object. |
proxies.EnableCache(func) | The method for enabling the caching feature. |
Private members
Property | Usage |
---|---|
proxies.callSoap() | This function implements the start of the client-server communication by sending a SOAP message. |
proxies._response() | The callback function for the XMLHttpRequest object that dispatches the response to servive.func or service.onException. |
ajax._getXMLDOM() | Get a browser specific implementation of the XMLDOM object, containing a XML document. |
proxies.current | A property that references the current executed function during a call. |
proxies.xmlhttp | A property that hold the current active XMLHttpRequest object during a call. |
Analysing problems
With proxies.service.function.corefunc is an entry point in the core implementation of the proxies object available that may be helpful when analysing problems.
To set up a debug output in an alert box that displays the response of a WebService call use:
proxies.CalcService.CalcPrimeFactors.corefunc = proxies.alertResult;
If the full response text of the received SOAP message is needed you can use:
proxies.CalcService.CalcPrimeFactors.corefunc = proxies.alertResponseText;
Instead of attaching a special function for the further processing of the respose of a WebService Call it is possiblble to just hook up the window.alert function to display the result.
proxies.CalcService.CalcPrimeFactors.func = window.alert;
I recommend always attaching a function for handling possible exceptions, at least while developing. When calling a WebService asynchonously there will be no executing code that can catch an exception so you must provide a method in this case.
proxies.CalcService.CalcPrimeFactors.onException = proxies.alertException;
If there are still problems with the communication of the SOAP messages I recommend to use a http monitor tool like Fiddler: http://www.fiddlertool.com
Supported datatypes
With Version 2.0 there is now more support for different datatypes.
Simple datatypes
Up to now only those methods where supported that where converting of the parameters and result values was not necessary. This applies to strings and numbers.
With this version the datatypes defined on the server and the WSDL are passed to the client so that the datatypes can be converted using JavaScript at runtime. In the generated proxy code, the listing of the names of the parameters is now extended by an optional specification of the datatype. Without this the values are treated as strings.
In the HTML object model, the JavaScript datatypes are not well supported. The value that is displayed inside an HTML input field is always a string, even if it’s containing only digits. So when calling the proxy functions all the parameters are also accepted as JavaScript strings and converted (if possible) to the right types.
XML data
Passing XML documents was implemented to make it possible to pass complex data. In the supported browser clients the XMLDocument Object from Microsoft or Firefox and on the server the .NET XmlDocument class can be used.
A Method has to be is declared in C# like this:
[WebMethod()]
public XmlDocument Calc(XmlDocument xDoc) {
...
return (xDoc);
} // Calc
The proxy functions also accept the XML document as a string type. In this case, the contents of the passed string is passed directly to the server any must for this reason contain a valid XML document without the declarations any without any "XML processing Instructions" like <? ... ?>.
With this datatype it is possible to pass complex data directly to the server an there is no need to define a method with many parameters if using this datatype. If the data scheme is extended with new fields it will not be necessary to give a new signature to the WebService.
The disadvantage of this approach is that the content of the XML document cannot be validated by the WebService infrastructure because there is no schema for this part of the conversation available.
Datatype mappings
XML datatypes | Alias in the proxy attributes | JavaScript Datatype |
---|---|---|
string | string / null | String |
int, unsignedInt, short, unsignedShort, unsignedLong,s:long |
int | Number (parseInt) |
double, float | float | Number (parseFloat) |
dateTime | date | Date |
boolean | bool | Boolean |
System.Xml.XmlDocument | x |
In Mozilla / Firefox: XMLDocument In Internet Explorer: ActiveXObject("Microsoft.XMLDOM") ActiveXObject("MSXML2.DOMDocument") |
The implementation of the call
The transmission of the SOAP/XML Messages can be implemented using the appropriate XMLHTTP object that is available in many state-of-the-art browsers today. This implementation was (until now) tested with Internet Explorer and Firefox.
/// <summary>Get a browser specific implementation of the XMLHTTP object.</summary>
function getXMLHTTP() {
var obj = null;
try {
obj = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) { }
if (obj == null) {
try {
obj = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) { }
} // if
if ((obj == null) && (typeof XMLHttpRequest != "undefined"))
obj = new XMLHttpRequest();
return(obj);
} // getXMLHTTP
This object is implemented in different technologies, depending on the available technologies in the browsers. It was first developed by Microsoft in the Internet Explorer as an ActiveX control and the Mozilla developers re-implemented it by providing the same methods and properties. A call can be done using the following sequence of methods:
x.open("POST", p.service.url, true); // async call
x.setRequestHeader("SOAPAction", p.action);
x.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
x.onreadystatechange = p.corefunc; // hook up a method for the result processing
x.send(soap); // send a soap request
More details and some more internal description can be found in ajax.js include file.
A Proxy generator for JavaScript
Retrieving a WSDL description is very easy when implementing in ASP.NET. You navigate to the URL of the WebService and use the link that is available on this page. You can also attach a WSDL Parameter:
http://www.mathertel.de/AJAXEngine/S02_AJAXCoreSamples/CalcService.asmx?WSDL
The proxy generator retrieves this XML document by using an HttpWebRequest. By using a XSLT transformation it is now very simple way to implementing a WSDL to JavaScript compiler.
The complex part lies in writing the right transformations. Inside the wsdl.xslt file you can find the templates of the JavaScript code that define these proxy objects. Instead of generating another XML document this transformation produces plain text that is valid JavaScript code.
GetJavaScriptProxy.aspx sourcecode
<%@ Page Language="C#" Debug="true" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.Xml" %>
<%@ Import Namespace="System.Xml.Xsl" %>
<!--
/// 19.07.2005 white space removed
/// 20.07.2005 more datatypes and XML Documents
/// 04.09.2005 XslCompiledTransform
-->
<script runat="server">
private string FetchWsdl(string url) {
Uri uri = new Uri(Request.Url, url + "?WSDL");
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
req.Credentials = CredentialCache.DefaultCredentials;
// req.Proxy = WebRequest.DefaultWebProxy; // running on the same server !
req.Timeout = 6 * 1000; // 6 seconds
WebResponse res = req.GetResponse();
#if DOTNET11
XmlDocument data = new XmlDocument();
data.Load(res.GetResponseStream());
XslTransform xsl = new XslTransform();
xsl.Load(Server.MapPath("~/ajaxcore/wsdl.xslt"));
System.IO.StringWriter sOut = new System.IO.StringWriter();
xsl.Transform(data, null, sOut, null);
#else
XmlReader data = XmlReader.Create(res.GetResponseStream());
XslCompiledTransform xsl = new XslCompiledTransform();
xsl.Load(Server.MapPath("~/ajaxcore/wsdl.xslt"));
System.IO.StringWriter sOut = new System.IO.StringWriter();
xsl.Transform(data, null, sOut);
#endif
return (sOut.ToString());
} // FetchWsdl
</script>
<%
string asText = Request.QueryString["html"];
Response.Clear();
if (asText != null) {
Response.ContentType = "text/html";
Response.Write("<pre>");
} else {
Response.ContentType = "text/text";
} // if
string fileName = Request.QueryString["service"];
if (fileName == null)
fileName = "CalcService";
// get good filenames only (local folder)
if ((fileName.IndexOf('$') >= 0) || (Regex.IsMatch(fileName, @"\b(COM\d|LPT\d|CON|PRN|AUX|NUL)\b", RegexOptions.IgnoreCase)))
throw new ApplicationException("Error in filename.");
if (! Server.MapPath(fileName).StartsWith(Request.PhysicalApplicationPath, StringComparison.InvariantCultureIgnoreCase))
throw new ApplicationException("Can show local files only.");
string ret = FetchWsdl(fileName);
ret = Regex.Replace(ret, @"\n *", "\n");
ret = Regex.Replace(ret, @"\r\n *""", "\"");
ret = Regex.Replace(ret, @"\r\n, *""", ",\"");
ret = Regex.Replace(ret, @"\r\n\]", "]");
ret = Regex.Replace(ret, @"\r\n; *", ";");
Response.Write(ret);
%>
wsdl.xslt sourcecode
<?xml version="1.0" ?>
<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:fmt="urn:p2plusfmt-xsltformats"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
>
<!--
/// 19.07.2005 optional documentation
/// 20.07.2005 more datatypes and XML Documents
/// 20.07.2005 more datatypes and XML Documents fixed
-->
<xsl:strip-space elements="*" />
<xsl:output method="text" version="4.0" />
<xsl:param name="alias">
<xsl:value-of select="wsdl:definitions/wsdl:service/@name" />
</xsl:param>
<xsl:template match="/">
// javascript proxy for webservices
// by Matthias Hertel
/* <xsl:value-of select="wsdl:definitions/wsdl:documentation" /> */
<xsl:for-each select="/wsdl:definitions/wsdl:service/wsdl:port[soap:address]">
<xsl:call-template name="soapport" />
</xsl:for-each>
</xsl:template>
<xsl:template name="soapport">
proxies.<xsl:value-of select="$alias" /> = {
url: "<xsl:value-of select="soap:address/@location" />",
ns: "<xsl:value-of select="/wsdl:definitions/wsdl:types/s:schema/@targetNamespace" />"
} // proxies.<xsl:value-of select="$alias" />
<xsl:text>
</xsl:text>
<xsl:for-each select="/wsdl:definitions/wsdl:binding[@name = substring-after(current()/@binding, ':')]">
<xsl:call-template name="soapbinding11" />
</xsl:for-each>
</xsl:template>
<xsl:template name="soapbinding11">
<xsl:variable name="portTypeName" select="substring-after(current()/@type, ':')" />
<xsl:for-each select="wsdl:operation">
<xsl:variable name="inputMessageName" select="substring-after(/wsdl:definitions/wsdl:portType[@name = $portTypeName]/wsdl:operation[@name = current()/@name]/wsdl:input/@message, ':')" />
<xsl:variable name="outputMessageName" select="substring-after(/wsdl:definitions/wsdl:portType[@name = $portTypeName]/wsdl:operation[@name = current()/@name]/wsdl:output/@message, ':')" />
<xsl:for-each select="/wsdl:definitions/wsdl:portType[@name = $portTypeName]/wsdl:operation[@name = current()/@name]/wsdl:documentation">
/** <xsl:value-of select="." /> */
</xsl:for-each>
proxies.<xsl:value-of select="$alias" />.<xsl:value-of select="@name" /> = function () { return(proxies.callSoap(arguments)); }
proxies.<xsl:value-of select="$alias" />.<xsl:value-of select="@name" />.fname = "<xsl:value-of select="@name" />";
proxies.<xsl:value-of select="$alias" />.<xsl:value-of select="@name" />.service = proxies.<xsl:value-of select="$alias" />;
proxies.<xsl:value-of select="$alias" />.<xsl:value-of select="@name" />.action = "<xsl:value-of select="soap:operation/@soapAction" />";
proxies.<xsl:value-of select="$alias" />.<xsl:value-of select="@name" />.params = [<xsl:for-each select="/wsdl:definitions/wsdl:message[@name = $inputMessageName]">
<xsl:call-template name="soapMessage" />
</xsl:for-each>];
proxies.<xsl:value-of select="$alias" />.<xsl:value-of select="@name" />.rtype = [<xsl:for-each select="/wsdl:definitions/wsdl:message[@name = $outputMessageName]">
<xsl:call-template name="soapMessage" />
</xsl:for-each>];
</xsl:for-each>
</xsl:template>
<xsl:template name="soapMessage">
<xsl:variable name="inputElementName" select="substring-after(wsdl:part/@element, ':')" />
<xsl:for-each select="/wsdl:definitions/wsdl:types/s:schema/s:element[@name=$inputElementName]//s:element">
<xsl:choose>
<xsl:when test="@type='s:string'">
"<xsl:value-of select="@name" />"
</xsl:when>
<xsl:when test="@type='s:int' or @type='s:unsignedInt' or @type='s:short' or @type='s:unsignedShort' or @type='s:unsignedLong' or @type='s:long'">
"<xsl:value-of select="@name" />:int"
</xsl:when>
"<xsl:value-of select="@name" />:float"
</xsl:when>
<xsl:when test="@type='s:dateTime'">
"<xsl:value-of select="@name" />:date"
</xsl:when>
<xsl:when test="./s:complexType/s:sequence/s:any">
"<xsl:value-of select="@name" />:x"
</xsl:when>
<xsl:otherwise>
"<xsl:value-of select="@name" />"
</xsl:otherwise>
</xsl:choose>
<xsl:if test="position()!=last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The files for the implementation and more samples are also available live on the sample website for my ajax project: http://www.mathertel.de/AJAXEngine