C/AL and AL Side-by-Side Development with AdvaniaGIT

Microsoft supports Side-by-Side development for C/AL and AL.  To start using the Side-by-Side development make sure you have the latest version of AdvaniaGIT add-in for Visual Studio Code and update the PowerShell scripts by using the “Advania: Go!” command.

When the Business Central environment is built use the “Advania: Build C/AL Symbol References for AL” to enable the Side-by-Side development for this environment.  This function will reconfigure the service and execute the Generate Symbol References command for the environment.  From here on everything you change in C/AL on this environment will update the AL Symbol References.

So let’s try this out.

I converted my C/AL project to AL project with the steps described in my previous post.  Then selected to open Visual Studio Code in AL folder.

In my new Visual Studio Code window I selected to build an environment – the Docker Container.

When AdvaniaGIT builds a container it will install the AL Extension for Visual Studio Code from that Container.  We need to read the output of the environment build.  In this example I am asked to restart Visual Studio Code before reinstalling AL Language.  Note that if you are not asked to restart Visual Studio Code you don’t need to do that.

After restart I can see that the AL Language extension for Visual Studio Code is missing.

To fix this I execute the “Advania: Build NAV Environment” command again.  This time, since the Container is already running only the NAV license and the AL Extension will be updated.

Restart Visual Studio Code again and we are ready to go.

If we build new environment for our AL project we must update the environment settings in .vscode\launch.json.  This we can do with a built in AdvaniaGIT command.

We can verify the environment by executing “Advania: Check NAV Environment”.  Everything should be up and running at this time.

Since we will be using Side-by-Side development for C/AL and AL in this environment we need to enable that by executing “Advania: Build C/AL Symbol References for AL”.

This will take a few minutes to execute.

Don’t worry about the warning.  AdvaniaGIT takes care of restarting the service.  Let’s download AL Symbols and see what happens.

We can see that AL now recognizes the standard symbols but my custom one; “IS Soap Proxy Client Mgt.” is not recognized.  I will tell you more about this Codeunit in my next blog post.

I start FinSql to import the Codeunit “IS Soap Proxy Client Mgt.”

Import the FOB file

Close FinSql and execute the “AL: Download Symbols” again.  We can now see that AL recognizes my C/AL Codeunit.

Now I am good to go.

Presenting the Data Exchange Framework

Earlier this month Arend-Jan contacted me about being a presenter on Dutch Dynamics Community NAV Event for March 2016.  Of course I was honored and after a moments thought I accepted.

The following presentation was repeated two times for close to 140 people in total.  I had a great time and am thankful for the opportunity.

I promised to share one Codeunit – that general mapping Codeunit – and here it is: Codeunit 60000.

If you download the presentation you can also read my notes.  They should help you better understand the whole story.

 

Data Exchange Framework enhancements

I have made several enhancements to the Data Exchange Framework in NAV 2016.  Previously I wrote about text or a comma separated file import and about access to a local data file when creating a xml/json structure.  Both those enhancements are included in the objects attached to this post.

I have needed to handle JSON files where the actual data is not in the node value but in the node name.  To support this I added a boolean field to the Data Exchange Column Definition table (1223).

ConvertNodeNameToValue

To support this I added the same field to the Data Exchange Field Mapping table (1225) as a flow field, calculating the value from the definition table.

Codeunit 1203 is used to import XML and JSON files into the Data Exchange Field table.  I made two changes to the InsertColumn.  First, I added a parameter that accepts the node name and if the above switch is set to true I insert the node name into the column value instead of the node value.  The other change is that instead of finding only the first Data Exchange Column Definition I loop through all of them.  This allows me to create more than one definition based of the same XML node and therefore import into multiple columns from the same node.

To enable this scenario in the Currency Exchange Update Service I made changes to the Data Exchange Field Mapping Buffer table (1265) and the Data Exchange Setup Subform page.  More on that later.

A JSON file without a single root element will cause an error in the standard code.  Inspired my Nikola Kukrika on NAVTechDays 2015 I made changes to Codeunit 1237 utilizing the TryFunction to solve this problem.  To top this of I made a simple change to the Currency Exchange Rate Service Card page (1651) and table (1650) to allow JSON file type.

Having completed these changes I can now use a web service that delivers JSON in my Currency Exchange Rate Service.

CurrencyLayerJSON

Also inspired by Microsoft I added a transformation type to the Transformation Rule table (1237) to convert the Unix Timestamp (seconds since January 1st 1970 UTC) to a date.

UnixTimestamp

All objects and deltas are attached

DEF-Enhancements

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.

Using XML to transfer Data between NAV companies

In November 2010 I blogged about Transferring small amount of data between databases.  There I had a Form that can read and write a XML file with table data.  This Form was used frequently in my company to move data between companies and databases.

The good thing about this method is that I can move data between database versions.

XMLDataTransfer

Yesterday I got a request to share a NAV 2013 R2 version of this solution.  Here it is !

This version includes support for BLOB data in the tables.  BLOB data is converted to Base64 and included in the XML file.

Be careful when importing.  Existing data will be overwritten.

Dynamics XML Data Transfer for NAV 2013 R2

Text files, reading, writing, converting and different code pages

Microsoft Dynamics NAV is still using the old DOS code page for files.  If you create a file with the file variable and write text to that file you will get a DOS file.  The same thing happens when writing to a BLOB and exporting to a file.  The Code example below handles the DOS code page.

[code]OBJECT Codeunit 50000 Read and Write DOS File
{
OBJECT-PROPERTIES
{
Date=30.05.13;
Time=09:16:44;
Modified=Yes;
Version List=Dynamics.is;
}
PROPERTIES
{
OnRun=VAR
LineRead@10000000 : Text[250];
CrLf@10000001 : Text[2];
BEGIN
CrLf[1] := 13;
CrLf[2] := 10;
DOSFileName := FileMgt.ServerTempFileName(‘txt’);
DOSFile.CREATE(DOSFileName);
DOSFile.CREATEOUTSTREAM(OutStr);
StandardText.FINDSET;
REPEAT
OutStr.WRITETEXT(STRSUBSTNO(‘%1,%2’,StandardText.Code,StandardText.Description) + CrLf);
UNTIL StandardText.NEXT = 0;
DOSFile.CLOSE;

DOSFile.OPEN(DOSFileName);
DOSFile.CREATEINSTREAM(InStr);
WHILE NOT InStr.EOS DO BEGIN
InStr.READTEXT(LineRead,MAXSTRLEN(LineRead));
TempStandardText.Code := SELECTSTR(1,LineRead);
TempStandardText.Description := SELECTSTR(2,LineRead);
TempStandardText.INSERT;
END;
DOSFile.CLOSE;

MESSAGE(Text001,DOSFileName);
PAGE.RUNMODAL(PAGE::"Standard Text Codes",TempStandardText);
END;

}
CODE
{
VAR
StandardText@10000007 : Record 7;
TempStandardText@10000006 : TEMPORARY Record 7;
FileMgt@10000003 : Codeunit 419;
DOSFile@10000000 : File;
DOSFileName@10000004 : Text[250];
InStr@10000001 : InStream;
OutStr@10000002 : OutStream;
Text001@10000005 : TextConst ‘ENU=Server File Name : %1;ISL=Skr�arnafn � �j�ni : %1’;

BEGIN
END.
}
}[/code]

Using DotNet for the same job as the below example shows, will create a file with the Windows code page.

[code]OBJECT Codeunit 50001 Read and Write Windows File
{
OBJECT-PROPERTIES
{
Date=30.05.13;
Time=09:26:03;
Modified=Yes;
Version List=Dynamics.is;
}
PROPERTIES
{
OnRun=VAR
LineRead@10000000 : Text[250];
CrLf@10000001 : Text[2];
Loop@10000002 : Integer;
BEGIN
CrLf[1] := 13;
CrLf[2] := 10;
ISOFileName := FileMgt.ServerTempFileName(‘txt’);
StandardText.FINDSET;
REPEAT
dotNetFile.AppendAllText(ISOFileName,STRSUBSTNO(‘%1,%2′,StandardText.Code,StandardText.Description) + CrLf);
UNTIL StandardText.NEXT = 0;

dotNetArray := dotNetFile.ReadAllLines(ISOFileName);
FOR Loop := 0 TO (dotNetArray.Length – 1) DO BEGIN
LineRead := dotNetArray.GetValue(Loop);
TempStandardText.Code := SELECTSTR(1,LineRead);
TempStandardText.Description := SELECTSTR(2,LineRead);
TempStandardText.INSERT;
END;

MESSAGE(Text001,ISOFileName);
PAGE.RUNMODAL(PAGE::"Standard Text Codes",TempStandardText);
END;

}
CODE
{
VAR
dotNetFile@10000011 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.File";
dotNetArray@10000010 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089’.System.Array";
StandardText@10000007 : Record 7;
TempStandardText@10000006 : TEMPORARY Record 7;
FileMgt@10000003 : Codeunit 419;
ISOFileName@10000004 : Text[250];
Text001@10000005 : TextConst ‘ENU=Server File Name : %1;ISL=Skr�arnafn � �j�ni : %1’;

BEGIN
END.
}
}
[/code]

And to write and read UTF-8 encoded file

[code]OBJECT Codeunit 50002 Read and Write UTF8 File
{
OBJECT-PROPERTIES
{
Date=30.05.13;
Time=09:26:51;
Modified=Yes;
Version List=Dynamics.is;
}
PROPERTIES
{
OnRun=VAR
LineRead@10000000 : Text[250];
CrLf@10000001 : Text[2];
Loop@10000002 : Integer;
BEGIN
CrLf[1] := 13;
CrLf[2] := 10;
ISOFileName := FileMgt.ServerTempFileName(‘txt’);
StandardText.FINDSET;
REPEAT
dotNetFile.AppendAllText(ISOFileName,STRSUBSTNO(‘%1,%2’,StandardText.Code,StandardText.Description) + CrLf,Encoding.GetEncoding(‘utf-8’));
UNTIL StandardText.NEXT = 0;

dotNetArray := dotNetFile.ReadAllLines(ISOFileName,Encoding.GetEncoding(‘utf-8′));
FOR Loop := 0 TO (dotNetArray.Length – 1) DO BEGIN
LineRead := dotNetArray.GetValue(Loop);
TempStandardText.Code := SELECTSTR(1,LineRead);
TempStandardText.Description := SELECTSTR(2,LineRead);
TempStandardText.INSERT;
END;

MESSAGE(Text001,ISOFileName);
PAGE.RUNMODAL(PAGE::"Standard Text Codes",TempStandardText);
END;

}
CODE
{
VAR
dotNetFile@10000011 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.File";
dotNetArray@10000010 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Array";
Encoding@10000000 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089’.System.Text.Encoding";
StandardText@10000007 : Record 7;
TempStandardText@10000006 : TEMPORARY Record 7;
FileMgt@10000003 : Codeunit 419;
ISOFileName@10000004 : Text[250];
Text001@10000005 : TextConst ‘ENU=Server File Name : %1;ISL=Skr�arnafn � �j�ni : %1’;

BEGIN
END.
}
}
[/code]

This also gives us an easy way to convert files from one code page to another. For example from the DOS format to the Windows format.

[code]
ServerISOFileName := FileMgt.ServerTempFileName(‘xml’);
dotNetFile.WriteAllText(
ServerISOFileName,
dotNetFile.ReadAllText(ServerDOSFileName,Encoding.GetEncoding(‘ibm850’)),
Encoding.GetEncoding(‘iso-8859-1’));[/code]

Also if you use the UTF-8 example and replace GetEncoding(‘utf-8’) with GetEncoding(‘ibm850’) you will get a DOS formatted file.  Microsoft offers a list of all supported encoding methods here.  The beauty with the DotNet methods is the possibility to use RunOnClient property to read and write files from the client computer.

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.

Dotnet WebRequest error handling

In August last year I posted a way to use dotnet interop and webrequest to replace the automation objects.  I saw that we had a limitation that we have gotten used to.  When using the WinHTTP automation we where able to look at the status and if the status was not the integer 200 we had an error.  We where able to show this error to the user and continue running the code.  When we use the dotnet interop we are missing this error handling method.  We use

[code]  HttpWebResponse := HttpWebRequest.GetResponse;[/code]

and if we have an error the code stoppes.  I have gotten a few request about this error handling and finally today I took a look at this and saw no easy way to solve this in C/Side.  So I created a DLL.  I created a C# class library in Visual Studio 2010 with the following code

using System;
namespace NAVWebRequest
{
public class NAVWebRequest
{
public bool doRequest (ref System.Net.HttpWebRequest WebRequest, ref System.Net.WebException WebException, ref System.Net.WebResponse WebResponse)
{
try
{
WebResponse = WebRequest.GetResponse();
return true;
}
catch (System.Net.WebException webExcp)
{
WebException = webExcp;
return false;
}
}
}
}

 

and the DLL that I build I put in my add-ins folder, both on the server and on my developement client machine.

Next I add this DLL and the HttpWebException to my Codeunit

VAR
Text003@1100408001 : TextConst 'ENU=Error: %1\%2;ISL=St”Ðuvilla %1, %2\\%3\%4';
Credential@1000000015 : DotNet "'System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.NetworkCredential";
HttpWebRequest@1000000014 : DotNet "'System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.HttpWebRequest";
HttpWebResponse@1000000013 : DotNet "'System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.WebResponse";
HttpWebException@1000000017 : DotNet "'System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Net.WebException";
MemoryStream@1000000012 : DotNet "'mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.IO.MemoryStream";
XMLRequestDoc@1000000011 : DotNet "'System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Xml.XmlDocument";
XMLResponseDoc@1000000010 : DotNet "'System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.System.Xml.XmlDocument";
NAVWebRequest@1000000018 : DotNet "'NAVWebRequest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f53f0925d26e1382'.NAVWebRequest.NAVWebRequest";

 

and instead of using GetResponse as I showed before I use my new class library

 NAVWebRequest := NAVWebRequest.NAVWebRequest;
IF NOT NAVWebRequest.doRequest(HttpWebRequest,HttpWebException,HttpWebResponse) THEN BEGIN
Log.SetErrorMessage(HttpWebException.Message);
Log.INSERT;
COMMIT;
ERROR(Text003,HttpWebException.Status.ToString,HttpWebException.Message);
END;

 

and I now have the possibility to log errors and continue my batch.

The DLL needed is attached.

NAVWebRequest

[CurrLanguage :=] LANGUAGE([NewLanguage]) support in XMLPorts

It is commonly used option to change the reporting language according to a customer, vendor og an employee.  However, some reports are XML files.  I found that I also needed to change the data language in XML files.

The workaround is to change the GLOBALLANGUAGE before executing the xmlport.  To get similar language support in xmlports as in reports would be ideal.

Just created a suggestion on Microsoft Connect.  Go ahead and vote if you agree.

Using a .dll proxy for web services

I have now completed my first all-dotnet codeunit.  The codeunit uses a dll file that I created from the web service WDSL.  This makes the programming a lot easier.

This solution has a BLOB fields that stores both incoming and outgoing xml.  When I use a proxy dll I don’t build a xml document and I never handle xml documents.  Again dotnet has a solution.  I use xml serializer from the system.xml object.

[code]LOCAL PROCEDURE SerializeToXMLStream@1100408008(VAR Object@1100408002 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Object" RUNONCLIENT;VAR NavOutstr@1100408004 : OutStream);
VAR
xmlSerializer@1100408000 : DotNet "’System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Xml.Serialization.XmlSerializer" RUNONCLIENT;
StreamWriter@1100408003 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.StreamWriter" RUNONCLIENT;
File@1100408001 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.File" RUNONCLIENT;
NavInstr@1100408005 : InStream;
BEGIN
IF ISNULL(Object) THEN EXIT;
StreamWriter := StreamWriter.StreamWriter(TempFileName);
xmlSerializer := xmlSerializer.XmlSerializer(Object.GetType);
xmlSerializer.Serialize(StreamWriter,Object);
StreamWriter.Close;
UPLOADINTOSTREAM(”,MagicPath,”,TempFileName,NavInstr);
COPYSTREAM(NavOutstr,NavInstr);
File.Delete(TempFileName);
END;[/code]

I can send the proxy object to this function and will get the xml into the BLOB field.

[code]CurrencyRates := StatementService.GetCurrencyRates(TypeRates,CREATEDATETIME(RatesDate,235959T));

WITH BankAccAction DO BEGIN
"Modified by User ID" := USERID;
"Modification Date and Time" := CURRENTDATETIME;
"Incoming Message".CREATEOUTSTREAM(OutStr);
SerializeToXMLStream(CurrencyRates,OutStr);
MODIFY;
COMMIT;
END;[/code]

This example is executed on the client since I am using certificates and web services security for the communication.