Since my previous blog entry was about how
to build an IBF compliant Web service for Axapta, it would not have made sense if I did not do the same for Navision. So that's exactly what this entry all about: Creating an IBF compliant Web service for Navision.
Just as for Axapta I started with any
prior knowledge about or experience with Navision. So the first thing I did was
using MSN Search to find some reading on the topic of creating Web services for Navision. This resulted in the lecture
of the two following MSDN articles:
Just as the other day with Axapta, these two articles basically provided me with enough information to get the job done. I say 'basically' since this time I need to
confess that it took me more than just 2 hours. The most important reason for this was that apart from having to learn about working with the C/Side IDE and mastering the basics about the programming language C/AL, I encountered a lot of problems to get the basic infrastructure right.
Getting the Navision Application Server and the Navision Database Server
configured right turned out to be a big challenge for me.
For my first posting on this topic I've
chosen to use the 'native' Navision Database Server instead of Microsoft SQL Server. And even while I'm using Navision 4.0 I didn't leverage the new XML
Ports to compile the XML strings involved with this solution. This means however that the C/AL source files here below can also be used for previous versions of Navision.
For the implementation of the Web service I've chosen to make
it as similar as possible as to the one I created for Axapta. One could say that both implementations are based on a common model. It's however not really perfect,
but you'll see what I mean.
So you'll have again 4 classes in C#:
- Customer
- Customers
- CustomerReferenceById
- CustomersReferenceByCompanyName
Here you've the Customer class:
[XmlRoot("Customer", Namespace="Navision2IBF")]
public class Customer
{
private string _accNum;
private string _name;
private string _street;
private string _state;
private string _city;
private string _zip;
private string _country;
[XmlElement]
public string AccountNumber {get{return _accNum;} set{_accNum = value;}}
[XmlElement]
public string Name {get{return _name;} set{_name = value;}}
[XmlElement]
public string Street {get{return _street;} set{_street = value;}}
[XmlElement]
public string State {get{return _state;} set{_state = value;}}
[XmlElement]
public string City {get{return _city;} set{_city = value;}}
[XmlElement]
public string Zip {get{return _zip;} set{_zip = value;}}
[XmlElement]
public string Country {get{return _country;} set{_country = value;}}
}
Here you've the Customers class:
public class Customers : CollectionBase
{
// Get Customer as a specific index
public Customer this[int index]
{
get {return (Customer) List[index];}
set {List[index] = value;}
}
public int Add(Customer item)
{
return List.Add(item);
}
public int IndexOf(Customer item)
{
return List.IndexOf(item);
}
public void Insert(int index, Customer item)
{
List.Insert(index, item);
}
public void Remove(Customer item)
{
List.Remove(item);
}
public bool Contains(Customer item)
{
return List.Contains(item);
}
public void CopyTo(Customer[] destination, int index)
{
List.CopyTo(destination, index);
}
}
Here you've the CustomerReferenceById class:
[XmlRoot("CustomerReferenceById", Namespace="Navision2IBF")]
public class CustomerReferenceById
{
private string _accNum;
[XmlAttribute]
public string AccountNumber {get{return _accNum;} set{_accNum = value;}}
}
And finally below the CustomersReferenceByCompanyName class:
[XmlRoot("CustomersReferenceByCompanyName", Namespace="Navision2IBF")]
public class CustomerReferenceByCompanyName
{
[XmlAttribute]
public string companyName;
}
As was the case in the Axapta Web service I defined an interface that is implemented by the Web service:
public interface ICustomerInformationAccess
{
Customer GetCustomerById(CustomerReferenceById customerId);
Customers GetCustomersByName(CustomerReferenceByCompanyName customerName);
}
The actual implementation for this interface is where things get interesting:
public class NavisionWebService : System.Web.Services.WebService, ICustomerInformationAccess
Interesting because the implementation is completely different from what we had to do for Axapta. The way you integrate with Navision is based on interacting with an instance of the Navision Application Server (NAS) via Microsoft Message Queuing (MSMQ) messages. Sending and receiving these MSMQ messaging is pretty straight forward in Visual C# - at least if you've some experience with using Visual Studio .NET to create MSMQ applications.
For sending MSMQ messages I declared following variable as part of the NavisionWebService class:
private System.Messaging.MessageQueue mqToNavision;
And for receiving MSMQ messages:
private System.Messaging.MessageQueue mqFromNavision;
Both are initialized as part of the InitializeComponent() method:
private void InitializeComponent()
{
this.mqToNavision = new System.Messaging.MessageQueue();
this.mqFromNavision = new System.Messaging.MessageQueue();
this.mqToNavision.Path = "FormatName:DIRECT=OS:contoso1\\private$\\tonavision";
this.mqFromNavision.Path = "FormatName:DIRECT=OS:contoso1\\private$\\fromnavision";
}
Here you've the implementation for GetCustomerById:
[WebMethod]
public Customer GetCustomerById(CustomerReferenceById accNum)
{
Customer cust = null;
string request = "GetCustomerById(" + accNum.AccountNumber + ")";
mqToNavision.Send(request, "Navision MSMQ-BA");
mqFromNavision.Formatter = new System.Messaging.XmlMessageFormatter(new Type[] {typeof(Customer)});
try
{
System.Messaging.Message msg = mqFromNavision.Receive(new System.TimeSpan(0, 0, 0, 30));
cust = (Customer) msg.Body;
}
catch(Exception x)
{
System.Diagnostics.Debug.WriteLine(x.Message);
}
return cust;
}
The implementation for GetCustomersByName is similar:
public Customers GetCustomersByName(CustomerReferenceByCompanyName name)
{
Customers customers = new Customers();
string request = "GetCustomersByName(" + name.companyName+ ")";
mqToNavision.Send(request, "Navision MSMQ-BA");
mqFromNavision.Formatter = new System.Messaging.XmlMessageFormatter(new Type[] {typeof(Customers)});
try
{
System.Messaging.Message msg = mqFromNavision.Receive(new System.TimeSpan(0, 0, 0, 30));
customers = (Customers) msg.Body;
}
catch(Exception x)
{
System.Diagnostics.Debug.WriteLine(x.Message);
}
return customers;
}
The hard part for a "Microsoft-classic"-guy like me was
writing the C/AL code that is responsible for sending and receiving the MSMQ messages at the other side. Luckily the MSDN articles mentioned above are written so that you don't really need much prior knowledge about Navision to understand the basics.
It comes down to creating 2 new C/AL codeunits and slightly modifying the standard codeunit ApplicationManagement.
These new codeunits are:
- Navision2IBF
- Navision2IBFBiz
The first one - Navision2IBF - is where
the event handler MessageReceived() is implemented that is responsible for reading and writing to and from MSMQ. From within this event handler the second codeunit - Navision2IBF - is called for doing the actual business logic and compiling
the XML data to be returned via an MSMQ message. The standard codeunit ApplicationManagement is responsible for making sure that the event handler MessageReceived() as implemented in Navision2IBF gets instantiated. This is done with a slight modification in the function NASHandler() that parses NAS start-up parameters:
NASHandler(NASID : Text[260])
...
IF CGNASStartedinLoop = FALSE THEN
CASE Parameter OF
...
'NAVISION2IBF':
CODEUNIT.RUN(CODEUNIT::Navision2IBF);
...
The implementation for the Navision2IBF looks like this:
OnRun()
CLEARALL;
IF ISCLEAR(MQBus) THEN
CREATE(MQBus);
IF ISCLEAR(CC2) THEN
CREATE(CC2);
IF ISCLEAR(XMLDom) THEN
CREATE(XMLDom);
CC2.AddBusAdapter(MQBus,1);
MQBus.OpenReceiveQueue('.\private$\toNavision', 0, 0);
ParseRequest(string : Text[250])
Request := COPYSTR(string, 1, STRPOS(string, '(') - 1);
auxstring := COPYSTR(string, STRPOS(string, '(') + 1, STRLEN(string) - STRPOS(string, '(') - 1);
argpos := 1;
commapos := STRPOS(auxstring, ',');
WHILE (commapos <> 0) DO
BEGIN
Parameters[argpos] := COPYSTR(auxstring, 1, commapos - 1);
auxstring := COPYSTR(auxstring, STRPOS(auxstring, ',') + 1);
argpos := argpos + 1;
commapos := STRPOS(auxstring, ',');
END;
Parameters[argpos] := auxstring;
parcount := argpos;
CC2::MessageReceived(VAR InMessage : Automation "''.IDISPATCH")
// Get the message
InMsg := InMessage;
InS := InMsg.GetStream();
// Load the message into an XML document and find a node
XMLDom.load(InS);
XMLNode := XMLDom.selectSingleNode('string');
// Parse the request and according to the request variable, redirect to the appropriate function
ParseRequest(XMLNode.text);
CASE Request OF
'GetCustomerById':
Navision2IBFBiz.GetCustomerById(Parameters[1], XMLDom);
'GetCustomersByName':
Navision2IBFBiz.GetCustomersByName(Parameters[1], XMLDom);
END;
// Open the response queue and create a new message
MQBus.OpenWriteQueue('.\private$\fromNavision', 0, 0);
OutMsg := CC2.CreateoutMessage('Message queue://.\private$\fromNavision');
XMLDom.save(OutMsg.GetStream());
OutMsg.Send(0);
In stead of explaining all the details about the code I invite you to read the articles mentioned above since my code
is for 80% a straight copy of what you'll find there.
And finally here you've the code for
the other codeunit - Navision2IBFBiz:
GetCustomerById(CustomerID :
Code[30];VAR XMLDom : Automation "'Microsoft XML, v3.0'.DOMDocument")
IF ISCLEAR(XMLDom) THEN
CREATE(XMLDom);
XMLDom.loadXML('<?xml version="1.0"?><Customer></Customer>');
XMLRoot := XMLDom.documentElement;
CustomerRecord.SETFILTER("No.", CustomerID);
IF CustomerRecord.FIND('-') THEN BEGIN
AddAttribute(XMLRoot, 'xmlns:xsd', 'http://www.w3.org/2001/XMLSchema');
AddAttribute(XMLRoot, 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
AddAttribute(XMLRoot, 'xmlns',
'Navision2IBF');
AddElement(XMLRoot, 'AccountNumber',
XMLNode, CustomerRecord."No.");
AddElement(XMLRoot, 'Name', XMLNode,
CustomerRecord.Name);
AddElement(XMLRoot, 'Street', XMLNode,
CustomerRecord.Address);
AddElement(XMLRoot, 'State', XMLNode,
'');
AddElement(XMLRoot, 'City', XMLNode,
CustomerRecord.City);
AddElement(XMLRoot, 'Zip', XMLNode,
CustomerRecord."Post Code");
AddElement(XMLRoot, 'Country',
XMLNode, CustomerRecord."Country Code");
END;
GetCustomersByName(Name :
Text[250];VAR XMLDom : Automation "'Microsoft XML, v3.0'.DOMDocument")
IF ISCLEAR(XMLDom) THEN
CREATE(XMLDom);
XMLDom.loadXML('<?xml version="1.0"?><ArrayOfCustomer></ArrayOfCustomer>');
XMLRoot := XMLDom.documentElement;
CustomerRecord.SETFILTER(Name, Name + '*');
IF CustomerRecord.FIND('-') THEN BEGIN
AddAttribute(XMLRoot, 'xmlns:xsd', 'http://www.w3.org/2001/XMLSchema');
AddAttribute(XMLRoot, 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
REPEAT
XMLCustomerNode := XMLRoot.ownerDocument.createElement('Customer');
XMLRoot.appendChild(XMLCustomerNode);
AddElement(XMLCustomerNode, 'AccountNumber', XMLNode, CustomerRecord."No.");
AddElement(XMLCustomerNode, 'Name', XMLNode, CustomerRecord.Name);
AddElement(XMLCustomerNode, 'Street', XMLNode, CustomerRecord.Address);
AddElement(XMLCustomerNode, 'State', XMLNode, '');
AddElement(XMLCustomerNode, 'City', XMLNode, CustomerRecord.City);
AddElement(XMLCustomerNode, 'Zip', XMLNode, CustomerRecord."Post Code");
AddElement(XMLCustomerNode, 'Country', XMLNode, CustomerRecord."Country Code");
UNTIL (CustomerRecord.NEXT = 0);
END;
AddElement(...)
CreatedXMLNode
: Automation ...
IF ISCLEAR(XMLNode) THEN
MESSAGE('XMLNode is NULL');
NewElement :=
XMLNode.ownerDocument.createNode('element', NodeName, 'Navision2IBF');
NewElement.text := NodeValue;
XMLNode.appendChild(NewElement);
CreatedXMLNode := NewElement;
AddAttribute(VAR XMLNode :
Automation "'Microsoft XML, v3.0'.IXMLDOMNode";Name : Text[260];NodeValue :
Text[260])
IF NodeValue <> '' THEN BEGIN
XMLNewAttributeNode :=
XMLNode.ownerDocument.createAttribute(Name);
XMLNewAttributeNode.nodeValue :=
NodeValue;
XMLNode.attributes.setNamedItem(XMLNewAttributeNode);
END;
I plan to complete this posting later with some more details to get you started without having to solve all the problems I encountered.
However right now I need to run...