Connecting to NAV Web Services from Microsoft Dynamics NAV 2009 SP1


Please read this post to get a brief explanation of the scenario I will implement in Microsoft Dynamics NAV 2009 SP1. Please also read this post in order to understand how Web Services works using pure XML and no fancy objects.

Like Javascript, NAV 2009 SP1 does not natively have support for consuming Web Services. It does however have support for both Client and Server side COM automation and XmlHttp (which is compatible with XmlHttpRequest which we used in the Javascript sample here) is available in Microsoft XML.

Client or Serverside

When running XmlHttp under the RoleTailored Client we have to determine whether we want to run XmlHttp serverside or clientside. My immediate selection was Serverside (why in earth do this Clientside) until I tried it out and found that my server was not allowed to impersonate me against a web services which again needs to impersonate my against the database.

The double hub issue becomes a triple hub issue and now it suddenly becomes clear that XmlHttp in this sample of course needs to run clientside:-)

Compatibility

The sample below will run both in the RoleTailored Client and in the Classic Client.

InvokeNavWS

As in the Javascript example, I will create a function called InvokeNavWS – and in this function I will do the actual Web Service method invocation. In Javascript we setup an event to be called when the send method was done and as you might know, this is not doable on the Roletailored Client.

Fortunately, we are using synchronous web services, meaning that it is actually not necessary to setup this event. We can just check the status when send returns.

xmlhttp.send allows you to send either a string or a XML Document. Having in mind that a string in NAV Classic is max. 1024 characters, I decided to go with a XML Document. In the RoleTailored Client I could have used BigText, but that doesn’t work in Classic.

Creating a XML Document will take slightly more time than building up a large string, but it is the safest way to go. Start by adding an Envelope, a body, a method and then transfer the parameter nodes one by one (there might be smarter ways to do this:-)

The return value is always a nodeList and we only look at the responseXML property of the xmlhttp (which is an XML document).

The Code for InvokeNavWS looks like this:

InvokeNavWS(URL : Text[250];method : Text[20];nameSpace : Text[80];returnTag : Text[20];parameters : Text[1024];VAR nodeList : Automation "’Microsoft XML, v6.0′.IXMLDOMNodeList") result : Boolean
result := FALSE;
// Create XML Document
CREATE(xmldoc,TRUE,TRUE);
// Create SOAP Envelope
soapEnvelope := xmldoc.createElement(‘Soap:Envelope’);
soapEnvelope.setAttribute(‘xmlns:Soap’, ‘
http://schemas.xmlsoap.org/soap/envelope/’);
xmldoc.appendChild(soapEnvelope);
// Create SOAP Body
soapBody := xmldoc.createElement(‘Soap:Body’);
soapEnvelope.appendChild(soapBody);
// Create Method Element
soapMethod := xmldoc.createElement(method);
soapMethod.setAttribute(‘xmlns’, nameSpace);
soapBody.appendChild(soapMethod);
// Transfer parameters by loading them into a XML Document and move them
CREATE(parametersXmlDoc,TRUE,TRUE);
parametersXmlDoc.loadXML(‘<parameters>’+parameters+'</parameters>’);
IF parametersXmlDoc.firstChild.hasChildNodes THEN
BEGIN
  WHILE parametersXmlDoc.firstChild.childNodes.length>0 DO
  BEGIN
    node := parametersXmlDoc.firstChild.firstChild;
    node := parametersXmlDoc.firstChild.removeChild(node);
    soapMethod.appendChild(node);
  END;
END;
// Create XMLHTTP and SEND
CREATE(xmlhttp, TRUE, TRUE);
xmlhttp.open(‘POST’, URL, FALSE);
xmlhttp.setRequestHeader(‘Content-type’, ‘text/xml; charset=utf-8’);
xmlhttp.setRequestHeader(‘SOAPAction’, method);
xmlhttp.send(xmldoc);
// If status is OK – Get Result XML
IF xmlhttp.status=200 THEN
BEGIN
  xmldoc := xmlhttp.responseXML;
  xmldoc.setProperty(‘SelectionLanguage’,’XPath’);
  xmldoc.setProperty(‘SelectionNamespaces’,’xmlns:tns="’+nameSpace+’"’);
  nodeList := xmldoc.selectNodes(‘//tns:’+returnTag);
  result := TRUE;
END;

and the local variables for InvokeNavWS are

Name              DataType      Subtype                                 Length
xmlhttp           Automation    ‘Microsoft XML, v6.0’.XMLHTTP   
xmldoc            Automation    ‘Microsoft XML, v6.0’.DOMDocument   
soapEnvelope      Automation    ‘Microsoft XML, v6.0’.IXMLDOMElement   
soapBody          Automation    ‘Microsoft XML, v6.0’.IXMLDOMElement   
soapMethod        Automation    ‘Microsoft XML, v6.0’.IXMLDOMElement   
node              Automation    ‘Microsoft XML, v6.0’.IXMLDOMNode   
parametersXmlDoc  Automation    ‘Microsoft XML, v6.0’.DOMDocument   

As in the Javascript sample I have create a couple of “high” level functions for easier access:

SystemService_Companies(VAR nodeList : Automation "’Microsoft XML, v6.0′.IXMLDOMNodeList") result : Boolean
result := InvokeNavWS(systemServiceURL, ‘Companies’, SystemServiceNS, ‘return_value’, ”, nodeList);

CustomerPage_Read(No : Text[20];VAR nodeList : Automation "’Microsoft XML, v6.0′.IXMLDOMNodeList") result : Boolean
result := InvokeNavWS(customerPageURL, ‘Read’, CustomerServiceNS, ‘Customer’, ‘<No>’+No+'</No>’, nodeList);

CustomerPage_ReadMultiple(filters : Text[1024];VAR nodeList : Automation "’Microsoft XML, v6.0′.IXMLDOMNodeList") result : Boolean
result := InvokeNavWS(customerPageURL, ‘ReadMultiple’, CustomerServiceNS, ‘Customer’, filters, nodeList);

The “main” program

OnRun()
baseURL := ‘
http://localhost:7047/DynamicsNAV/WS/’;
systemServiceURL := baseURL + ‘SystemService’;
SoapEnvelopeNS := ‘
http://schemas.xmlsoap.org/soap/envelope/’;
SystemServiceNS := ‘urn:microsoft-dynamics-schemas/nav/system/’;
CustomerServiceNS := ‘urn:microsoft-dynamics-schemas/page/customer’;

CLEAR(nodeList);
IF SystemService_Companies(nodeList) THEN
BEGIN
  DISPLAY(‘Companies:’);
  FOR i:=1 TO nodeList.length DO
  BEGIN
    node := nodeList.item(i-1);
    DISPLAY(node.text);
    IF i=1 THEN cur := node.text;
  END;

  customerPageURL := baseURL + EncodeURIComponent(cur) + ‘/Page/Customer’;
  DISPLAY(‘URL of Customer Page: ‘+ customerPageURL);

  IF CustomerPage_Read(‘10000’, nodeList) THEN
  BEGIN
    DISPLAY(‘Name of Customer 10000: ‘ + nodeList.item(0).childNodes.item(2).firstChild.text);
  END;

  IF CustomerPage_ReadMultiple(‘<filter><Field>Country_Region_Code</Field><Criteria>GB</Criteria></filter>’+
‘<filter><Field>Location_Code</Field><Criteria>RED|BLUE</Criteria></filter>’, nodeList) THEN
  BEGIN
    DISPLAY(‘Customers in GB served by RED or BLUE warehouse:’);
    FOR i:=1 TO nodeList.length DO
    BEGIN
      node := nodeList.item(i-1);
      DISPLAY(node.childNodes.item(2).firstChild.text);
    END;
  END;

  DISPLAY(‘THE END’);

END;

with the following local variables:

Name       DataType      Subtype                                 Length
nodeList   Automation    ‘Microsoft XML, v6.0’.IXMLDOMNodeList   
node       Automation    ‘Microsoft XML, v6.0’.IXMLDOMNode   
i          Integer       

As it was the case in the Javascript sample I am using simple xml nodelist code to navigate and display various values. baseURL, cur, SystemServiceURL etc. are all global Text variables used as constants.

DISPLAY points to a function that just does a IF CONFIRM(s) THEN ; to display where we are and running this on the RoleTailored Client will display the following Confirm Dialogs:

image image image

image

image

image

image image image image

image

Note that the URL of the Customer Page is different from all the other examples. This is because NAV doesn’t have a way of Encoding an URL, so I have to do the company name encoding myself and when I encode a company name, I just encode all characters, that works perfectly:

EncodeURIComponent(uri : Text[80]) encodedUri : Text[240]
// No URI Encoding in NAV – we do it ourself…
HexDigits := ‘0123456789ABCDEF’;
encodedUri := ”;
FOR i:=1 TO STRLEN(uri) DO
BEGIN
  b := uri[i];
  encodedUri := encodedUri + ‘%  ‘;
  encodedUri[STRLEN(encodedUri)-1] := HexDigits[(b DIV 16)+1];
  encodedUri[STRLEN(encodedUri)] := HexDigits[(b MOD 16)+1];
END;

(Again, there might be smarter ways to do this – I just haven’t found it).

I hope this is helpful.

Good luck

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Comments (9)

  1. lyot says:

    I see you’re using the std page functions to read multiple records.

    How do I approach this if I want to update one or multiple records?

  2. freddyk says:

    You could have to write a small function using the InvokeNAVWS with the right parameters.

    This is just an example – not a solution.

    You will need to understand the details of this post to use it anyway.

  3. Luis says:

    What about invoking non-NAV web services (external ones) from MS Dynamics NAV 2009 SP1? We'd have to get the XML url of the external web service and read its nodes.

  4. Gottfried Mayer says:

    This example is great and I succeeded to write a codeunit in NAV to call a webservice.

    Although when using the NAV2009 Application Server to call the functions (with the same user that I succeeded to run the code in NAV Classic Client), I always get a HTTP Error 400 back.

    I suspect a authentication related issue. Do you have any experience with calling Web Services from NAV2009 Application Server?

    Any help would be appreciated

     Gottfried

  5. Gottfried Mayer says:

    Working perfectly now also with the Application Server, it was a classic PEBKAC problem 🙁

    Thanks for this post, it helped me a lot!

  6. Sergey says:

    Hi, Freddy

    I managed to connect to the web service and call the Read method. However, I cannot call the Update or Delete method.

    I have two companies. I'm connecting to Company1 and call the Read method for a customer, then I'm connecting to Company2 and I'm calling the Update method for the same customer no. So I'm calling the InvokeNAVWS function with the same nodeList variable as a parameter but with Update instead of Read. But it's not working, so I believe I haven't understood the way it should work.

    Can you at least guide me in the right direction?

    Thanks!

  7. Heveen says:

    I would like to know how to add several parameter when a create method is call..

  8. Giuseppe Maino says:

    When I use the read_multiple method calling the standard page 38 Item Ledger Entries, in the body of the xml received I have not always the same number of nodes because not all the fields are filled. I didn't find a property or something else to get always the same number of nodes. There is a way to get always the same number of nodes?

    Thanks!

  9. Giuseppe Maino says:

    I tried to obtain the same result by publishing a codeunit with an xmlport as a parameter, but I don't understand how to pass xmlport parameter from the calling codeunit.

    I receive in the response the error "Parameter esporta in method ExportCustomer in service ExportCustomer is null!" where esporta is the name of the variable of type xmlport in the codeunit published.

    Thanks