Welcome to MSDN Blogs Sign in | Join | Help

Creating an IBF Compliant Web Service for Navision

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...

Posted by yvesk | 15 Comments

Creating an IBF Compliant Web Service for Axapta

Yesterday I was requested to create an Information Bridge Framework 1.5 (IBF) compliant Web service that exposes customer information from Axapta.

 

Initially I thought that this wouldn't be easy. However it merely took me approximately 2 hours to develop a fully functional IBF solution for Axapta. I still find this pretty impressive given that I had no experience at all with programmatically interacting with Axapta.

 

The first and only thing I had to learn about was Axapta’s Business Connector. Searching the Web got me to following two useful MSDN articles:

I was surprised by the simplicity of the - yet powerful - programming model provided by the Axapta Business Connector.

 

These two articles gave me enough information to create the IBF solution for Axapta. Allow me to share my code with you.

 

As a first step I created four classes in .NET using C#:

  • Customer
  • Customers
  • CustomerReferenceById
  • CustomersReferenceByCompanyName

Here you’ve the Customer class:

 

[XmlRoot("Customer", Namespace="Axapta2IBF")]

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="Axapta2IBF")]

public class CustomerReferenceById

{

       private string _accNum;

 

       [XmlAttribute]

       public string AccountNumber {get{return _accNum;} set{_accNum = value;}}

}

 

And finally, here you’ve the CustomersReferenceByName class:

 

[XmlRoot("CustomersReferenceByCompanyName", Namespace="Axapta2IBF")]

public class CustomerReferenceByCompanyName

{

       [XmlAttribute]

       public string companyName;

}

 

The second – and last – step is the actual implementation of the Web service methods.

 

To make things clean I first created a class that defines the interface that is implemented by the Web service:

 

public interface ICustomerInformationAccess

{

       Customer GetCustomerById(CustomerReferenceById customerId);

       Customers GetCustomersByName(CustomerReferenceByCompanyName customerName);

}

 

The Web service will implement this interface:

 

public class AxaptaWebService : System.Web.Services.WebService, ICustomerInformationAccess {…}

 

The implementation of the first method looks like this:

 

[WebMethod]

public Customer GetCustomerById(CustomerReferenceById accNum)

{

       AxaptaCOMConnector.Axapta axapta = new AxaptaCOMConnector.AxaptaClass();

       AxaptaCOMConnector.IAxaptaObject axaptaObj;

 

       axapta.Logon("Admin", "", "", "");

       axaptaObj = axapta.CreateObject("WebServiceCusInfo",null,null, null,null,null,null);

       axapta.Refresh();

 

       Customer cust = new Customer();

 

       cust.AccountNumber = accNum.AccountNumber;

       cust.Name = (string) axaptaObj.Call("retCustName", accNum.AccountNumber, null,null,null,null,null);

       cust.Street = (string) axaptaObj.Call("retCustStreet", accNum.AccountNumber, null,null,null,null,null);

       cust.City = (string) axaptaObj.Call("retCustCity", accNum.AccountNumber, null,null,null,null,null);

       cust.State = (string) axaptaObj.Call("retCustState", accNum.AccountNumber, null,null,null,null,null);

       cust.Zip = (string) axaptaObj.Call("retCustZip", accNum.AccountNumber, null,null,null,null,null);

       cust.Country = (string) axaptaObj.Call("retCustCountry", accNum.AccountNumber, null,null,null,null,null);

 

       axapta.Logoff();

 

       return cust;

}

 

In order to make this work you need to implement the X++ code as described in the first MSDN article mentioned above.

 

The implementation for the second method looks like this:

 

[WebMethod]

[return:XmlArray(ElementName="Customers", Namespace="Axapta2IBF")]

public Customers GetCustomersByName(CustomerReferenceByCompanyName name)

{

       Customers customers = new Customers();

       AxaptaCOMConnector.Axapta axapta = new AxaptaCOMConnector.AxaptaClass();

       axapta.Logon("Admin", "", "", "");

      

       AxaptaCOMConnector.IAxaptaObject axaptaQuery;

       axaptaQuery = axapta.CreateObject("Query", null, null, null, null, null, null);

                   

       int custTable = 77;

       AxaptaCOMConnector.IAxaptaObject axaptaDataSource;

       axaptaDataSource = (AxaptaCOMConnector.IAxaptaObject) axaptaQuery.Call("AddDataSource", custTable, null, null, null, null, null);

 

       AxaptaCOMConnector.IAxaptaObject axaptaQueryRun;

       axaptaQueryRun = axapta.CreateObject("QueryRun", axaptaQuery, null, null, null, null, null);

                   

       AxaptaCOMConnector.IAxaptaRecord axaptaRecord;

       while (axaptaQueryRun.Call("Next", null, null, null, null, null, null) != null)

       {

              axaptaRecord = (AxaptaCOMConnector.IAxaptaRecord) axaptaQueryRun.Call("GetNo", 1, null, null, null, null, null);

             if (axaptaRecord.Found == false) break;

             string custName = (string) axaptaRecord.get_field("Name");

             if (custName.ToLower().IndexOf(name.companyName.ToLower()) >= 0)

             {

                    Customer c = new Customer();

                    c.Name = custName;

                    c.AccountNumber = (string) axaptaRecord.get_field("AccountNum");

                    c.Street = (string) axaptaRecord.get_field("Street");

                    c.City = (string) axaptaRecord.get_field("City");

                    c.State = (string) axaptaRecord.get_field("State");

                    c.Zip = (string) axaptaRecord.get_field("ZipCode");

                    c.Country = (string) axaptaRecord.get_field("Country");

                    customers.Add(c);

             }           

       }

                   

       axapta.Logoff();

 

       return customers;

}

 

All the code above is pretty straight forward. To complete the IBF solution no more code is involved.

 

You only need to go through six IBF Wizards in Visual Studio .NET:

  1. Importing the Web service
  2. Creating a Customer entity and it’s default view
  3. Creating a Customers entity and it’s default view
  4. Creating a Region control library to display the Customer’s default view
  5. Creating a Reference List Region for the Customers’s entity default view
  6. Creating a Search page

In a next posting I’ll provide you with the actual screen shots of these 6 Wizards.

Posted by yvesk | 3 Comments
 
Page view tracker