Need NAS to communicate with a web service ?

My recent post on creating a class in visual studio to simplify the NAV C/AL code for web service is a easy solution for NAV 2013.  It will work both for the user and for the Job Queue.  In NAV 2009 it will also work for the user running the Role Tailored Client but it will not work in the Job Queue.

There is a solution.  Use the VB.NET NAV Application Server.  This application server is able to execute any codeunit in NAV with dotnet support. 

The license required to start this application server is also cheaper than the classic NAS license.

Service to import from Serial Port

Similar to the last post I have also been asked to listen to a serial port and import the data into NAV.  I even used the same VB.NET service.  On the NAV side I added two functions to my web service.

Where RMSerial is a table with the following code.

Here is the VB.NET code for the service.

[code lang=”vb”]Imports System
Imports System.Timers
Imports System.Net
Imports System.IO
Imports System.IO.Ports

Public Class FileImportService
Dim Salvor1 As Salvor.SalvorWebService
Dim Timer2 As System.Timers.Timer
Dim User As New System.Net.NetworkCredential
Dim Serial1 As New System.IO.Ports.SerialPort

Protected Overrides Sub OnStart(ByVal args() As String)
‘ Add code here to start your service. This method should set things
‘ in motion so your service can do its work.
Salvor1 = New Salvor.SalvorWebService
User.Domain = "<Domain>"
User.UserName = "<User>"
User.Password = "<Password>"
Salvor1.Credentials = User

Timer2 = New System.Timers.Timer(30000)
AddHandler Timer2.Elapsed, AddressOf ProcessSerialData
Timer2.Interval = 30000
Timer2.Enabled = True
Timer2.Stop()

If My.Settings.COMPort <> "" Then
If Serial1.IsOpen Then
Serial1.Close()
End If
WriteDebug("COM Port Selected: " & My.Settings.COMPort)
Serial1.PortName = My.Settings.COMPort
Serial1.BaudRate = 9600
Serial1.Parity = Parity.Even
Serial1.DataBits = 8
Serial1.StopBits = 1
Serial1.Open()
If Serial1.IsOpen Then
WriteDebug("Serial Port Opened")
Else
WriteDebug("Failed to open Serial Port")
End If
AddHandler Serial1.DataReceived, AddressOf Serial1_DataReceived
End If
End Sub

Protected Overrides Sub OnStop()
‘ Add code here to perform any tear-down necessary to stop your service.
If My.Settings.COMPort <> "" Then
If Serial1.IsOpen Then
Serial1.Close()
End If
End If
End Sub

Protected Sub Serial1_DataReceived()
Timer2.Stop()
WriteDebug("Serial Event Started, buffer size: " & Serial1.ReadBufferSize)
Dim LineRead As String
Dim LineResponse As String = ""
Dim WaitLoop As Integer = 0
LineRead = Serial1.ReadExisting
If LineRead = "!" Then
LineResponse = "$"
ElseIf LineRead = "*" Then
LineResponse = "&"
ElseIf Left(LineRead, 1) = "[" Then
While Right(LineRead, 1) <> "]" And WaitLoop < 10
Threading.Thread.Sleep(100)
LineRead = LineRead & Serial1.ReadExisting
WaitLoop = WaitLoop + 1
End While
If Right(LineRead, 1) = "]" Then
Try
If Salvor1.AboutCompany = "SAM" Then
LineResponse = Salvor1.COMPort(LineRead)
End If
Catch ex As Exception
LineResponse = "%"
WriteToEventLog("Web Service unavailable:" & ex.ToString, EventLogEntryType.Error)
End Try
Else
LineResponse = "%"
End If
End If
Serial1.Write(LineResponse)
WriteDebug("Serial read: " & LineRead)
WriteDebug("Serial response: " & LineResponse)
Timer2.Interval = 30000
Timer2.Enabled = True
Timer2.Start()
End Sub

Protected Sub ProcessSerialData()
Timer2.Enabled = False
Timer2.Stop()
Dim Success As Boolean
Try
Success = Salvor1.ProcessSerialData
If Not Success Then
WriteToEventLog("Failed to process serial data", EventLogEntryType.Error)
End If
Catch ex As Exception
WriteToEventLog("Web Service unavailable:" & ex.ToString, EventLogEntryType.Error)
End Try
End Sub

Protected Sub WriteToEventLog(ByVal Message As String, ByVal EntryType As EventLogEntryType)
Dim MyLog As New EventLog()
‘ Check if the the Event Log Exists
If Not Diagnostics.EventLog.SourceExists(Me.ServiceName) Then
Diagnostics.EventLog.CreateEventSource(Me.ServiceName, Me.ServiceName & " Log")
‘ Create Log
End If
MyLog.Source = Me.ServiceName
‘ Write to the Log
Diagnostics.EventLog.WriteEntry(MyLog.Source, Message, EntryType)
End Sub

Protected Sub WriteDebug(ByVal Message As String)
If My.Settings.Debug Then
WriteToEventLog(Message, EventLogEntryType.Information)
End If
End Sub
End Class
[/code]

Service to import files

The task is to import every file that is dropped into a specific folder on my local drive into NAV.  The solution is a windows service programmed in Visual Studio 2008 VB.NET.

The first step is to create a web service in Dynamics NAV that accepts a text line and a file name.  Another function to remove the file if the import fails and the third to process the file after it has been imported.

The vb.net code from Visual Studio

[code lang=”vb”]Imports System
Imports System.Timers
Imports System.Net
Imports System.IO

Public Class FileImportService
Dim Salvor1 As Salvor.SalvorWebService
Dim Timer1 As System.Timers.Timer
Dim User As New System.Net.NetworkCredential

Protected Overrides Sub OnStart(ByVal args() As String)
‘ Add code here to start your service. This method should set things
‘ in motion so your service can do its work.
Salvor1 = New Salvor.SalvorWebService
User.Domain = "<Domain>"
User.UserName = "<User>"
User.Password = "<Password>"
Salvor1.Credentials = User

Timer1 = New System.Timers.Timer(30000)
AddHandler Timer1.Elapsed, AddressOf OnTimedEvent

Timer1.Interval = 30000
Timer1.Enabled = True
Timer1.Start()
WriteDebug("Timer 1 Started")

‘ If the timer is declared in a long-running method, use
‘ KeepAlive to prevent garbage collection from occurring
‘ before the method ends.
GC.KeepAlive(Timer1)

End Sub

Protected Overrides Sub OnStop()
‘ Add code here to perform any tear-down necessary to stop your service.
End Sub

Protected Sub OnTimedEvent(ByVal source As Object, ByVal e As ElapsedEventArgs)
Timer1.Enabled = False
Timer1.Stop()
WriteDebug("File Event Started")
Try
If Salvor1.AboutCompany = "SAM" Then
ReadFolder()
End If
Catch ex As Exception
WriteToEventLog("Web Service unavailable:" & ex.ToString, EventLogEntryType.Error)
End Try
Timer1.Enabled = True
Timer1.Start()
End Sub

Protected Sub ReadFolder()
Dim dirInfo As New DirectoryInfo(My.Settings.ImportFolder)
Dim FileArray As FileInfo() = dirInfo.GetFiles()

For Each TextFile In FileArray
If ReadFile(TextFile) Then
WriteDebug("Check File: " & TextFile.Name)
If Salvor1.ProcessFile(TextFile.Name) Then
DeleteFile(TextFile)
Else
WriteDebug("Rollback File: " & TextFile.Name)
Salvor1.RemoveFile(TextFile.Name)
End If

Else
Salvor1.RemoveFile(TextFile.Name)
End If
Next

End Sub

Protected Function ReadFile(ByVal TextFile As FileInfo) As Boolean
Dim Success As Boolean
Try
If File.Exists(TextFile.FullName) Then
Dim ioFile As New StreamReader(TextFile.FullName)
Dim ioLine As String
Success = True

While Not ioFile.EndOfStream
ioLine = ioFile.ReadLine
Success = Success And Salvor1.InsertLine(TextFile.Name, ioLine)
End While
ioFile.Close()
End If
Catch ex As Exception
Success = False
WriteToEventLog("Import of file " & TextFile.FullName & " failed:" & ex.ToString, EventLogEntryType.Error)
End Try
Return Success
End Function

Protected Sub DeleteFile(ByVal TextFile As FileInfo)
Try
WriteDebug("Delete File: " & TextFile.Name)
TextFile.Delete()
Catch ex As Exception
WriteToEventLog("Failed to delete file " & TextFile.FullName & ":" & ex.ToString, EventLogEntryType.Error)
End Try
End Sub

Protected Sub WriteToEventLog(ByVal Message As String, ByVal EntryType As EventLogEntryType)
Dim MyLog As New EventLog()
‘ Check if the the Event Log Exists
If Not Diagnostics.EventLog.SourceExists(Me.ServiceName) Then
Diagnostics.EventLog.CreateEventSource(Me.ServiceName, Me.ServiceName & " Log")
‘ Create Log
End If
MyLog.Source = Me.ServiceName
‘ Write to the Log
Diagnostics.EventLog.WriteEntry(MyLog.Source, Message, EntryType)
End Sub

Protected Sub WriteDebug(ByVal Message As String)
If My.Settings.Debug Then
WriteToEventLog(Message, EventLogEntryType.Information)
End If
End Sub
End Class[/code]

Geometric mean

In one of my recent projects I needed to calculate a geometric mean for a group of numbers.  The Geometric mean formula is generally, if the numbers are x_1,\ldots,x_n, the geometric mean G satisfies G = \sqrt[n]{x_1 x_2 \cdots x_n},

The problem appeared when I needed to multiply numbers and got a overflow on the decimal data type in Dynamics NAV.

I produced two solutions to this problem; first for Classic Client using Excel and secondly for Role Tailored Client using Add-in.  Using Excel for this job is slow but the Add-in works great.

In Excel I add values to a column and then use the built in function GEOMEAN(‘<Range>’) to calculate the value.  This works for large values.

The GeoMean Class Add-in I created in Visual Studio in VB.NET
[code htmlscript=”false” lang=”vb”]Public Class GeoMeanClass
Dim TotalValue As Double
Dim NoOfValues As Integer
Public Sub ResetValue()
TotalValue = 1
NoOfValues = 0
End Sub
Public Sub AddValue(ByVal Value As Decimal)
If Value = 0 Then Exit Sub
TotalValue = TotalValue * Value
NoOfValues = NoOfValues + 1
End Sub
Public ReadOnly Property NoOfStoredValues() As Integer
Get
Return NoOfValues
End Get
End Property
Public ReadOnly Property GetGeoMean() As Decimal
Get
If NoOfValues = 0 Then
Return 0
Else
Return CDec(TotalValue ^ (1 / NoOfValues))
End If
End Get
End Property
End Class[/code]
Attached are the Add-in and the codeunits needed to calculate Geometric mean.

GeoMean

 

VB.NET NAV Application Server

Most of my clients require a running NAV Application Server.  The NAS that is included in NAV 2009 R2 requires a license that is included in most licenses today.  However, there are cases where more than one NAS is needed.  That requires additional NAS licenses.  Where the customer is running NAV 2009 R2 middle tier service this changes.  By running a VB.NET NAV Application Server it is possible to setup multiple services on a single CAL license.  The CAL license is not as expensive as the NAS license.  Here is the solution that I offer.

First, I create a codeunit in NAV

[code htmlscript=”false”]ExecuteCodeunit(CodeunitID : Integer;Log : Boolean) Success : Boolean

IF Log THEN LogEntryNo := InsertLogEntry(5,CodeunitID);
Success := CODEUNIT.RUN(CodeunitID);
IF Log THEN
UpdateLogEntry(LogEntryNo,Success)
ELSE IF NOT Success THEN BEGIN
LogEntryNo := InsertLogEntry(5,CodeunitID);
UpdateLogEntry(LogEntryNo,Success)
END;

InsertLogEntry(ObjectType : ‘,,,Report,,Codeunit’;ObjectNo : Integer) : Integer
WITH JobQueueLogEntry DO BEGIN
INIT;
ID := CREATEGUID;
“User ID” := USERID;
“Start Date/Time” := CURRENTDATETIME;
“Object Type to Run” := ObjectType;
“Object ID to Run” := ObjectNo;
INSERT(TRUE);
COMMIT;
EXIT(“Entry No.”);
END;

UpdateLogEntry(LogEntryNo : Integer;WasSuccess : Boolean)
WITH JobQueueLogEntry DO BEGIN
GET(LogEntryNo);
“End Date/Time” := CURRENTDATETIME;
IF WasSuccess THEN
Status := Status::Success
ELSE BEGIN
Status := Status::Error;
SetErrorMessage(COPYSTR(GETLASTERRORTEXT,1,1000));
END;
MODIFY;
COMMIT;
END;[/code]

This codeunit uses the Job Queue Log to log the execution.  Next step is to publish this codeunit as a web service in table no. 2000000076.  Default web service name is NAVAppServer.

On the server you install the following files (in my case to C:\Program Files (x86)\Dynamics.is\VB.NET NAV Application Server)


Next step is to edit the VB.NET NAV Application Server.exe.config file and customize the values.

To install as a service start command prompt in elevated mode and execute installutil.exe command.  The install will prompt for the user to start the service.

The last step is to change the service startup and start the service.

The service will create entries in the Application Log.

NAV Web Service Codeunit

The following ZIP files are encrypted.

VB.NET NAV Application Server DEFAULT Executables

NAV App Server Visual Studio 2008 Project

Using Web Services for your NAS jobs

In NAV 7 the NAV Application Server will no longer be supported.  The Job Queue has been redesigned to support the new STARTSESSION feature that will create a new session on the service tier to execute a given task.

In NAV 2009 and going forward it is possible to use web services to act like an application server with the help of a simple program with a timer.

For example a program with a code like this
[code htmlscript=”false” lang=”vb”]Public Class NAVAppServer
Dim Success As Boolean
Dim NAVApp1 As New NAVApp.NAVAppServer
Dim SystemUser As New System.Net.NetworkCredential
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs)
NAVApp1.UseDefaultCredentials = True
NAVApp1.Url = "http://dynamics.is:7047/DynamicsNAS/WS/Dynamics%20Inc/Codeunit/NAVAppServer"
NAVApp1.ExecuteCodeunit(50059, True)

End Sub
End Class[/code]
The Codeunit NAVAppServer is here and also attached
[code htmlscript=”false”]ExecuteCodeunit(CodeunitID : Integer;Log : Boolean) Success : Boolean
LogEntryNo := InsertLogEntry(5,CodeunitID);
Success := CODEUNIT.RUN(CodeunitID);
UpdateLogEntry(LogEntryNo,Success);

InsertLogEntry(ObjectType : ‘,,,Report,,Codeunit’;ObjectNo : Integer) : Integer
WITH JobQueueLogEntry DO BEGIN
INIT;
ID := CREATEGUID;
"User ID" := USERID;
"Start Date/Time" := CURRENTDATETIME;
"Object Type to Run" := ObjectType;
"Object ID to Run" := ObjectNo;
INSERT(TRUE);
COMMIT;
EXIT("Entry No.");
END;

UpdateLogEntry(LogEntryNo : Integer;WasSuccess : Boolean)
WITH JobQueueLogEntry DO BEGIN
GET(LogEntryNo);
"End Date/Time" := CURRENTDATETIME;
IF WasSuccess THEN
Status := Status::Success
ELSE BEGIN
Status := Status::Error;
SetErrorMessage(COPYSTR(GETLASTERRORTEXT,1,1000));
END;
MODIFY;
COMMIT;
END;[/code]
This is published as a web service by adding an entry into table 2000000076 “Web Service”.

NAVAppServerCodeunit

 

My asp.net website using NAV Web Service

I am building an asp.net website that communicates with NAV via Web Services.  One of the issues I had to solve was the authentication between the web and the NAV Web Services.

You can ether use NTLM authentication or the current user.  If you will be using NTLM you will need code similar to this in you website.
[code htmlscript=”false” land=”vb”]Dim NAVPunch1 As New WebService.NAVPunch
Dim User As New System.Net.NetworkCredential
User.Domain = "Dynamics.is"
User.UserName = "Gunnar"
User.Password = "<password>"
NAVPunch1.Credentials = User
NAVPunch1.Url = "http://Dynamics.is:7047/DynamicsNAV/WS/" & _
"Dynamics/Codeunit/NAVPunch"[/code]
This also means that you have to store the username, domain and password in you web site code. You will also need to enable NTLM authentication in your CustomSettings.xml
[code htmlscript=”false” lang=”vb”]<add key="WebServicesUseNTLMAuthentication" value="true"></add>[/code]
The other way is to use the credentials of the user running the web. In that case the code would be similar to this:
[code htmlscript=”false” lang=”vb”]NAVPunch1.UseDefaultCredentials = True
NAVPunch1.Url = "http://Dynamics.is:7047/DynamicsNAV/WS/" & _
"Dynamics/Codeunit/NAVPunch"[/code]
And no changes to CustomSettings.xml are required.  The authentication will be handled with IIS.  You will need to go into Internet Information Services (IIS) Manager.  Go into Application Pools and add a new application pool

Select a name that fits you web site and then go to Advanced Settings…

and update the Idendity.

Then go and select this Application Pool for the web site.

The final step is to make sure that the user you select in your code or in the application pool has access to NAV Web Services.  That is done with the standard authentication methods in Dynamics NAV.

Web Service that deliveres BLOB data

I wanted to be able to use the Record Link table in NAV to link to my attachments. Since that part of NAV is not customizable every attachment that I link to NAV needs to be accessible via URL.

So, when I import or scan a file into NAV it can be stored in a BLOB or I can store it in a customized external database. After storing the file I create a URL link to that file and insert that as a new link to any record in NAV. To complete this task I created a NAV Web Service that delivers the file as a base64 string. Here is the part of the code that encodes the BLOB stream to a BigText variable.
[code htmlscript=”false”]TempFile.CREATETEMPFILE;
TempFileName := TempFile.NAME;
TempFile.CLOSE;
TempFile.TEXTMODE(FALSE);
TempFile.CREATE(TempFileName);
TempFile.CREATEOUTSTREAM(OutStr);
DocumentStore.Blob.CREATEINSTREAM(InStr);
COPYSTREAM(OutStr,InStr);
TempFile.CLOSE;

CREATE(XMLDoc);
CREATE(ADOStream);
XMLNode := XMLDoc.createNode(‘element’, ‘ImageFile’, ‘SKYRR Signing’);
XMLNode.dataType := ‘bin.base64’;

ADOStream.Type := 1;
ADOStream.Open();
ADOStream.LoadFromFile(TempFileName);
XMLNode.nodeTypedValue := ADOStream.Read();
ADOStream.Close();

Document.ADDTEXT(XMLNode.text);

CLEAR(ADOStream);
CLEAR(XMLNode);
CLEAR(XMLDoc);
ERASE(TempFileName);[/code]
Then I created a aps.net website that can get the document both from this NAV Web Service and also from the customized database all based on the parameters passed with the URL.

Attached is a part of the NAV code and the website required to deliver the attachment.

WebSite

Attachment NAV Source Code

 

Example on using NAV Web Service in Visual Studio vb.net

This is using the web service in my previous post.  In Visual Studio I start by adding the web reference.  Select Project -> Add Service Reference

Then select Advanced…

Then Add Web Reference…

Where I insert URL encoded string for the published codeunit including the company name.

I created a form with a listbox object and a button.  I used this code to insert all employee no’s into the listbox.
[code htmlscript=”false” lang=”vb”]Public Class Form1
Dim ClockID As String
Dim Success As Boolean
Dim DefCred As Boolean
Dim ResponseMessage As String
‘The Web Service
Dim NAVPunch1 As New WebService.NAVPunch
‘New set of credentials
Dim User As New System.Net.NetworkCredential

Private Sub Form1_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
If DefCred Then
NAVPunch1.UseDefaultCredentials = True
NAVPunch1.Url = ""
Else
User.Domain = ""
User.UserName = ""
User.Password = ""
NAVPunch1.Credentials = User
NAVPunch1.Url = ""
End If
ClockID = "{4A212826-CAF3-4E3C-9D97-6923A692A209}"

End Sub
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
‘ Get Employee List
ResponseMessage = ""
ListBox1.Items.Clear()
Dim EmployeeList As New WebService.EmployeeList
Try
Success = NAVPunch1.GetClockEmployees(ClockID, _
EmployeeList, _
ResponseMessage)
CheckBox1.Checked = Success
TextBox1.Text = ResponseMessage
If Success Then
For Each Employee In EmployeeList.Employee
ListBox1.Items.Add(Employee.No)
Next
End If
Finally
End Try
End Sub
End Class[/code]

Scanning in Remote Desktop

When running Dynamics NAV as a Remote Application or in Remote Desktop you are missing connection to your locally connected scanner.  I have created a solution that uses a standalone application running on the client computer.  That application uses the TwainControlX from Ciansoft and connects to a Microsoft SQL database.  The database needs to be on a computer that both the client computer and the remote desktop computer have access to.

On that database server a database is created with only one table.  That table can be truncated every night in a maintenance job.  This is the table creation script.

The program on the client then connects to the database and to the scanner.

A code in Dynamics NAV creates the request for a scan by inserting a entry into the ScanRequest table and the application on the client computer automatically scans the images and updates that scan request entry.  The updated entry then containes the image file that is imported into Dynamics NAV or attached as a link.

The following ZIP files are encrypted.  View the Product page for more information.

Twain Scanner (ISL)
NAV Myndlestur (TwainOCX) Source Code (ISL)