Link to a Page with bookmark

I was scanning a TIF image into a BLOB field in Dynamics NAV.  If the BLOB field is defined as an image I will see a thumbnail for the image in my Page.

DocView

In my scanning code I wanted to create a record link for the record I was linking to and that link was supposed to open this page and display the correct image thumbnail.  An UI Add-in can then be used to view the image in full-page mode.  I have not made that UI Add-in, if you know of any available one please let me know.

I saw that if I created a link to the Page from the Windows Client I got a bookmark string in the URL and I did not know how the bookmark string was created.  A quick search did not help me so I asked Microsoft.  The answer came from Duilio Tacconi this morning; there is a “magic” FORMAT statement.  I suggest that Microsoft add this knowledge to the help text for the format property for general availability but Duilio sent me a link to MSDN and to Freddy’s blog.

Here is how I did this.  I first created and stored the start of the URL.

[code]
IF "Default Storage" = "Default Storage"::"Internal with Windows Client" THEN
IF "Link Web URL" = ” THEN
"Link Web URL" :=
SigningTools.MiddleTierServicePath + ‘/’ +
SigningTools.URLEncode(COMPANYNAME) +
‘/runpage?page=10001150&mode=view’;[/code]

And then in the scanning upload function I add

[code]
RecRef.GETTABLE(Storage);
URL := SigningSetup."Link Web URL" + ‘&bookmark=’ + FORMAT(RecRef.RECORDID,0,10);[/code]

The result is an URL that I use in RecRef.ADDLINK function.  I can now link to a multiple scanned documents from any record in the database.

The functions that I use to encode the company name and to find the middle tier service path are here.

[code]OBJECT Codeunit 57129 Path Tools
{
OBJECT-PROPERTIES
{
Date=29.04.13;
Time=08:39:25;
Modified=Yes;
Version List=Dynamics.is;
}
PROPERTIES
{
OnRun=BEGIN
END;

}
CODE
{
VAR
Environment@1000000000 : DotNet "’mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Environment" RUNONCLIENT;

PROCEDURE URLEncode@1100409003(String@1100409000 : Text[1024]) CodedString : Text[1024];
VAR
httpUtility@1000000000 : DotNet "’System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’.System.Web.HttpUtility";
BEGIN
httpUtility := httpUtility.HttpUtility;
CodedString := httpUtility.UrlPathEncode(String);
END;

PROCEDURE MiddleTierServicePath@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";
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’);

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 SOAPWebServicePath@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 ODataWebServicePath@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";
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’);

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;

BEGIN
END.
}
}

[/code]

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

The new PingPong add-in for NAV 2013

In NAV 2009 I used a custom control add-in to enable timers in the Role Tailored Client.  NAV 2013 ships with a control add-in that is called PingPong.  In upgrading one of my solution to NAV 2013 I wanted to remove the custom control and introduce the PingPong instead.

PingPongProperties

The control requires a name and as the standard functionality does I use the name PingPong.  The code I had in the – OnControlAddin trigger is now moved to a new trigger, PingPong::Pong.  The new timer does not work the same way the old one does.  The method used is similar to the new method in the Job Queue where the sleep function is used instead of a regular timer.  By executing Currpage.PingPong.Ping(500) a new thread is started that sleeps for 500 milliseconds and then fires the trigger PingPong::Pong.  Hence the add-in name.  When all required code in this trigger has been executed another Ping is required to thow the next ball.

PingPongTriggers

This is all good if the application is only using one timer.  I saw that if I already had one PingPong working then in the subsequent page PingPong did not work.  The good news is that the timer control add-in that I created for NAV 2009 also works in NAV 2013.  The page that I open from the page running PingPong will continue to use the timer add-in that originated from Freddy and I changed a little bit.

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

Blob Data and TRANSFERFIELDS

As I was reading the list of published platform updates for NAV 2013 I saw that Microsoft was fixing a bug where the content of a BLOB field is not transferred to a new record with the TRANSFERFIELDS function.  I must admit that I have never counted on the TRANSFERFIELDS function to move the BLOB data from one table to another.  I have always created in- and outstream like this

[code]
InboxTransaction2.CALCFIELDS("PDF Document");
IF "InboxTransaction2.PDF Document".HASVALUE THEN BEGIN
InboxTransaction2."PDF Document".CREATEINSTREAM(InStr);
HandledInboxTransaction2."PDF Document".CREATEOUTSTREAM(OutStr);
COPYSTREAM(OutStr,InStr);
END;[/code]

Now, If I am using a version of NAV 2013 that has fixed the TRANSFERFIELDS bug I can simply do

[code]
InboxTransaction2.CALCFIELDS("PDF Document");
HandledInboxTransaction2."PDF Document" := InboxTransaction2."PDF Document";[/code]

or

[code]
InboxTransaction2.CALCFIELDS("PDF Document");
HandledInboxTransaction2.TRANSFERFIELDS(InboxTransaction2);[/code]

Keep in mind that if the CALCFIELDS function is missing then nothing will be transferred.

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

Intercompany Transaction from Sales Posting

Some of our customers are using Intercompany Posting.  This is a useful tool but I have needed to make several changes to the code to make sure that the customer gets the functionality that is needed.  For example we are using separate IC Partner codes for each Responsibility Center within a single company.

One thing though I would like Microsoft to fix in Codeunit 80.  There the Intercompany Posting code is executed after a COMMIT.  I suggest a small change.

[code]
IF NOT WhseRqst.ISEMPTY THEN
WhseRqst.DELETEALL;
END;

//#IC-
IF Invoice AND ("Bill-to IC Partner Code" <> ”) THEN
IF "Document Type" IN ["Document Type"::Order,"Document Type"::Invoice] THEN
ICInOutBoxMgt.CreateOutboxSalesInvTrans(SalesInvHeader)
ELSE
ICInOutBoxMgt.CreateOutboxSalesCrMemoTrans(SalesCrMemoHeader);
//#IC+

InsertValueEntryRelation;
IF NOT InvtPickPutaway THEN
COMMIT;
CLEAR(WhsePostRcpt);
CLEAR(WhsePostShpt);
CLEAR(GenJnlPostLine);
CLEAR(ResJnlPostLine);
CLEAR(JobPostLine);
CLEAR(ItemJnlPostLine);
CLEAR(WhseJnlPostLine);
CLEAR(InvtAdjmt);
Window.CLOSE;
//#IC-
// IF Invoice AND ("Bill-to IC Partner Code" <> ”) THEN
// IF "Document Type" IN ["Document Type"::Order,"Document Type"::Invoice] THEN
// ICInOutBoxMgt.CreateOutboxSalesInvTrans(SalesInvHeader)
// ELSE
// ICInOutBoxMgt.CreateOutboxSalesCrMemoTrans(SalesCrMemoHeader);
//#IC+
END;[/code]

There is also another coding method in Codeunit 427 that I would like Microsoft to change.

[code]

CreateOutboxJnlTransaction(TempGenJnlLine : TEMPORARY Record "Gen. Journal Line";Rejection : Boolean) : Integer
ICPartner.GET(TempGenJnlLine."IC Partner Code");
IF ICPartner."Inbox Type" = ICPartner."Inbox Type"::"No IC Transfer" THEN
EXIT(0);

GLSetup.LOCKTABLE;
GetGLSetup;
IF GLSetup."Last IC Transaction No." < 0 THEN
GLSetup."Last IC Transaction No." := 0;
ICTransactionNo := GLSetup."Last IC Transaction No." + 1;
GLSetup."Last IC Transaction No." := ICTransactionNo;
GLSetup.MODIFY;
[/code]

I don’t like to use the General Ledger Setup table to store the last transaction number.