Click here to Skip to main content
Click here to Skip to main content
Go to top

A proxy generator to WebServices for JavaScript and AJAX

, 20 Sep 2005
Rate this:
Please Sign up or sign in to vote.
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 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;  // 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 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; // 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 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:

// 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 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:

// 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 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 strings 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 strings.

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 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 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:

///<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 by 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");
// hook up a method for the result processing
x.onreadystatechange = p.corefunc; 
x.send(soap); // send a soap request

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" %>

<!-- 
/// 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;
    // running on the same server !
    // req.Proxy = WebRequest.DefaultWebProxy; 
    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);
%>

The source code of wsdl.xslt

<?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: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

Share

About the Author

Matthias Hertel
Architect Deutsche Bank AG
Germany Germany
see http://www.mathertel.de
Follow on   Google+

Comments and Discussions

 
GeneralEXCELLENT!! Pinmembermerlin9818-May-08 6:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140916.1 | Last Updated 20 Sep 2005
Article Copyright 2005 by Matthias Hertel
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid