Introduction
From the languages and programming environments like C, the .NET CLR and Java, we know the proxy generation mechanisms that are 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 network transfer is taken off from your application code.
If you want to implement a communication from JavaScript to web services 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 a part of an AJAX framework available through my blog site that is still under development.
Some AJAX implementations use their own way to transport 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
Web services can be described by using the formal description standard for web services called WSDL (Web Service Description Language). Everything we need to know for calling a web service is available in this XML formatted information and all we need to do is transform this information into a JavaScript source code syntax that can be directly executed by using an 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 web service specific code must be included:
<script type="text/javascript" src="ajax.js"></script>
<script type="text/javascript"
src="GetJavaScriptProxy.aspx?service=CalcService.asmx">
</script>
The implementation of 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 global variable that we need. The individual proxies 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 web service and generates the specific JavaScript for this service containing local proxy methods that can be called to execute the corresponding method on the server.
Asynchronous Calls
Calling a server-side method may look like this:
proxies.CalcService.CalcPrimeFactors.func =
displayFactors;
proxies.CalcService.CalcPrimeFactors(12);
function displayFactors (retVal) {
document.getElementById("outputField").value = retVal;
}
Here, you seen an asynchronous call. The function CalcPrimeFactors()
returns immediately and the client side scripting continues. After a few milliseconds (or longer), the server will send back the result of the called method of the web service 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 function attribute must remain unset or null
and the result of the server-side method is directly returned from the client side method call. This way 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;
var f = proxies.CalcService.CalcPrimeFactors(12);
Implementation Details
Here is a sample extract of the code that is generated for the client to show 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
:
proxies.CalcService = {
url: "http://localhost:1049/CalcFactors/CalcService.asmx",
ns: "http://www.mathertel.de/CalcFactorsService/"
}
For each web service 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:
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 web service 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 that shows how to enable this:
proxies.EnableCache(proxies.CalcService.CalcPrimeFactors)
By calling this method, a JavaScript object is added that stores all the 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 only 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.
Reference
This is the map of the objects and properties that are used for the proxy functions to work:
Property | Usage |
proxies.service.url | URL of the WebServices |
proxies.service.ns | Namespace of the WebServices |
proxies.service.function() | Calling a server-side method |
proxies.service.function.fname | Name of the method |
proxies.service.function.action | SOAP action of the method, used in the HTTP header |
proxies.service.function.params | Array with the names and types of the parameters |
proxies.service.function.func | Function for receiving the result |
proxies.service.function.onException | Function to handle an exception |
proxies.service.function.corefunc | Debugging helper function |
proxies.service.function.service | A link back to the service object |
proxies.EnableCache(func) | The method for enabling the caching feature |
Supported Data Types
With version 2.0, there is now more support for different data types.
Simple Data Types
Till now, only those methods were supported that were converting the parameters and the result values were not necessary. This applies to string
s and numbers.
With this version, the data types defined on the server and the WSDL are passed to the client so that the data types 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 data type. Without these, the values are treated as string
s.
In the HTML object model, the JavaScript data types are not well supported. The value that is displayed inside an HTML input field is always a string
, even if it contains only digits. So, when calling the proxy functions, all the parameters are also accepted as JavaScript string
s 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 declared in C# like this:
[WebMethod()]
public XmlDocument Calc(XmlDocument xDoc) {
...
return (xDoc);
}
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 and it must for this reason contain a valid XML document without the declarations and without any "XML processing instructions" like <? ... ?>
.
With this data type, it is possible to pass complex data directly to the server. And there is no need to define a method with many parameters if we use this data type. If the data scheme is extended with new fields, it will not be necessary to give a new signature to the web service.
The disadvantage of this approach is that the content of the XML document cannot be validated by the web service infrastructure because there is no schema for this part of the conversation available.
Data Type Mappings
XML data types | Alias in the proxy attributes | JavaScript Data type |
string | string / null | String |
int , unsignedInt ,
short , unsignedShort ,
unsignedLong , slong | 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:
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 ((obj == null) &&
(typeof XMLHttpRequest != "undefined"))
obj = new XMLHttpRequest();
return(obj);
}
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 by using the following sequence of methods:
x.open("POST", p.service.url, true);
x.setRequestHeader("SOAPAction", p.action);
x.setRequestHeader("Content-Type",
"text/xml; charset=utf-8");
x.onreadystatechange = p.corefunc;
x.send(soap);
More details and some more internal description can be found in the ajax.js include file.
A Proxy Generator for JavaScript
Retrieving a WSDL description is very easy when implemented in ASP.NET. You navigate to the URL of the web service and use the link that is available on this page. You can also attach a WSDL parameter.
The proxy generator retrieves this XML document by using an HttpWebRequest
. By using an XSLT transformation, it is now very simple to implement 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 defines these proxy objects. Instead of generating another XML document, this transformation produces plain text that is valid JavaScript code.
The Source Code of GetJavaScriptProxy.aspx
<%@ Page Language="C#" Debug="true" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.Xml" %>
<%@ Import Namespace="System.Xml.Xsl" %>
<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.Timeout = 6 * 1000;
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());
}
</script>
<%
string asText = Request.QueryString["html"];
Response.Clear();
if (asText != null) {
Response.ContentType = "text/html";
Response.Write("<pre>");
} else {
Response.ContentType = "text/text";
}
string fileName = Request.QueryString["service"];
if (fileName == null)
fileName = "CalcService";
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);
%>
The Source Code of wsdl.xslt
="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/">
<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:when test="@type='s:double' or @type='s:float'">
"<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>
History
- 6th July, 2005: First version published
- Simple data types without conversion
- 7th September, 2005: Second version published
- Simple data with conversion
- XML data type support
- Caching feature added
- 16th December, 2005: Broken links corrected
The files for implementation and more samples are available in the sample website of my AJAX project.
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.