Labels

Tuesday, June 21, 2011

Web service for Dynamics NAV (server-side)

Today I would like to discuss with you how to implement a Web service for Dynamics NAV and to interact with this service from Dynamics NAV C/SIDE Client or even web server.

My target was to build a NAV database (because employees are used to work with it's interface) with online access. In this online database additional information about customers (such as bonus balance) should be stored. Information about customer bonus balance should be available not only in NAV working database, but in internet, too. Further more, when posting a discount in NAV operational database, customer bonus balance should be reduced.

From the system point of view, the Navision Application Server, connected to bonus database, should be started. Certain TCP ports should be opened. When the message come to this port some event should be triggered, and a response should be posted back. The language for this interaction should be XML.

Codeunit 1, Trigger 99 NASHandler(ATASID : Text[260])
GenSetupL.GET;
GenSetupL.TESTFIELD("NAS Application Port");
IF ISCLEAR(CC) THEN CREATE(CC);
IF ISCLEAR(SBA) THEN CREATE(SBA);
CC.AddBusAdapter(SBA, 0);
SBA.OpenSocket(GenSetupL."NAS Application Port", '');
MESSAGE(StartedNASTxt, GenSetupL."NAS Application Port");

where
CC Automation 'Navision Communication Component version 2'.CommunicationComponent
SBA Automation 'Navision Socket Bus Adapter'.SocketBusAdapter
are the global variables. After adding CC variables the new trigger was created automaticly:
CC::MessageReceived(VAR InMessage : Automation "''.IDISPATCH")
Then the new trigger for proceeding incoming request was created and the next code was added:



HandleRequest()
XMLDocInstruct := XMLOutG.createProcessingInstruction('xml','version="1.0" encoding="UTF-8" standalone="no"');
XMLOutG.insertBefore(XMLDocInstruct,XMLOutG.childNodes.item(0));
DocResponseRoot := XMLOutG.createElement('Response');
XMLOutG.documentElement := DocResponseRoot;
DocRoot:=XMLInG.selectSingleNode('//Request');
IF ISCLEAR(DocRoot) THEN BEGIN
  AddSubNodeText(DocResponseRoot, XMLNode, 'Error', MethodNotProvided);
  EXIT;
END;
Method := GetNamedAttributeText(DocRoot,'method');
CASE Method OF
'GetBalance':
  BEGIN
    XMLNode := DocRoot.selectSingleNode('CardID');
    IF ISCLEAR(XMLNode) THEN BEGIN
      AddSubNodeText(DocResponseRoot, XMLNode, 'Error', ParametersNotProvided);
      EXIT;
    END;
    CalculateCardBalance(CardL.ID, TotalBalanceL);
    AddSubNodeText(DocResponseRoot, XMLNode, 'TotalBalance', FORMAT(TotalBalanceL,0,9));
  END;

// ... other method proceeding goes here
END;

A start point for receiving incoming message is here: 

CC::MessageReceived(VAR InMessage : Automation "''.IDISPATCH")
CLEAR(XMLOutG);
IF ISCLEAR(XMLInG) THEN
  CREATE(XMLInG);
IF ISCLEAR(XMLOutG) THEN
  CREATE(XMLOutG);

XMLOutG.async := FALSE;
ComIn := InMessage;
InStrm := ComIn.GetStream;
Found := FALSE;
REPEAT
  IF InStrm.READTEXT(Txt) = 0 THEN Found := TRUE;
UNTIL InStrm.EOS OR Found;

XMLInG.load(InStrm);

ComOut := ComIn.CreateReply();

OutStrm := ComOut.GetStream();
HandleRequest();

OutStrm.WRITETEXT('HTTP/1.1 200 OK');

OutStrm.WRITETEXT();
OutStrm.WRITETEXT('Content-Type: text/xml; charset=utf-8');
OutStrm.WRITETEXT();

TempFile := ENVIRON('TEMP') + '\bonus_out.xml';
XMLOutG.save(TempFile);
F.OPEN(TempFile);
OutStrm.WRITETEXT('Content-Length: ' + FORMAT(F.LEN));
F.CLOSE;
OutStrm.WRITETEXT();
OutStrm.WRITETEXT();
XMLOutG.save(OutStrm);
ComOut.Send(0);
ComIn.CommitMessage;


Some comments.

  1. The incoming stream is used to read message from the 0-character, because there are some headers at the top of it. 
  2. As we could see when sending HTTP response we should indicate the length of the response. That's why we should save the XML response to the temporary file, open it as a stream and read the length of it.
Now, we are ready to implement some modification in the client database. 

No comments: