2008-03-23

AJAX, Web Services, WSDL and SharePoint

You may at some stage have to work for a client who's very particular about security, what web parts can be used and even whether sharepoint designer can be used.

Recently I had the opportunity to work for one such client who wanted considerable customization, but without the use of SPD, creating a custom web part was out of the question, the sharepoint servers were on another continent, and I was given contribute permissions one a subsite. Like I've said many times before I hate telling somebody something can't be done.

The customizations involved cross site permissions checking, aggregating, site meta data displaying and performing actions on new/edit forms for particular content types. The only option I could think of was talking to sharepoint's web services with IE's XMLHttpRequest object.

Some people think that AJAX is some fancy new'ish library that enables you to do clientside post backs; well it is and it isn't... it's been around since the turn of the millennium and most of the AJAX tool kits/libraries have a lot fluff for browser compatibility, and if your not working in an environment where you need to support multiple browsers I wouldn't bother with any pre-packaged library/toolkit.


All you need is to instantiate an HttpRequest object, IE7 and FF have it built in so you can call methods without creating the ActiveXObject.



So how is it done?

1.- Assign a variable to the Request object

2.- Open a connection to the web service

3.- Post some data to the service



4.- Capture the reply




5.- Do some formatting







That simple, but there's more to it...

The web service is going to expect the post in some form, so most have a WSDL (Web Service Definition Language) you can query to build your post. SharePoints web services lay it all out for you to just cut & paste pretty much.

The web service is also going to send back the data in some form, usually XML. You can use javascript to interpret the XML string, but all browsers come with an XML parser. So you load the XML from the web service in to an XML DOM object, and now you can loop through, and extract the data. Look here for available methods: http://msdn2.microsoft.com/en-us/library/ms757828(VS.85).aspx

You're probably here because you want code. Here's fireWS.zip a web page you can run on your local machine that will query the WSDL, or you can put the source in a content query web part and run it from any page in SharePoint. Looking at its source should give you a good idea of how to query the WSDL, loop through nodes and construct the XML post.

Below the break down of the page and bear in mind that this is a demo and the code can be refactored... Originally it was a Javascript class, so all you would do was call new fireWS(constuc webservice), but I've split it out to individual functions and used global variables instead of properties.




Start off by creating a couple of global variables to share among the functions:

var sXmlDomVer='MSXML2.DOMDocument.3.0',oWsdlXml, oSoapXml, oSoapBody, oSoapOp;

The below instantiates a request object, and takes the web service url and the xml string to post. If you want to support other browsers add some checks here.

function fireWS(sWSUrl, sXml){
var oXmlHttp=false;
if(!oXmlHttp && typeof XMLHttpRequest!='undefined') {
try {oXmlHttp = new XMLHttpRequest();}
catch(e){oXmlHttp=false;}
}
if(!oXmlHttp && window.createRequest) {
try {oXmlHttp = window.createRequest();}
catch(e){oXmlHttp=false;}
}
if(!oXmlHttp){
try{oXmlHttp = window.createRequest();}
catch(e){oXmlHttp = new ActiveXObject('Microsoft.XMLHTTP');}
}
if(!oXmlHttp){alert('Sorry, you\'re browser doesn\'t support this control')}
if(!sXml){method='GET'}
else{method='POST'}
oXmlHttp.open(method, sWSUrl, false);
oXmlHttp.setRequestHeader('Content-Type','text/xml; charset=utf-8');
oXmlHttp.send(sXml);
document.getElementById('headers').innerText = oXmlHttp.statusText +' '+ oXmlHttp.getAllResponseHeaders();
if (oXmlHttp.readyState==4){
return oXmlHttp.responseText;
}
}


This function parses an XML string and returns an XMLDOM object. If you want to support other browsers add some checks here.

function xmlParse(str){
oXmlDom = new ActiveXObject(sXmlDomVer)
oXmlDom.async = false;
oXmlDom.loadXML(str);
if (oXmlDom.parseError.errorCode != 0){
var myErr = oXmlDom.parseError;
if(document.getElementById('debug').checked){alert('Error parsing xml ' + myErr.reason +'\n\n'+ str)}
}
return oXmlDom;
}


This function is used to search for a paraticular node.

function getMyNode(obj, nodeName, attribute, attributeVal){
try{objXml = obj.getElementsByTagName(nodeName);}
catch(e){}
if(objXml.length == 0){objXml = obj.getElementsByTagName(nodeName.substring(nodeName.indexOf(':')+1,nodeName.length));}
if(objXml.length == 0){return false}
if(attribute != null && objXml){
for(var i=0; i<objXml.length; i++){if(objXml[i].getAttribute(attribute) == attributeVal){return objXml[i];}}
return false;
}
return objXml;
}


This function is used to add a node to another node. Looks like I wanted to be able to define the position to add it in, then I changed my mind.

function addMyNode(oSoap, type, nodeName, parentNode, v, ns){
var newNode = oSoap.createNode(type, nodeName, ns);
try{
switch(type){
case 1:
if(v){newNode.text = v};
return parentNode.appendChild(newNode);
case 2:
if(v){newNode.text = v};
return parentNode.attributes.setNamedItem(newNode);
default:
return parentNode.appendChild(newNode);
}
}
catch(e){
if(document.getElementById('debug').checked){alert('Error creating node "'+ nodeName +'"\ntype '+ type +'\nv '+ v +'\n\n'+ e.description)}
}
}


This removes childnodes so you can run a new query.

function removeChildNodes(oXml){
for(var i=0;i<oXml.childNodes.length;i++){
oXml.removeChild(oXml.childNodes.item(i));
}
}


This is for adding table rows to the SOAP properties table.

function renderPropsTbl(sProp,oProp){
if(document.getElementById('tblConnProps')!=null){
var oTbl = document.getElementById('tblConnProps');
}
else{
var oTbl = document.createElement('table');
oTbl.id = 'tblConnProps';
document.getElementById('divConnProps').appendChild(oTbl);
}
var newRow = oTbl.insertRow(oTbl.rows.length);
newRow.id = newRow.uniqueID;
var newCell;
newCell = newRow.insertCell(0);
newCell.id = newCell.uniqueID;
newCell.innerText = sProp;
newCell = newRow.insertCell(1);
newCell.id = newCell.uniqueID;
newCell.appendChild(oProp);
}


This is for removing table rows from the SOAP properties table.

function removePropsTbl(iTR){
var tblConnProps = document.getElementById('tblConnProps');
for(var i=tblConnProps.rows.length-1;i>=iTR; i--){tblConnProps.deleteRow(i);}
}


This for finding the bindings in the WSDL, I should really figure out an intelligent way of finding out the namspac, but instead I just use wsdl.

function getWdslBinding(url){
var oProps = document.getElementById('divConnProps');
oProps.innerHTML = '';
oWsdlXml = xmlParse(fireWS(url +'?WSDL'));
var oWsdlBindings = getMyNode(oWsdlXml, 'wsdl:binding');
if(oWsdlBindings){
var oBindings = document.createElement('select');
oBindings.id = 'selBinding';
oBindings.attachEvent('onchange',getWdslOperations);
for(var i=0; i<oWsdlBindings.length; i++){
if(oWsdlBindings[i].attributes.length > 0){
var newopt = document.createElement('option');
newopt.value = oWsdlBindings[i].getAttribute('name');
newopt.text = oWsdlBindings[i].getAttribute('name');
oBindings.options.add(newopt);
}
}
renderPropsTbl('Binding Port:',oBindings);
getWdslOperations();
}
}


This needs a bit of work but it finds the operations for the selected binding, and it sorts the operations for aestetics.

function getWdslOperations(){
try{removePropsTbl(1)}
catch(e){}
var oWsdlOps = getMyNode(oWsdlXml,'wsdl:binding','name',document.getElementById('selBinding').value);
if(getMyNode(oWsdlOps,'soap12')){sSoapVer = 'soap12';sSoapNS = 'http://www.w3.org/2003/05/soap-envelope';}
else{sSoapVer = 'soap';sSoapNS = 'http://schemas.xmlsoap.org/soap/envelope/';}
oSoapXml = xmlParse('<' + sSoapVer +':Envelope xmlns:'+ sSoapVer +'="'+ sSoapNS +'"/>');
oSoapBody = addMyNode(oSoapXml, 1, sSoapVer +':Body', oSoapXml.documentElement, '', sSoapNS);
oWsdlOps = getMyNode(oWsdlOps,'wsdl:operation');
if(oWsdlOps){
var oOps = document.createElement('select');
oOps.id = 'selOps';
oOps.attachEvent('onchange',getWdslOpElements);
arrWsdlOps = new Array();
for(var i=0; i<oWsdlOps.length; i++){
if(oWsdlOps[i].attributes.length > 0){
arrWsdlOps.push(oWsdlOps[i].getAttribute('name'));
}
}
arrWsdlOps.sort();
for(var i=0; i<arrWsdlOps.length; i++){
var newopt = document.createElement('option');
newopt.value = arrWsdlOps[i]; newopt.text = arrWsdlOps[i];
oOps.options.add(newopt);
}
renderPropsTbl('Operation:',oOps);
getWdslOpElements();
}
}


This gets the elements for an operations and adds an input fields.

function getWdslOpElements(){
try{
removePropsTbl(2);
document.getElementById('result').innerText = '';
oSoapBody.removeChild(oSoapOp);
}
catch(e){}
var sOp = document.getElementById('selOps').value;
oWsdlOpElem = getMyNode(oWsdlXml,'s:element','name',sOp);
oSoapOp = addMyNode(oSoapXml, 1, sOp, oSoapBody, '', oWsdlXml.documentElement.getAttribute('targetNamespace'));
if(oWsdlOpElem.hasChildNodes()){
oWsdlOpParams = getMyNode(oWsdlOpElem,'s:element');
for(var i=0; i<oWsdlOpParams.length; i++){
if(oWsdlOpParams[i].attributes.length > 0){
var sOpParamType = oWsdlOpParams[i].getAttribute('type'), sOpParamName = oWsdlOpParams[i].getAttribute('name'), iInputType;
if(oWsdlOpParams[i].hasChildNodes()){iInputType = 1;}
else{
if(sOpParamType.indexOf('tns:')>-1){
var oTNSOpParams = getMyNode(oWsdlXml, 's:simpleType', 'name', sOpParamType.replace('tns:',''));
if(oTNSOpParams){
oTNSOpParams = getMyNode(oTNSOpParams, 's:enumeration');
iInputType = 2;
}
else{iInputType = 1;}
}
else{iInputType = 0;}
}
switch(iInputType){
case 1:
var oOpPN = document.createElement('textarea');
oOpPN.cols = 60;oOpPN.rows = 3;
sOpParamType = 'XML Schema';
break;
case 2:
var oOpPN = document.createElement('select');
for(var iT=0; iT<oTNSOpParams.length; iT++){
if(oTNSOpParams[iT].attributes.length > 0){
var newopt = document.createElement('option');
newopt.value = oTNSOpParams[iT].getAttribute('value');
newopt.text = oTNSOpParams[iT].getAttribute('value');
oOpPN.options.add(newopt);
}
}
break;
default:
var oOpPN = document.createElement('input');
oOpPN.size = 80;
}
oOpPN.id = 'soapp'+ sOpParamName;
renderPropsTbl(sOpParamName +' ('+ sOpParamType +'):',oOpPN);
var oOpParam = addMyNode(oSoapXml, 1, sOpParamName, oSoapOp, '',oWsdlXml.documentElement.getAttribute('targetNamespace'));
}
}
renderPropsTbl('',document.createElement('<INPUT TYPE="Button" NAME="buildSoap" VALUE="Fire Soap" onclick="buildSoap();" class="btn">'));
}
}


This loops through the form and builds the SOAP post.

function buildSoap(){
var soapFlds = document.getElementById('tblConnProps').getElementsByTagName('*');
for(var i=0;i<soapFlds.length;i++){
if(soapFlds[i].value != '' && soapFlds[i].id.indexOf('soapp')>-1){
if(soapFlds[i].type == 'textarea'){
try{
var oFldXml = xmlParse(soapFlds[i].value);
oFldXml.namespaceURI = oOpParam.namespaceURI;
var oOpParam = oSoapOp.selectSingleNode(soapFlds[i].id.replace('soapp',''));
removeChildNodes(oOpParam);
oOpParam.appendChild(oFldXml.documentElement);
}
catch(e){
try{
var oOpParam = oSoapOp.selectSingleNode(soapFlds[i].id.replace('soapp',''));
var oXmlDom = new ActiveXObject(sXmlDomVer)
oXmlDom.async = false;
oXmlDom.loadXML('<'+ soapFlds[i].id.replace('soapp','') +' xmlns="'+ oOpParam.namespaceURI +'">'+ soapFlds[i].value +'</'+ soapFlds[i].id.replace('soapp','') +'>');
oSoapOp.replaceChild(oXmlDom.documentElement, oOpParam);
}
catch(e){alert('Error in buildSoap(): '+ e.description);}
}
}
else{
try{
var oOpParam = oSoapOp.selectSingleNode(soapFlds[i].id.replace('soapp',''));
oOpParam.text = soapFlds[i].value;
}
catch(e){}
}
}
}
fireSoap(oSoapXml.xml,document.getElementById('result'))
}


The rest just renders the output.

function fireSoap(soap, oResDisp){
var sUrl = document.getElementById('url').value + document.getElementById('asmx').options[document.getElementById('asmx').selectedIndex].text;
var sRtnXml = fireWS(sUrl, soap);
document.getElementById('soap').innerText = oSoapXml.xml.replace(/></gi,'>\n<');;
//document.getElementById('soap').parentElement.style.display = 'block';
getMethodXml(sUrl);
if(sRtnXml.indexOf('z:row')>-1){
var row = xmlParse(sRtnXml).getElementsByTagName('z:row');
var sAtt = '<table border="1">';
if(row[0].attributes.length > 0){
sAtt += '<tr>';
for(var i=0; i < row[0].attributes.length; i++){sAtt += '<th>'+ row[0].attributes[i].name +'</th>';}
sAtt += '</tr>';
}
for(var i=0; i<row.length; i++){
if(row[i].attributes.length > 0){
sAtt += '<tr>';
for(var ii=0; ii < row[i].attributes.length; ii++){sAtt += '<td>'+ row[i].attributes[ii].value +'</td>';}
sAtt += '</tr>';
}
}
sAtt += '</table>';
oResDisp.innerHTML = sAtt;
}
oResDisp.innerText = sRtnXml.replace(/></gi,'>\n<');oResDisp.parentElement.style.display = 'block';
}


Once you start playing around with this you'll soon realize it's power and flexibility, and you could even save yourself the effort of registering more dll's on your server. The holly grail would be to create Http End Points for all your SQL data.

If "Web page security validation" is enable you will not be able to do any add/edit/delete actions, so you're limited to listing. This is also the case with serverside web service calls unless you explicitly "allowunsafeupdates".

This will only work if you download the html file (fireWS.zip) and run it locally with reduced permissions OR reduce your internet zone IE permissions and run it online fireWS.htm.

Usage Example:
  1. Enter service URL: http://tb.ohchr.org/
  2. Choose web service eg: /_vti_bin/Lists.asmx
  3. Operation: GetListCollection
  4. Fire Soap
  5. You should now see the reply...
Now you have the list GUIDs so you can do some more stuff.

Like:
  1. Operation: GetList
  2. listName: {17B611E5-4BF5-48C0-8926-E614A8126197}
  3. Fire Soap


Cool, we've got all the field names.

Let's try a list query...
  1. Operation: GetListItems
  2. listName: {17B611E5-4BF5-48C0-8926-E614A8126197}
  3. query: <Query><Where><Contains><FieldRef Name="ConventionCode" /><Value Type="Text">C</Value></Contains></Where></Query>
  4. viewFields: <ViewFields><FieldRef Name="ID" /><FieldRef Name="Title" /><FieldRef Name="ConventionCode" /><FieldRef Name="ConventionOrder" /><FieldRef Name="Convention" /><FieldRef Name="Author" /></ViewFields>
  5. rowLimit: 7
  6. Fire Soap

As you can see it's a easy... I haven't tried all the web services as this is just a curiosity for me.



Web services: http://msdn2.microsoft.com/en-us/library/ms445292.aspx