Now that you have a basic understanding of web services, and have created your own, you're probably wondering what this has to do with Ajax. Quite simply, web services are another avenue for Ajax applications to retrieve information. In the last chapter, you learned how to retrieve and use RSS and Atom feeds to display information to the user, which is very similar to using web services. The main difference is that by using web services, you are able to pass information to the server that can be manipulated and sent back; you aren't simply pulling information.
You can use JavaScript to consume a web service from within a web page so long as your users have a modern browser. Internet Explorer 5.0 and higher, as well as the Mozilla family of browsers (including Firefox), all have some functionality that allows web services to be consumed.
First, you'll need a test harness to test the various approaches to calling web services from the browser. This test harness is fairly simple: there is a list box to select one of the four arithmetic operations to execute, a text box for each of the two operands, and a button to invoke the service. These controls are disabled until the page has fully loaded. Below those controls is another text box to display any valid result as well as two text areas to display the request and response data:
<html> <head> <title>Web Service Test Harness</title> <script type=" text/javascript"> var SERVICE_URL = "http://localhost/Math/Math.asmx"; var SOAP_ACTION_BASE = "http://www.wrox.com/services/math"; function setUIEnabled(bEnabled) { var oButton = document.getElementById("cmdRequest"); oButton.disabled = !bEnabled; var oList = document.getElementById("lstMethods"); oList.disabled = !bEnabled } function performOperation() { var oList = document.getElementById("lstMethods"); var sMethod = oList.options[oList.selectedIndex].value; var sOp1 = document.getElementById("txtOp1").value; var sOp2 = document.getElementById("txtOp2").value; //Clear the message panes document.getElementById("txtRequest").value = ""; document.getElementById("txtResponse").value = ""; document.getElementById("txtResult").value = ""; performSpecificOperation(sMethod, sOp1, sOp2); } </script> </head> <body onload=" setUIEnabled(true)"> Operation: <select id=" lstMethods" style=" width: 200px" disabled=" disabled"> <option value=" add" selected=" selected">Add</option> <option value=" subtract">Subtract</option> <option value=" multiply">Multiply</option> <option value=" divide">Divide</option> </select> <br/><br/> Operand 1: <input type=" text" id=" txtOp1" size="10"/><br/> Operand 2: <input type=" text" id=" txtOp2" size="10"/><br/><br/> <input type=" button" id=" cmdRequest" value=" Perform Operation" onclick=" performOperation();" disabled=" disabled"/> <br/><br/> Result: <input type=" text" size="20" id=" txtResult"> <br/> <textarea rows="30" cols="60" id=" txtRequest"></textarea> <textarea rows="30" cols="60" id=" txtResponse"></textarea> </body> </html>
The setUIEnabled() function is used to enable and disable the user interface of the test harness. This ensures that only one request is sent at a time. There are also two constants defined, SERVICE_URL and SOAP_ACTION_BASE, that contain the URL for the web service and the SOAP action header required to call it, respectively. The button calls a function named performOperation(), which gathers the relevant data and clears the text boxes before calling performSpecificOperation(). This method must be defined by the particular test being used to execute the web service call (which will be included using a JavaScript file). Depending on your personal browser preferences, the page resembles Figure 6-7.
In an early effort to get developers excited about web services, Microsoft developed and released a web service behavior for Internet Explorer. Behaviors enable you to redefine the functionality, properties, and methods of an existing HTML element or to create an entirely new one. The advantage of behaviors is the encapsulation of functionality into a single file, ending with the .htc extension. Although the promise of behaviors never lived up to the hype, the web service behavior is a solid component that can be very useful to web developers. You can download the behavior from http://msdn.microsoft.com/library/default.asp?url=/workshop/author/webservice/webservice.asp.
Note |
This file and the others associated with this chapter are included in the code download for Professional Ajax, at www.wrox.com. |
You can add behaviors to elements in a number of ways. The most straightforward is to use the element's CSS style property. To add the web service behavior to a <div/>, the code would be:
<div id=" divServiceHandler" style=" behavior: url(webservice.htc);"></div>
This assumes that the behavior is in the same folder on the server as the web page.
To show how to use this behavior, create a new version of the test harness and insert the highlighted line directly below the <body/> element, as follows:
<body onload=" setUIEnabled(true);">
<div id=" divServiceHandler" style=" behavior: url(webservice.htc);"></div>
Operation: <select id=" lstMethods" style=" width: 200px" name=" lstMethods"
disabled=" disabled">
The next step is to define the performSpecificOperation() method specific to using the web service behavior. This method accepts three arguments: the method name and the two operands. The code is as follows:
var iCallId = 0; function performSpecificOperation(sMethod, sOp1, sOp2) { var oServiceHandler = document.getElementById("divServiceHandler"); if (!oServiceHandler.Math) { oServiceHandler.useService(SERVICE_URL + "?WSDL", "Math"); } iCallId = oServiceHandler.Math.callService(handleResponseFromBehavior, sMethod, sOp1, sOp2); }
A variable, iCallId, is initialized to zero. Although this plays no part in the test, it can be used to keep track of multiple simultaneous calls. Then, a reference to the <div/> element that has the behavior attached is stored in oServiceHandler. Next, a test is done to see if the behavior has already been used by checking to see whether the Math property exists. If it doesn't exist, you must set up the behavior by passing the URL of the WSDL file and a identifying name for the service to useService(). The reason for the identifier is to enable the behavior to use more than one service at a time. The callService() method is then executed, passing in a callback function (handleResponseFromBehavior()), the name of the method, and the two arguments.
When the response is received, the callback function, handleResponseFromBehavior(), will be called:
function handleResponseFromBehavior(oResult) { var oResponseOutput = document.getElementById("txtResponse"); if (oResult.error) { var sErrorMessage = oResult.errorDetail.code + "\n" + oResult.errorDetail.string; alert("An error occurred:\n" + sErrorMessage + "See message pane for SOAP fault."); oResponseOutput.value = oResult.errorDetail.raw.xml; } else { var oResultOutput = document.getElementById("txtResult"); oResultOutput.value = oResult.value; oResponseOutput.value = oResult.raw.xml; } }
The callback function is passed an oResult object containing details about the call. If the error property is not zero, the relevant SOAP fault details are displayed; otherwise oResult.value, the returned value, is displayed on the page.
You can place the performSpecificOperation() and handleResponseFromBehavior() functions in an external JavaScript file and include them in the test harness page using the <script/> element, as follows:
<script type=" text/javascript" src=" WebServiceExampleBehavior.js"></script>
As you can see, using the web service behavior is fairly straightforward. All the work is done by the behavior behind the scenes, and although the webservice.htc file is a bit large for a script file (51KB), it can provide some very useful functionality.
Note |
If you want to see how the behavior works, feel free to examine the webservice.htc file in a text editor. Be warned, however; it was not meant as a tutorial and contains nearly 2300 lines of JavaScript. |
Modern Mozilla-based browsers, such as Firefox and Netscape, have some high-level SOAP classes built into their implementation of JavaScript. These browsers seek to wrap the basic strategy of making SOAP calls with easier-to-use classes. As with the previous example, you first must define the performSpecificOperation() function:
function performSpecificOperation(sMethod, sOp1, sOp2) { var oSoapCall = new SOAPCall(); oSoapCall.transportURI = SERVICE_URL; oSoapCall.actionURI = SOAP_ACTION_BASE + "/" + sMethod; var aParams = []; var oParam = new SOAPParameter(sOp1, "op1"); oParam.namespaceURI = SOAP_ACTION_BASE; aParams.push(oParam); oParam = new SOAPParameter(sOp2, "op2"); oParam.namespaceURI = SOAP_ACTION_BASE; aParams.push(oParam); oSoapCall.encode(0, sMethod, SOAP_ACTION_BASE, 0, null, aParams.length, aParams); var oSerializer = new XMLSerializer(); document.getElementById("txtRequest").value = oSerializer.serializeToString(oSoapCall.envelope); setUIEnabled(false); //more code here}
This script takes advantage of a number of built-in classes, the first of which is SOAPCall. This class wraps web service functionality in a similar way to the web service behavior for Internet Explorer. After creating an instance of SOAPCall, you set two properties: transportURI, which points to the web service location, and actionURI, which specifies the SOAP action and method name.
Next, two parameters are created using the SOAPParameter constructor, which takes the value and the name of the parameter to create. Each parameter has its namespace URI set to the value of the targetNamespace in the WSDL schema section. In theory this shouldn't be necessary, but the Mozilla SOAP classes seem to be designed with the RPC style in mind and our service uses the document style, so this extra step is needed. Both of these parameters are pushed onto the aParams array. The encode() method prepares all the data for the call. There are seven parameters for this call. The first is the version of SOAP being used, which can be set to zero unless it is important that a specific version is needed. The second parameter is the name of the method to use, and the third is that of the targetNamespace from the schema portion of the WSDL file. The next parameter is the count of how many extra headers are needed in the call (none in this case), followed by an array of these headers (here set to null). The last two parameters contain the number of SOAPParameter objects being sent and the actual parameters, respectively.
Next, you actually need to send the request, which you can do by using the asyncInvoke() method, as follows:
function performSpecificOperation(sMethod, sOp1, sOp2) { var oSoapCall = new SOAPCall(); oSoapCall.transportURI = SERVICE_URL; oSoapCall.actionURI = SOAP_ACTION_BASE + "/" + sMethod; var aParams = []; var oParam = new SOAPParameter(sOp1, "op1"); oParam.namespaceURI = SOAP_ACTION_BASE; aParams.push(oParam); oParam = new SOAPParameter(sOp2, "op2"); oParam.namespaceURI = SOAP_ACTION_BASE; aParams.push(oParam); oSoapCall.encode(0, sMethod, SOAP_ACTION_BASE, 0, null, aParams.length, aParams); document.getElementById("txtRequest").value = oSerializer.serializeToString(oSoapCall.envelope); setUIEnabled(false); oSoapCall.asyncInvoke( function (oResponse, oCall, iError) { var oResult = handleResponse(oResponse, oCall, iError); showSoapResults(oResult); } ); }
The asyncInvoke() method accepts only one argument: a callback function (handleResponse()). This function will be passed three arguments by the SOAP call: a SOAPResponse object, a pointer to the original SOAP call (to track multiple instances, if necessary), and an error code. These are all passed to the handleResponse function for processing, when the call returns:
function handleResponse(oResponse, oCall, iError) { setUIEnabled(true); if (iError != 0) { alert("Unrecognized error."); return false; } else { var oSerializer = new XMLSerializer(); document.getElementById("txtResponse").value = oSerializer.serializeToString(oResponse.envelope); var oFault = oResponse.fault; if (oFault != null) { var sName = oFault.faultCode; var sSummary = oFault.faultString; alert("An error occurred:\n" + sSummary + "\n" + sName + "\nSee message pane for SOAP fault"); return false; } else { return oResponse; } } }
If the error code is not zero, an error has occurred that can't be explained. This happens only rarely; in most cases an error will be returned through the fault property of the response object.
Another built-in class is used now, XMLSerializer. This takes an XML node and can convert it to a string or a stream. In this case a string is retrieved and displayed in the right-hand text area.
If oResponse.fault is not null, a SOAP fault occurred, so an error message is built and displayed to the user and no further action taken. Following a successful call, the response object is passed out as the function's return value and processed by the showSoapResults() function:
function showSoapResults(oResult) { if (!oResult) return; document.getElementById("txtResult").value = oResult.body.firstChild.firstChild.firstChild.data; }
After checking that oResult is valid, the value of the <methodResult> element is extracted using the DOM.
Note |
There is a method of the SoapResponse named getParameters, which, in theory, can be used to retrieve the parameters in a more elegant manner. It does not seem to work as advertised with document style calls, however, necessitating the need to examine the structure of the soap:Body using more primitive methods. |
The only way to consume web services on almost all modern browsers is by using XMLHttp. Because Internet Explorer, Firefox, Opera, and Safari all have some basic support for XMLHttp, this is your best bet for cross-browser consistency. Unfortunately, this type of consistency comes at a price — you are responsible for building up the SOAP request by hand and posting it to the server. You are also responsible for parsing the result and watching for errors.
The two protagonists in this scenario are the XmlHttp ActiveX class from Microsoft and the XmlHttpRequest class that comes with the more modern Mozilla-based browsers listed above. They both have similar methods and properties, although in the time-honored fashion of these things Microsoft was first on the scene before standards had been agreed on, so the later released Mozilla version is more W3C compliant. The basic foundation of these classes is to allow an HTTP request to be made to a web address. The target does not have to return XML — virtually any content can be retrieved — and the ability to post data is also included. For SOAP calls, the normal method is to send a POST request with the raw <soap:Envelope> as the payload.
In this example, you'll once again be calling the test harness. This time, however, you'll be using the zXML library to create XMLHttp objects and constructing the complete SOAP call on your own. This library uses a number of techniques to examine which XML classes are supported by the browser. It also wraps these classes and adds extra methods and properties so that they can be used in a virtually identical manner. The generation of the SOAP request string is handled in a function called getRequest():
function getRequest(sMethod, sOp1, sOp2) { var sRequest = "<soap:Envelope xmlns:xsi=\"" + "http://www.w3.org/2001/XMLSchema-instance\" " + "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " + "xmlns:soap=\" http://schemas.xmlsoap.org/soap/envelope/\">\n" + "<soap:Body>\n" + "<" + sMethod + "xmlns=\"" + SOAP_ACTION_BASE + "\">\n" + "<op1>" + sOp1 + "</op1>\n" + "<op2>" + sOp2 + "</op2>\n" + "</" + sMethod + ">\n" + "</soap:Body>\n" + "</soap:Envelope>\n"; return sRequest; }
The getRequest() function is pretty straightforward; it simply constructs the SOAP string in the appropriate format. (The appropriate format can be seen using the .NET test harness described in the creation of the Math service.) The completed SOAP string is returned by getRequest() and is used by performSpecificOperation() to build the SOAP request:
function performSpecificOperation(sMethod, sOp1, sOp2) { oXmlHttp = zXmlHttp.createRequest(); setUIEnabled(false); var sRequest = getRequest(sMethod, sOp1, sOp2); var sSoapActionHeader = SOAP_ACTION_BASE + "/" + sMethod; oXmlHttp.open("POST", SERVICE_URL, true); oXmlHttp.onreadystatechange = handleResponse; oXmlHttp.setRequestHeader("Content-Type", "text/xml"); oXmlHttp.setRequestHeader("SOAPAction", sSoapActionHeader); oXmlHttp.send(sRequest); document.getElementById("txtRequest").value = sRequest; }
First, a call is made on the zXmlHttp library to create an XMLHttp request. As stated previously, this will be an instance of an ActiveX class if you are using Internet Explorer or of XmlHttpRequest if you are using a Mozilla-based browser. The open method of the object received attempts to initialize the request. The first parameter states that this request will be a POST request containing data, and then comes the URL of the service, followed by a Boolean parameter specifying whether this request will be asynchronous or whether the code should wait for a response after making the call.
The onreadystatechange property of the request specifies which function will be called when the state of the request alters.
The performSpecificOperation() function then adds two headers to the HTML request. The first specifies the content type to be text/xml, and the second adds the SOAPAction header. This value can be read from the .NET test harness page or can be seen in the WSDL file as the soapAction attribute of the relevant <soap:operation/> element. Once the request is sent, the raw XML is displayed in the left text box. When the processing state of the request changes, the handleResponse() function will be called:
function handleResponse() { if (oXmlHttp.readyState == 4) { setUIEnabled(true); var oResponseOutput = document.getElementById("txtResponse"); var oResultOutput = document.getElementById("txtResult"); var oXmlResponse = oXmlHttp.responseXML; var sHeaders = oXmlHttp.getAllResponseHeaders(); if (oXmlHttp.status != 200 || !oXmlResponse.xml) { alert("Error accessing Web service.\n" + oXmlHttp.statusText + "\nSee response pane for further details."); var sResponse = (oXmlResponse.xml ? oXmlResponse.xml : oXmlResponseText); oResponseOutput.value = sHeaders + sResponse; return; } oResponseOutput.value = sHeaders + oXmlResponse.xml; var sResult = oXmlResponse.documentElement.firstChild.firstChild.firstChild.firstChild.data; oResultOutput.value = sResult; } }
The handleResponse() function reacts to any change of state in the request. When the readyState property equals 4, which equates to complete, there will be no more processing and it can be checked to see if there is a valid result.
If the oXmlHttp.status does not equal 200 or the responseXML property is empty, an error has occurred and a message is displayed to the user. Should the error be a SOAP fault, that information is also displayed in the message pane. If, however, the error wasn't a SOAP fault, the responseText is displayed. If the call has succeeded, the raw XML is displayed in the right text box.
Assuming that the XML response is available, there are a number of ways to extract the actual result including XSLT, using the DOM or text parsing; unfortunately, very few of these work across browsers in a consistent manner. The DOM method of gradually stepping through the tree is not very elegant, but it does have the merit of being applicable to whichever variety of XML document is in use.