Ping your web service for security

Increasingly we are using Web Services to communicate between NAV companies.  In a production environment these communication is usually secured by the SSL standard. When the communication is sent over the Internet I would recommend that a secure ping be added before every communication to make sure that the correct machines are speaking together.

The payment service module in NAV 2013 R2 has an encryption functionality.  I am using this functionality every time that I store user names and passwords in the database. This encryption functionality is based on the key located in the Online Service add-in folder.

KeyLocation

You can copy the Providerkey.key file from one server to another or use the Payment Service Connection Setup page to upload, download, delete and to create a new key.

EncryptionSetup

Create a function and add to you web service.

EncryptedPingWebService

 

The encrypted string is then decrypted and compared with the original string sent.

EncryptedPing

The EncryptionMgt refers to Codeunit 824.  A possibility to add the secret as a parameter to every web service call and skip the preceding ping function.  The code examples can be downloaded from Objects4NAV.

 

OData in NAV 2013 R2

Open Data Protocol (OData) is a data access protocol initially defined by Microsoft.  In NAV 2013 Microsoft first added a support for OData.  On the Developer Network Microsoft has a few walk-throughs aimed to get us started in using both SOAP and OData web services.

To get started with OData first make sure that the OData service is enabled on your developement server.

ODataEnabled

As you can see on the image above I also enable the use of NTLM Authentication.  In a production environment OData should use SSL as described in this walk-through from Microsoft.

I want to share with you a few points that I have found out and are not obvious in the walk-throughs.

When an external application makes a OData request NAV will behave the same way as a Windows Client would do.  The server will do the login routine by executing trigger 1 in Codeunit 1, CompanyOpen and when the request is finishing the server executes the CompanyClose trigger.  If the request if for a Page object then all the page triggers are executed, OnInit, OnOpenPage and so on.

The OData will only show the fields added to the Page or to the Query with the table primary key fields added.  Lets take a closer look at how the Page object works with OData.

In the OnOpenPage trigger and in the SourceTableView property it is possible to add filters.  These filters will apply to the OData stream and you will not be able to insert data into NAV outside of these filters – same functionality as if you where using the Page in the Windows Client.  The properties; Editable, InsertAllowed, ModifyAllowed and DeleteAllowed all work with OData.  Lets for example look at Page 39 – General Journal.  In the OnOpenPage trigger the code is filtering the table data with a Template Name and a Batch Name.

Page39

From OData, the variable OpenedFromBatch will always be False so the first template and batch for the PAGE::”General Journal” will always be selected and new entries will always be inserted into that journal.  This also means that it is not possible to use Page 39 to read the journal lines from any other journal then the first one.  Fields that are not visible in a Page are visible and usable in OData.

When creating a new record the code in the OnNewRecord trigger is executed.

OnNewRecord

This will all work fine for the first template and batch.  The AutoSplitKey property is also active so as long as you are fine with inserting in to the default journal then you can use this Page for your OData web service.

The easiest way is still to create a new page dedicated to the web service functionality, show the primary key fields in the page and skip the OnOpenPage and the OnNewRecord code.  I use the OnNewRecord code to put in default values for the table I am inserting into.

On the Microsoft web site walk-through it is shown how to create a new customer, look for a customer and modify a customer.

I have found that I want to add one line to that example

NAV nav = new NAV(new Uri("http://localhost:7048/DynamicsNAV/OData/Company('CRONUS%20International%20Ltd.')"));
nav.Credentials = CredentialCache.DefaultNetworkCredentials; 
nav.IgnoreResourceNotFoundException = true;

Without IgnoreResourceNotFoundException the following code example will return an exception if the customer is not found within the given filter.

private static Customer GetCustomer(NAV nav, string customerNo)
{
var customers = (from c in nav.Customer
where c.No == customerNo
select c);
foreach (Customer customer in customers)
return customer;
return null;
}

By combining a Get function like this with a New or Modify function it is easy to update the existing value for any given table in the database.

private static Boolean AddNewDefaultDimensionCodeValue(NAV nav, int tableNo, string no, string dimensionCode, string dimensionCodeValue)
{
DefaultDimensions existingDefaultDimension = GetDefaultDimension(nav, tableNo, no, dimensionCode);
if (existingDefaultDimension == null)
{
DefaultDimensions newDefaultDimension = new DefaultDimensions();
newDefaultDimension.Table_ID = tableNo;
newDefaultDimension.No = no;
newDefaultDimension.Dimension_Code = dimensionCode;
newDefaultDimension.Dimension_Value_Code = dimensionCodeValue;
nav.AddToDefaultDimensions(newDefaultDimension);
nav.SaveChanges();
return true;
}
else
{
existingDefaultDimension.Dimension_Value_Code = dimensionCodeValue;
nav.UpdateObject(existingDefaultDimension);
nav.SaveChanges();
return false;
}
}
private static DefaultDimensions GetDefaultDimension(NAV nav, int tableNo, string no, string dimensionCode)
{
var dimensionValues = (from d in nav.DefaultDimensions
where d.Table_ID == tableNo && d.No == no && d.Dimension_Code == dimensionCode
select d);
foreach (DefaultDimensions dimensionValue in dimensionValues)
return dimensionValue;
return null;
}

Remember, that without the SaveChanges nothing will be updated in NAV.

Now go ahead and use OData to integrate NAV with all your external systems and devices.  Good luck.

Please add block possibility to Permission table

Yesterday I suggested to Microsoft an enhancement to the permission functionality.  That was an informal suggestion so I logged into Microsoft Connect and added a formal suggestion.

In table 2000000005 Permission we can assign permission to objects.  In the Classic Client we had the possibility to assign permissions to objects with the type System.  This is not working in the NAV 2013 (R2) client.

This causes a problem, for example a user with SUPER (Data) permission can delete a company from the database.

I suggest that a new option be added to fields 6, 7, 8, 9, 10 in the above table.
Current option string is ” ,Yes,Indirect”
The new option string would be ” ,Yes,Indirect,Blocked”

If an access type is blocked in any permission entry the access will be blocked even if there is access in another permission set.

I would for example add a line to the Permission table for the SUPER (Data) permission set that will block Insert, Modify and Delete for table 2000000006 Company.

Please help me by voting for the suggestion.

Where is my NAS Company ?

I have now gone through my first upgrade from NAV 2013 to NAV 2013 R2.

  • Stopped and uninstalled NAV 2013 Server
  • Opened NAV 2013 database with NAV 2013 Developement Environment and upgraded the database
  • Compiled all system tables (2000000004-2000000130)
  • Installed and started NAV 2013 R2 Server
  • Compiled all objects
  • Imported Upgrade700701.IS.1.fob from the NAV 2013 R2 DVD – where IS is the localized version
  • Started Page 104001 in every company and executed “Transfer Data”
  • Started Page 104001 and executed “Delete Objects”
  • Imported NAV 2013 R2 fob file with the application
  • Restarted NAV 2013 R2 Server
  • Imported Upgrade700701.IS.2.fob from the NAV 2013 R2 DVD – where IS is the localized version
  • Started Page 104002 in every company and executed “Transfer Data”
  • Started Page 104001 and executed “Delete Unused Tables” and “Delete Upgrade Toolkit”

There where a few things I would have liked to go better.

  • I needed to upload the developer license to the database to be able to upgrade.  In an environment where multiple databases share the same license this could be a problem.
  • Codeunit 104004 only deletes the upgrade data for the current company as if the upgrade process is not designed to run for a database with more than one company.  Tables 104003 and 104037 are not emptied.  There is no way to execute this Codeunit for all companies since it is deleted in the first execution.  Therefore I needed to manually empty the upgrade tables and delete them.
  • Another problem with Codeunit 104004 is that is does not delete the Queries 104001 – 104004.  I needed to manually delete them after the upgrade process.

I sure would like Microsoft to take a look at these issues and find a solution.

Then I wanted to start the NAS Service.  In the Server Administration I did not see the NAS Company setting.  I looked at the documentation for Configuring NAS Service and found

Setting Description
NAS Company Specifies the Microsoft Dynamics NAV company that opens when NAS services start.

So I asked my MVP’s for the answer and Arend-Jan Kauffmann answered.

NAVAdministration

The PowerShell parameter changes from NASCompany to ServiceDefaultCompany.

In the documentation it states that

It is recommended that you create a separate Microsoft Dynamics NAV Server instance for each NAS services application. See How to: Create a Microsoft Dynamics NAV Server Instance.

There are multiple reasons for running NAS services sessions in dedicated Microsoft Dynamics NAV Server instances:

  • Efficiency and convenience            
     When you change any Microsoft Dynamics NAV Server setting, you must restart the instance for the change to take effect, which interrupts all services using that instance. So if you are running different types of services in the same instance—for example, RoleTailored client services and NAS services—making a change to the settings for either service type will require a server instance restart that interrupts all other service types running through that instance.
    Even for different types of NAS services applications it’s wise to run each application in a separate server instance. For example, if you will be using NAS services for a Microsoft Office Outlook Integration application and also for a Microsoft Dynamics NAV job queue application, create a separate Microsoft Dynamics NAV Server instance for each NAS services application. This way, if you need to modify settings for the Microsoft Office Outlook Integration application you will not affect the Microsoft Dynamics NAV job queue application, and vice-versa.
  • Performance            
    Configuring NAS services applications to use separate server instances makes better use of the server computer’s resources, allowing you to run more applications with less degradation.
  • Efficient error tracking            
    If a NAS services session terminates in an error, and there are no other services running on the Microsoft Dynamics NAV Server instance, the service instance terminates and can be handled like any other Windows service. For example, you could configure the Recovery tab on the Service configuration tool in Control Panel to restart or otherwise manage the service.

The same then applies to OData Services I guess – create a dedicated server instance for OData.

I used the following PowerShell script to create a new NAS Server Instance

[code]$DatabaseServerName = ‘localhost’
$DatabaseName = ‘NAV2013_COMPANY’
$ServiceInstanceName = ‘NAV2013_COMPANY’
$CompanyName = ‘Company Name’
$MgtPort = 7089
$ClientPort = $MgtPort + 1
$SoapPort = $ClientPort + 1
$ODataPort = $SoapPort + 1

Get-Credential | New-NAVServerInstance -ServerInstance $ServiceInstanceName -ServiceAccount User -ClientServicesCredentialType Windows -ClientServicesPort $ClientPort -DatabaseName $DatabaseName -DatabaseServer $DatabaseServerName -ManagementServicesPort $MgtPort -SOAPServicesPort $SoapPort -ODataServicesPort $ODataPort
Set-NAVServerInstance -ServerInstance $ServiceInstanceName -Start
Set-NAVServerConfiguration -ServerInstance $ServiceInstanceName -KeyName ‘NASServicesStartupCodeunit’ -KeyValue ‘450’
Set-NAVServerConfiguration -ServerInstance $ServiceInstanceName -KeyName ‘NASServicesStartupMethod’ -KeyValue ”
Set-NAVServerConfiguration -ServerInstance $ServiceInstanceName -KeyName ‘NASServicesStartupArgument’ -KeyValue ‘JOBQUEUE’
Set-NAVServerConfiguration -ServerInstance $ServiceInstanceName -KeyName ‘ServicesDefaultCompany’ -KeyValue $CompanyName
Set-NAVServerConfiguration -ServerInstance $ServiceInstanceName -KeyName ‘NASServicesEnableDebugging’ -KeyValue $True
Set-NAVServerInstance -ServerInstance $ServiceInstanceName -Restart[/code]

Now I can start to use NAV 2013 R2.

Reading the NAV 2013 Service Configuration

In a previous blog I showed how to get the database name and the database server name for NAV 2009.  I used Automation object to read the service configuration file and pull from there the required information.

This method is obsolete for NAV 2013 but here is the required code to pick similar and more information from the service configuration file.

[code]OBJECT Codeunit 50000 Read Service Config
{
OBJECT-PROPERTIES
{
Date=18.06.13;
Time=22:51:31;
Modified=Yes;
Version List=Dynamics.is;
}
PROPERTIES
{
SingleInstance=Yes;
OnRun=BEGIN
END;

}
CODE
{

PROCEDURE FindMiddleTierServicePath@1000000002() ServicePath : Text[1024];
VAR
ActiveSession@1000000002 : Record 2000000110;
ServerFile@1000000001 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.File";
XMLDoc@1000000000 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlDocument";
XMLNode@1000000003 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlNode";
BEGIN
ActiveSession.SETRANGE("Session ID",SESSIONID);
ActiveSession.FINDFIRST;

XMLDoc := XMLDoc.XmlDocument;
IF ServerFile.Exists(APPLICATIONPATH + ‘Instances\’ + ActiveSession."Server Instance Name" + ‘\CustomSettings.config’) THEN
XMLDoc.Load(APPLICATIONPATH + ‘Instances\’ + ActiveSession."Server Instance Name" + ‘\CustomSettings.config’)
ELSE
XMLDoc.Load(APPLICATIONPATH + ‘CustomSettings.config’);

ServicePath := ‘DynamicsNAV://’ + ActiveSession."Server Computer Name" + ‘:’;

XMLNode := XMLDoc.SelectSingleNode(‘//appSettings/add[@key=”ClientServicesPort”]’);
ServicePath := ServicePath + XMLNode.Attributes.Item(1).InnerText + ‘/’ + ActiveSession."Server Instance Name";
CLEAR(XMLDoc);
END;

PROCEDURE FindSOAPWebServicePath@10010403() ServicePath : Text[1024];
VAR
ActiveSession@1000000002 : Record 2000000110;
ServerFile@1000000001 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.File";
XMLDoc@1000000000 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlDocument";
XMLNode@1000000003 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlNode";
httpUtility@1000000004 : DotNet "’System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’.System.Web.HttpUtility";
BEGIN
ActiveSession.SETRANGE("Session ID",SESSIONID);
ActiveSession.FINDFIRST;

httpUtility := httpUtility.HttpUtility;
XMLDoc := XMLDoc.XmlDocument;
IF ServerFile.Exists(APPLICATIONPATH + ‘Instances\’ + ActiveSession."Server Instance Name" + ‘\CustomSettings.config’) THEN
XMLDoc.Load(APPLICATIONPATH + ‘Instances\’ + ActiveSession."Server Instance Name" + ‘\CustomSettings.config’)
ELSE
XMLDoc.Load(APPLICATIONPATH + ‘CustomSettings.config’);

XMLNode := XMLDoc.SelectSingleNode(‘//appSettings/add[@key=”SOAPServicesSSLEnabled”]’);
IF UPPERCASE(XMLNode.Attributes.Item(1).InnerText) = ‘FALSE’ THEN
ServicePath := ‘http://’
ELSE
ServicePath := ‘https://’;

ServicePath := ServicePath + ActiveSession."Server Computer Name" + ‘:’;

XMLNode := XMLDoc.SelectSingleNode(‘//appSettings/add[@key=”SOAPServicesPort”]’);
ServicePath := ServicePath + XMLNode.Attributes.Item(1).InnerText + ‘/’ + ActiveSession."Server Instance Name" + ‘/WS/’;
ServicePath := ServicePath + httpUtility.UrlPathEncode(COMPANYNAME) + ‘/Services’;

CLEAR(XMLDoc);
END;

PROCEDURE FindODataWebServicePath@10010405() ServicePath : Text[1024];
VAR
ActiveSession@1000000002 : Record 2000000110;
ServerFile@1000000001 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.File";
XMLDoc@1000000000 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlDocument";
XMLNode@1000000003 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlNode";
BEGIN
ActiveSession.SETRANGE("Session ID",SESSIONID);
ActiveSession.FINDFIRST;

XMLDoc := XMLDoc.XmlDocument;
IF ServerFile.Exists(APPLICATIONPATH + ‘Instances\’ + ActiveSession."Server Instance Name" + ‘\CustomSettings.config’) THEN
XMLDoc.Load(APPLICATIONPATH + ‘Instances\’ + ActiveSession."Server Instance Name" + ‘\CustomSettings.config’)
ELSE
XMLDoc.Load(APPLICATIONPATH + ‘CustomSettings.config’);

ServicePath := ‘http://’ + ActiveSession."Server Computer Name" + ‘:’;

XMLNode := XMLDoc.SelectSingleNode(‘//appSettings/add[@key=”ODataServicesPort”]’);
ServicePath := ServicePath + XMLNode.Attributes.Item(1).InnerText + ‘/’ + ActiveSession."Server Instance Name" + ‘/OData/’;

CLEAR(XMLDoc);
END;

PROCEDURE FindDatabaseServerName@1000000000() DatabaseServerName : Text[1024];
VAR
ActiveSession@1000000003 : Record 2000000110;
ServerFile@1000000002 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.File";
XMLDoc@1000000001 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlDocument";
XMLNode@1000000000 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlNode";
DatabaseServer@1000000004 : Text[1024];
DatabaseInstance@1000000005 : Text[1023];
BEGIN
ActiveSession.SETRANGE("Session ID",SESSIONID);
ActiveSession.FINDFIRST;

XMLDoc := XMLDoc.XmlDocument;
IF ServerFile.Exists(APPLICATIONPATH + ‘Instances\’ + ActiveSession."Server Instance Name" + ‘\CustomSettings.config’) THEN
XMLDoc.Load(APPLICATIONPATH + ‘Instances\’ + ActiveSession."Server Instance Name" + ‘\CustomSettings.config’)
ELSE
XMLDoc.Load(APPLICATIONPATH + ‘CustomSettings.config’);

XMLNode := XMLDoc.SelectSingleNode(‘//appSettings/add[@key=”DatabaseServer”]’);
DatabaseServer := XMLNode.Attributes.Item(1).InnerText;

XMLNode := XMLDoc.SelectSingleNode(‘//appSettings/add[@key=”DatabaseInstance”]’);
DatabaseInstance := XMLNode.Attributes.Item(1).InnerText;

CLEAR(XMLDoc);

IF DatabaseInstance = ” THEN
DatabaseServerName := DatabaseServer
ELSE
DatabaseServerName := DatabaseServer + ‘\’ + DatabaseInstance;
END;

BEGIN
END.
}
}

[/code]

The four functions here will give you the path to start the Windows Client, the path to the Soap Web Service, the path to OData Web Service and finally the database server name including instance name if used.

The Codeunit is available here: ReadServiceConfig

Minimize C/AL Code for Online Communication

These days I keep busy upgrading Advania solutions to Dynamics NAV 2013.  All the older solutions that have communicated with web services have DOM objects and manually created XML files.  We had to add and remove namespace to be able to use XML ports but in the end this worked flawlessly.

In NAV 2013 it is possible to use DOM objects on the client-side but my mission is to execute as much as possible on the server-side and use dotnet interoperability in all cases.  That left me with two choices; rewrite all the DOM C/AL Code and use dotnet or build a class library and minimize the code as much as possible.

I tried both and for me the choice is simple.  I will go with the class library method.

If I have the WSDL (Web Services Description Language) as a file or supplied by the web service then I will get both the commands and the data types needed for the communication.  The C/AL Code needed can be as little as four lines.

[code]
Greidslur := Greidslur.GreidslurWS; // Construct the SoapHTTPClientProtocol
Greidslur.Url := XMLURLPaymentsUnsigned; // Set the URL to the service
SecurityHelper.SetUserName(Greidslur,WebServiceUserID,WebServicePassword); // Add authentication if needed
DoesExist := Greidslur.ErReikningurTil(BankNo,LedgerNo,AccountNo,OwnerID); // Carry out the communication[/code]

The identical C/AL Code required to do the same query with DOM is a lot longer

[code]DocumentReady := CREATE(DOMDocument);
IF NOT DocumentReady THEN
ERROR(Text003);

IXMLDOMProcessingInstruction := DOMDocument.createProcessingInstruction(‘xml’,’version="1.0" encoding="utf-8"’);
DOMDocument.appendChild(IXMLDOMProcessingInstruction);
IXMLDOMElement := DOMDocument.createElement(‘soap:Envelope’);
IXMLDOMElement.setAttribute(‘xmlns:soap’,’http://schemas.xmlsoap.org/soap/envelope/’);
IXMLDOMElement.setAttribute(‘xmlns:xsi’,’http://www.w3.org/2001/XMLSchema-instance’);
IXMLDOMElement.setAttribute(‘xmlns:xsd’,’http://www.w3.org/2001/XMLSchema’);
CreateHeader(IXMLDOMElement2); // Creates the authentication
IXMLDOMElement3 := DOMDocument.createElement(‘soap:Body’);
IXMLDOMElement4 := DOMDocument.createElement(‘ErReikningurTil’);
IXMLDOMElement4.setAttribute(‘xmlns’,’http://ws.isb.is’);
IXMLDOMElement5 := DOMDocument.createElement(‘banki’);
IXMLDOMElement5.nodeTypedValue(BankNo);
IXMLDOMElement4.appendChild(IXMLDOMElement5);
IXMLDOMElement5 := DOMDocument.createElement(‘hofudbok’);
IXMLDOMElement5.nodeTypedValue(LedgerNo);
IXMLDOMElement4.appendChild(IXMLDOMElement5);
IXMLDOMElement5 := DOMDocument.createElement(‘reikningsnumer’);
IXMLDOMElement5.nodeTypedValue(AccountNo);
IXMLDOMElement4.appendChild(IXMLDOMElement5);
IXMLDOMElement5 := DOMDocument.createElement(‘kennitala’);
IXMLDOMElement5.nodeTypedValue(OwnerID);
IXMLDOMElement4.appendChild(IXMLDOMElement5);
IXMLDOMElement3.appendChild(IXMLDOMElement4);
IXMLDOMElement.appendChild(IXMLDOMElement2);
IXMLDOMElement.appendChild(IXMLDOMElement3);
DOMDocument.appendChild(IXMLDOMElement);

XMLHttp.open( ‘POST’, XMLURLPaymentsUnsigned, FALSE);
XMLHttp.setRequestHeader(‘soapAction’,’http://ws.isb.is/ErReikningurTil’);
XMLHttp.send(DOMDocument);
DOMResponseDocument := XMLHttp.responseXML;

IXMLDOMNode := DOMResponseDocument.selectSingleNode(‘/soap:Envelope/soap:Body’);
IXMLDOMNodeList2 := IXMLDOMNode.childNodes;
FOR j := 1 TO IXMLDOMNodeList2.length DO BEGIN
IXMLDOMNode2 := IXMLDOMNodeList2.nextNode;
CASE IXMLDOMNode2.nodeName OF
‘soap:Fault’:
BEGIN
IXMLDOMNodeList3 := IXMLDOMNode2.childNodes;
FOR k := 1 TO IXMLDOMNodeList3.length DO BEGIN
IXMLDOMNode4 := IXMLDOMNodeList3.nextNode;
CASE IXMLDOMNode4.nodeName OF
‘faultcode’:
BEGIN
END;
‘faultstring’:
BEGIN
ERROR(DecodeText(IXMLDOMNode4.text));
END;
‘faultfactor’:
BEGIN
END;
END;
END;
END;
‘ErReikningurTilResponse’:
BEGIN
IXMLDOMNodeList3 := IXMLDOMNode2.childNodes;
IXMLDOMNode3 := IXMLDOMNodeList3.nextNode;
CASE IXMLDOMNode3.nodeName OF
‘ErReikningurTilResult’:
BEGIN
IF UPPERCASE(IXMLDOMNode3.text) = ‘TRUE’ THEN
DoesExist := TRUE;
END;
END;
END;
END;
END;

CLEAR(DOMDocument);
CLEAR(DOMResponseDocument);[/code]

This example should show you not only that the class library method is simpler but also the potential error in creating the XML is no longer available.

But, there is a but.  There are web services that do not have WSDL.  Just a simple ‘POST’ or ‘GET’ services that requires incoming XML and respond with a XML.  For all the XML files needed there should be a XML Schema Definition or what is normally knows as a XSD file.  If that is the case then there is also a dotnet solution for that.

The first step is to collect all the XSD files needed into a single folder on your local computer and start the Visual Studio Command Prompt.

VisualStudioCommandPrompt

Go to the folder with the XSD files.

VisualStudioCommandPromptStarted

Then you use the command “xsd CollectorEntity.xsd /classes” and repeat for all xsd files.  You should now have C# class file for all XSD files.

ClassesCreated

Next step is to build a class library in the same way that I showed before and add the class files to the solution.

VisualStudioAddFiles

Then build the solution and copy the DLL file to your server Add-ins folder and to your developement environment Add-ins folder and you are good to go.  After adding the new class as a dotnet variable to C/AL I am able to handle the data types from the XSD file the same way as if they were created by a WSDL.

[code]Login := Login.Login;
Login.user := WebServiceUserID;
Login.password := WebServicePassword;
Login.version := 1.1;

Login_answer := Login_answer.Login_answer;
Log."Outgoing Message".CREATEOUTSTREAM(OutStr);
PrepareSend(Login,OutStr);
Log.MODIFY;
COMMIT;

Log."Incoming Message".CREATEOUTSTREAM(OutStr);
Send(OutStr);
Log.MODIFY;
COMMIT;

IF Receive(Login_answer,Login_answer.GetType) THEN
SessionID := Login_answer.sessionid;[/code]

The functions, PrepareSend, Send and Receive will work for all data types

[code]OBJECT Codeunit 50000 Dotnet Send and Receive
{
OBJECT-PROPERTIES
{
Date=09.05.13;
Time=12:06:27;
Modified=Yes;
Version List=Dynamics.is;
}
PROPERTIES
{
OnRun=BEGIN
END;

}
CODE
{
VAR
HttpWebRequest@10010413 : DotNet "’System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Net.HttpWebRequest";
XMLResponseDoc@10010414 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlDocument";
ServiceURL@10000003 : Text[1024];
GotSetup@10000004 : Boolean;
Text003@10000005 : TextConst ‘ENU=Error: %1\%2;ISL=St”Ðuvilla %1, %2\\%3\%4′;

PROCEDURE GetSetup@3(NewServiceURL@10000000 : Text[1024]);
BEGIN
IF NOT GotSetup THEN BEGIN
ServiceURL := NewServiceURL;
END;
END;

PROCEDURE PrepareSend@10010407(VAR OutObj@1000000000 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Object";VAR RequestStream@10010401 : OutStream) : Boolean;
VAR
MemoryStream@1000000006 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.MemoryStream";
XmlTextWriter@1000000007 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlTextWriter";
XmlSerializer@1000000008 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.Serialization.XmlSerializer";
XMLRequestDoc@1000000013 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlDocument";
Encoding@1000000010 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089’.System.Text.Encoding";
InStr@10010400 : InStream;
BEGIN
Encoding := Encoding.UTF8;
MemoryStream := MemoryStream.MemoryStream;
XmlTextWriter := XmlTextWriter.XmlTextWriter(MemoryStream,Encoding);
XmlSerializer := XmlSerializer.XmlSerializer(OutObj.GetType);
XmlSerializer.Serialize(XmlTextWriter,OutObj);

XMLRequestDoc := XMLRequestDoc.XmlDocument;
XMLRequestDoc.PreserveWhitespace := TRUE;
MemoryStream.Position := 0;
XMLRequestDoc.Load(MemoryStream);
MemoryStream.Close;
XMLRequestDoc.Save(RequestStream);

HttpWebRequest := HttpWebRequest.Create(ServiceURL); // Live Server
HttpWebRequest.Method := ‘POST’;
HttpWebRequest.ContentType := ‘text/xml; charset=utf-8’;
HttpWebRequest.Accept := ‘text/xml’;
HttpWebRequest.UserAgent := ‘XMLClient 1.0′;
MemoryStream := HttpWebRequest.GetRequestStream;
XMLRequestDoc.Save(MemoryStream);
MemoryStream.Flush;
MemoryStream.Close;
END;

PROCEDURE Send@10010405(VAR ResponseStream@10010400 : OutStream) : Boolean;
VAR
HttpWebException@10010401 : DotNet "’System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Net.WebException";
HttpWebResponse@10010402 : DotNet "’System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Net.HttpWebResponse";
HttpStatusCode@1000000012 : DotNet "’System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Net.HttpStatusCode";
MemoryStream@1000000006 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.MemoryStream";
XMLResponseDoc@1000000009 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlDocument";
InStr@10010403 : InStream;
BEGIN
HttpWebResponse := HttpWebRequest.GetResponse;
IF HttpWebResponse.StatusCode.ToString <> HttpStatusCode.OK.ToString THEN
ERROR(Text003,HttpWebResponse.StatusCode.ToString,HttpWebResponse.StatusDescription);

MemoryStream := HttpWebResponse.GetResponseStream;
XMLResponseDoc := XMLResponseDoc.XmlDocument;
XMLResponseDoc.Load(MemoryStream);
MemoryStream.Flush;
MemoryStream.Close;
XMLResponseDoc.Save(ResponseStream);
END;

PROCEDURE Receive@10010406(VAR InObj@1000000001 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Object";typeResponse@1000000002 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Type") : Boolean;
VAR
XmlSerializer@1000000008 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.Serialization.XmlSerializer";
XmlNodeReader@1000000015 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlNodeReader";
BEGIN
XmlNodeReader := XmlNodeReader.XmlNodeReader(XMLResponseDoc.DocumentElement);
XmlSerializer := XmlSerializer.XmlSerializer(typeResponse);
InObj := XmlSerializer.Deserialize(XmlNodeReader);
EXIT(NOT ISNULL(InObj));
END;

EVENT XMLResponseDoc@10010414::NodeInserting@93(sender@10010401 : Variant;e@10010400 : DotNet "’System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlNodeChangedEventArgs");
BEGIN
END;

EVENT XMLResponseDoc@10010414::NodeInserted@94(sender@10010401 : Variant;e@10010400 : DotNet "’System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlNodeChangedEventArgs");
BEGIN
END;

EVENT XMLResponseDoc@10010414::NodeRemoving@95(sender@10010401 : Variant;e@10010400 : DotNet "’System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlNodeChangedEventArgs");
BEGIN
END;

EVENT XMLResponseDoc@10010414::NodeRemoved@96(sender@10010401 : Variant;e@10010400 : DotNet "’System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlNodeChangedEventArgs");
BEGIN
END;

EVENT XMLResponseDoc@10010414::NodeChanging@97(sender@10010401 : Variant;e@10010400 : DotNet "’System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.XmlNodeChangedEventArgs");
BEGIN
END;

EVENT XMLResponseDoc@10010414::NodeChanged@98(sender@10010401 : Variant;e@10010400 : DotNet "’System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089’.System.Xml.XmlNodeChangedEventArgs");
BEGIN
END;

BEGIN
END.
}
}

[/code]

If this i still causing you headaches just contact me and we will figure something out.

Twain Scanner for Dynamics NAV that works in remote desktop

A few days ago I published objects for NAV 2013 and details on how to use a twain scanner with the hardware hub.

I know that many happy Dynamics NAV users are still using older versions so I wanted to make this solution available for NAV 2009, both in the Role Tailored Client and in the Classic Client.

ClassicHubScanning

If you are running NAV 2009 you should be able to use the attached object to test your scanner.  It should be easy to change the code to work with older versions of Dynamics NAV.

Hub Scanning for NAV 2009

Hardware Hub IIS Service on Objects4NAV.com

Bar code reader and the Hardware Hub

We have some cases where the Dynamics NAV client is running in a remote desktop and the local machine has a bar code scanner connected.  One way is to have the bar code scanner configured as a keyboard.  This requires the focus on the correct input field in NAV to work.  The other way is to have the scanner configured to use a Serial Communication Port.

DevMgt

This allows me to listen to the Serial Port and handle the input with C/AL code.  To make sure that this will work where ever the client is running I choose to use the Hardware Hub.  Attached is the NAV objects required to test this functionality.  Start by installing the Serial Port Client.

HubSerialPortClient

Import the NAV object into your NAV 2013 and make sure the the HardwareHubProxy.dll is in the add-ins folder for the middle tier service.  The start Page 50095.  Press the assist button to get a new Serial Port GUID.

HubBarcodeDemo

Copy the Serial Port GUID into the Serial Port Client, choose the correct port settings and check the box to open the port.  You can test the scanning to make sure.  The last scanned bar code will appear in the Last Data text box.

HubSerialPortClientActive

In Dynamics NAV start the listener and test the scanning.

NAV objects are attached, Hub Barcode Demo.

Hardware Hub IIS Service on Objects4NAV.com

Scan document with NAV via the Hardware Hub

I have now created a document scanning solution that uses the Hardware Hub.  This means that you can place the scanner on any computer and the NAV Windows Client on any or the same computer.

All you need is the Hardware Hub Twain Client on the computer that is connected to the scanner.  The software is available for download and install in by selecting the link above.

Attached is a Page and a Codeunit for NAV 2013 for you to test the scanning.  Import the fob file into your NAV and run page 50093.  This will require the Hardware Hub Proxy installed on your server or your client add-ins folder.

HubScanning

Use the AssistEdit button to create a new Twain Scanner GUID.  The installed Hardware Hub Twain Client will minimize to you notification area.  Locate and double-click the notification icon to bring up the client settings and copy the GUID from the page to the client.

HubCLient

Then just minimize the client again to the notification area.  Close and reopen page 50093, select your scanner and scan.  The scanned document will be located on the server and you can download and open the document with the assist button.

In the attached ZIP file you will find the file HardwareHubProxy.dll that you will need to put in your server add-ins folder.  If you would like to test this on your local machine just change the RunOnClient property in Codeunit 50093 and put the HardwareHubProxy.dll in your Windows Client add-ins folder.

HubLocalSettings

Here is the required objects.  HubScanning
Updated HardwareHubProxy.dll

Hardware Hub IIS Service on Objects4NAV.com

Hardware Hub for Dynamics NAV

Well, of cource you can use this hub for other software but I created it for NAV.

Lets look at these issues

  • You can’t support the hardware directly within NAV
  • You like to have one method of communicating with hardware from NAV
  • Your hardware is on another machine
  • Your hardware is on the client machine and you are running NAV as a Remote Application
  • Your hardware is on the client machine and you are using remote desktop for your work

This is why I created the Hardware Hub Web Service.

HubDescription

This is an open Web Service that can be used to move data between NAV and a device interface.

To use the hardware hub you will need to create a GUID string.  This you can do with the C/AL command FORMAT(CREATEGUID).  This string is stored in a text(50) field in the database.  When you send a request you include this unique identifier and when you use the receive command to check for commands you use the same unique identifier.

Attached is the Hub Proxy Class to use with NAV.  In a production environment the Hub Proxy Class should be places in the service tier Add-ins folder.  I also attached a test page that you can use to test the hardware hub service.  The test page is running the proxy class on the client side so you will need to copy the Hub Proxy Class (HardwareHubProxy.dll) into the Role Tailored Client Add-ins folder.

WebServiceTest

TestHardwareHub

Hardware Hub IIS Service on Objects4NAV.com