Time driven actions with .Net Class Timer

Till Nav 2009 if time driven actions were needed, it was done using automation NTimer.dll (NavTimer). With Nav 2013 and newer Versions Microsoft recommends to avoid usage of automations. As a result many of the common used automations, shipped with Nav 2009 and earlier, disappeared. There are few descriptions, how to solve common issues formerly solved with automations. So, let’s have a look at the Timer issue.

The .Net Framework contains class System.Timers.Timer in assembly System.dll. To use and test it, create a new codeunit and add some global variables:

Variable Timer is of subtype System.Timers.Timer. We set properties RunOnClient to false, but WithEvents to true. Only one of these properties can be set to true at one time, both together is not allowed. But ok, we need the Triggers (in .Net called Events). With RunOnClient=false, the code runs on the server … and that’s ok. Setting WithEvents to true we get automatically all embedded .Net Events written in the C/AL code, in that case Timer::Elapsed(sender : Variant;e : DotNet “System.Timers.ElapsedEventArgs”) and Timer::Disposed(sender : Variant;e : DotNet “System.EventArgs”). We only use the first one.

In the sample we want create a file and write lines into the file, step by step, every 3 seconds one line. We use a Counter for a break condition.

OnRun()Counter := 0; // Counter is an Integer variableMESSAGE('Timer started');IF EXISTS('c:\temp\sample.txt') THEN  ERASE('c:\temp\sample.txt'); // delete the file, if it already existsTextFile.CREATE('c:\temp\sample.txt'); // TextFile is a FILE variableTextFile.TEXTMODE := TRUE;Timer := Timer.Timer(); // create a Timer instanceTimer.Interval := 3000; // i.e. 3 secs (unit is ms)Timer.Enabled := TRUE;Timer.Start(); // starts the timerTimer::Elapsed(sender : Variant;e : DotNet "System.Timers.ElapsedEventArgs")Counter := Counter + 1;TextFile.WRITE('line ' + FORMAT(Counter) + ', ' + FORMAT(TODAY) + ' ' + FORMAT(TIME));// stop timer after trigger Elapsed was called 10 timesIF Counter > 10 THEN BEGIN  Timer.Enabled := FALSE;  Timer.Stop(); // stops the timer  Timer.Close();  CLEAR(Timer);  TextFile.CLOSE;END;

Result:

cheers

Error: Could not load the selected type library

This is a common error, which can be caused by following reasons:

  • an automation is missing
  • the automation was missing, then installed on the system, but the nav object was not re-compiled after installation
  • a different version of the automation is expected by the nav object.

If this error occurs, check where the variable is defined (local or global variables list). After you found the variable, you will see, that the value in field Subtype is like “Unknown Automation Server.Application”. When exporting the Nav object as text file, you would find a subtype value like that: Class1@1000000010 : Automation “{D1233675-2BBA-49DD-AD90-1680A404EAD5} 1.0:{DB3E185E-C123-46C2-9C62-F4A4E81E0B8F}:’FunnyAutomation’.Class1”. If you don’t know exactly what you are missing, this strange looking subtype value can help you for research.

Next step is – and that is the importand one – remove the value in field subtype! Do not click on the Assist button first and choose the automation without removing the value. It won’t work! So, first remove the value, then click on the Assist button and search for the missing automation.

If you do not find the missing automation, then look for the setup file and install the automation. After that restart the CC Client and start this fix procedure from the beginning. If you don’t know, which automation is missing (maybe an old code, a code of a different developer, …), then start with a research, ask your colleagues, your PM, do a little web search or find an alternative solution.

If all is fine, the automation is installed and the Unknown… value was removed, find and choose the automation after clicking on the Assist button and select the needed embedded class of the automation. After that re-compile the Nav Object and test the whole thing.

cheers

Send Outlook Mail with different Sender Address

For sending outlook mails one can use CU 397 and it works fine, if it’s ok to use the standard outlook profile as sender address (“From”). If you want to use a different sender address, then this is not possible.

To get that possibility let’s have a look at the in CU 397 used .net assemblies. There we have especially assembly Microsoft.Dynamics.Nav.Integration.Office. For most cases a nice little thing. But it delivers no possibility to set/change the sender address. So what to do?

Assembly Microsoft.Dynamics.Nav.Integration.Office.dll references (internally) the assembly Microsoft.Office.Interop.Outlook. So let’s have a more precise look on THAT assembly. There we have the typical from COM to .Net converted assembly with strange looking classes like Microsoft.Office.Interop.Outlook.ApplicationClass, etc. But … this class ApplicationClass contains a property Session and further a property Accounts, what means the outlook user profiles. Voila! We have, what we want, the access to the solution. Accounts.item(index) gives us one distinct account, which has the property SmtpAddress, means the mail address of an outlook account. This mail address we will compare with the given sender mail address for sending the mail. What else do we need? The possibility to assign the sender address. So let’s check the mail message class Microsoft.Office.Interop.Outlook.MailItem. There we have property SendUsingAccount to set/assign the sender account. Ok then, let’s do it …

//local variables:olApp DotNet Microsoft.Office.Interop.Outlook.ApplicationClass.'Microsoft.Office.Interop.Outlook, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c'olMailItem DotNet Microsoft.Office.Interop.Outlook.MailItem.'Microsoft.Office.Interop.Outlook, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c'olItemType DotNet Microsoft.Office.Interop.Outlook.OlItemType.'Microsoft.Office.Interop.Outlook, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c'olAccountList DotNet Microsoft.Office.Interop.Outlook.Accounts.'Microsoft.Office.Interop.Outlook, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c'olAccount DotNet Microsoft.Office.Interop.Outlook.Account.'Microsoft.Office.Interop.Outlook, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c'olAttachmentType DotNet Microsoft.Office.Interop.Outlook.OlAttachmentType.'Microsoft.Office.Interop.Outlook, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c'fileName | TextsenderMailAddress | Textidx | Integer

Let’s say, we select the sender address from table “company information”.

senderMailAddress := CompInfo."E-Mail" // e.g. 'sender@test.com';olApp := olApp.ApplicationClass; // creates the outlook instanceolMailItem := olApp.CreateItem(olItemType.olMailItem);  // creates a new outlook mail message// find the selected outlook profile and set it as sender mailAddressolAccountList := olApp.Session.Accounts;idx := 1;REPEAT  olAccount := olAccountList.Item(idx);  IF LOWERCASE(olAccount.SmtpAddress) = LOWERCASE(senderMailAddress) THENolMailItem.SendUsingAccount := olAccount;  idx += 1;UNTIL idx > olAccountList.Count;olMailItem.Subject := 'subject text';olMailItem."To" := 'receiver@test.com';olMailItem.Body := 'This is the message.';fileName := 'c:\temp\test.docx'; // optional: file to attacholMailItem.Attachments.Add(fileName,olAttachmentType.olByValue,1,fileName);olMailItem.Display(TRUE); // Display the outlook window

cheers

Debugging Control Add-ins

If an error occurs, when running a page, where a custom control add-in was added, then it’s often not easy to find the error reason. Typical error message: Exception has been thrown by the target of an invocation.

An expert method to analyse add-ins is using Visual Studio.
For that run visual studio and goto menu “debug”, menu item “attach to process …”. in the following dialog select Microsoft.Dynamics.Nav.Client.exe.

Then start the nav windows client (RTC), open the page, which contains the field with the control-addin and start the according action. After that, the error is shown in visual studio at the exact position, where the error occurs (debug mode).
In most cases there is for sure an unhandled exception. Edit the c# source file, fix the code and add at this position a better exception handling to get a better speaking exception message.

Also helpful for analysing purposes is to develope a logging mechanism, e.g. write eventlog entries.

Encrypt/Decrypt strings in C/AL

Following can be used, if you need a encryption/decryption function in NAV. In this quite simple sample there is shown the usage of the TripleDES encryption algorithm. The base are the cryptograhic function in namespace System.Security.Cryptography of the .net framework.

// local variablesTripleDESEncDotNetSystem.Security.Cryptography.TripleDES.'mscorlib, Version=4.0.0.0, culture=neutral, PublicKeyToken=b77a5c561934e089'cStreamDotNetSystem.Security.Cryptography.CryptoStream.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'cModeDotNetSystem.Security.Cryptography.CryptoStreamMode.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'cEncDotNetSystem.Security.Cryptography.ICryptoTransform.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'TripleDESDecDotNetSystem.Security.Cryptography.TripleDES.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'cDecDotNetSystem.Security.Cryptography.ICryptoTransform.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'cKeyDotNetSystem.Byte.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'initVectorDotNetSystem.Byte.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'fStreamDotNetSystem.IO.FileStream.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'fModeDotNetSystem.IO.FileMode.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'sWriterDotNetSystem.IO.StreamWriter.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'sReaderDotNetSystem.IO.StreamReader.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'sDataText 1024FileNameText 1024fileFile// the codesData := 'Here is some data to encrypt.';FileName := 'c:\temp\CText.txt';// encrypt partfMode := fMode.OpenOrCreate;fStream := fStream.FileStream(FileName,fMode);TripleDESEnc := TripleDESEnc.Create('TripleDES');// create encryptor and internally a random key and a random IVcEnc := TripleDESEnc.CreateEncryptor;cMode := cMode.Write;cStream := cStream.CryptoStream(fStream,cEnc,cMode);sWriter := sWriter.StreamWriter(cStream);sWriter.WriteLine(sData);sWriter.Close;cStream.Close;fStream.Close;file.OPEN(FileName);file.READ(sData);file.CLOSE;// display encrypted stringMESSAGE('Encrypted: ' + sData);// decryptfMode := fMode.OpenOrCreate;fStream := fStream.FileStream(FileName, fMode);TripleDESDec := TripleDESDec.Create;// reuse the key/iv-pair, created abovecDec := TripleDESDec.CreateDecryptor(TripleDESEnc.Key,TripleDESEnc.IV);cMode := cMode.Read;cStream := cStream.CryptoStream(fStream,cDec,cMode);sReader := sReader.StreamReader(cStream);sData := sReader.ReadLine;sReader.Close;cStream.Close;fStream.Close;MESSAGE('Decrypted: ' + sData);

In the sample the same Key/IV pair was used for encryption and decryption, automatically created by method CreateEncryptor. To use your own Key/IV pair use method CreateEncryptor(byte[] key, byte[] iv) or set the Key and IV property of the encryptor/decryptor.

...CreateByteArray(cKey, 'abcdefghijklmnoqprstuvwx'); // length: 24CreateByteArray(initVector, 'abcdefgh'); // length: 8TripleDESEnc.Key(cKey);TripleDESEnc.IV(initVector);cEnc := TripleDESEnc.CreateEncryptor;...CreateByteArray(VAR ByteArray : DotNet "System.Array";InputString : Text)// local variables// idxInteger// ArrayTypeDotNetSystem.Type.'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'// ByteValueByte// StrLengthIntegerStrLength := strlen(InputString);ArrayType := ArrayType.GetType('System.Byte',FALSE);ByteArray := ByteArray.CreateInstance(ArrayType,StrLength);FOR idx := 1 TO StrLength DO BEGIN  ByteValue := InputString[idx];  ByteArray.SetValue(ByteValue,idx - 1);END;

Links:
http://msdn.microsoft.com/de-de/library/z565ef9x(v=vs.110).aspx

Sql Statements in C/AL – Part 2 : Create View

The following code can be used to create a sql view in a given, external database.

// local variablesADOConnection  Automation 'Microsoft ActiveX Data Objects 2.8 Library'.ConnectionADOCommand  Automation 'Microsoft ActiveX Data Objects 2.8 Library'.CommandADORecSet  Automation 'Microsoft ActiveX Data Objects 2.8 Library'.RecordsetconnString  Text 1024activeConnection  VariantviewName  Text 100// the code// replace the server ip and the ext. databasename with your valuesconnString := 'Driver={SQL Server};Server=127.0.0.1;Database=Cronus600';CREATE(ADOConnection);ADOConnection.ConnectionString(connString);ADOConnection.Open;// convert ADO Connection to VariantactiveConnection := ADOConnection;viewName := 'CRONUS$Item View';CREATE(ADOCommand);ADOCommand.ActiveConnection := activeConnection;ADOCommand.CommandType := 1;ADOCommand.CommandText :=  'SELECT * FROM INFORMATION_SCHEMA.TABLES where ' +  '(TABLE_TYPE = ''VIEW'') and (TABLE_NAME =''' + viewName + ''')';ADORecSet := ADOCommand.Execute;IF ADORecSet.EOF = TRUE THEN BEGIN  ADOCommand.CommandText := 'CREATE VIEW [' + viewName + ']' +'AS SELECT No_, Description FROM dbo.[CRONUS$Item]' +'WHERE No_ like ''100%''';  ADOCommand.Execute;  message('view created');END ELSE  MESSAGE('view already exists');ADORecSet.Close;ADOConnection.Close;CLEARALL;

follow also posting “Sql Statements in C/AL – Part 1 : Select”.

cheers

Error, when creating an instance of a .net class

Following error can occur, when creating an instance of a .net class in C/AL:

Reasons:

  • If the assembly is used on client side (property RunOnClient=True), then the assembly was not copied to the local Add-ins Folder of the RTC Client. To copy the assembly into this folder you need in most cases Admin rights. So, check that first.
  • If the assembly is used on server side (property RunOnClient=False), then there might be a version conflict between the assembly file located in the Add-ins Folder of the Nav Service and the used version in the C/AL Code. That can easy occur after installing Updates. So update the code and restart the nav service.
  • Missing System permission: That means, that the assembly was copied from the web or from a computer outside the local network to a target computer/server with windows server OS. Then, the assembly is automatically blocked by the system.

Executing external programs

Sometimes it’s needed to add functionality not delivered by NAV. One way is to use .Net assemblies or automations. Another scenario can be to run an external program in C/AL. One way is to use the quite old SHELL Command or class WshShell of the “Windows Scripting Host” (automation). In both cases, when running an external program, a security warning is displayed … not so good for automatic execution (batch).

Class System.Diagnostics.Process from .Net assembly System.dll
To avoid that and to have also a much prettier solution, use the class System.Diagnostics.Process from the .Net Framework (search for assembly System.dll). For usage in RTC Client set the property RunOnClient to true, for usage on the server only like job queues set the value to false.

Sample: creating a file using the dir command in a dos shell

// variable process | dotnet | System.Diagnostics.Process (assembly System.dll)
process := process.Process;
process.StartInfo.UseShellExecute := FALSE;
process.StartInfo.FileName := ‘cmd.exe’;
process.StartInfo.Arguments := ‘/c dir > c:\temp\dir.txt’;
process.StartInfo.CreateNoWindow := TRUE;
process.Start();
CLEAR(process);

sample: extract a rar file

process := process.Process;
process.StartInfo.UseShellExecute := FALSE;
process.StartInfo.FileName := ‘”C:\Program Files (x86)\WinRAR\unrar.exe”‘;
process.StartInfo.Arguments := ‘e -y -o+ c:\temp\archive.rar c:\temp’;
process.StartInfo.CreateNoWindow := TRUE;
process.Start();
CLEAR(process);

Stop pending processes:
Use methods WaitForExit or Kill to stop pending processes.

Links:
http://msdn.microsoft.com/en-us/library/system.diagnostics.process%28v=vs.110%29.aspx
http://msdn.microsoft.com/en-us/library/dd355282.aspx
http://msdn.microsoft.com/en-us/library/system.diagnostics.process.waitforexit(v=vs.110).aspx

cheers

Convert date string containing a month text value to a date value

Assuming you want to enter date values in the RTC client like 15-MAR-14. that won’t work. date values are only accepted with syntax like 15.03, 15.03.14, etc. But it can be that users prefer the above syntax. so then you can use a text field in your page and convert internally the value into a date value. Following function converts that kind of date string to a date value.

// local variables// dateString | Text// dateValue  | DatedateString := '15-MAR-14';dateValue := ConvertDateString(dateString);MESSAGE(FORMAT(dateValue,0,'<day,2>.<month,2>.<year4>'));ConvertDateString(dateString : Text[9]) : Date// local variables// dateString | Text// dayValue   | Integer// months | Text// monthValue | Integer// yearValue  | Integer// dateValue  | Date// mPos   | Integer// monthText  | Textif strpos(dateString,'-') > 0 then begin  dateString := CONVERTSTR(dateString,'-',',');  EVALUATE(dayValue,SELECTSTR(1,dateString));  months := 'jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec';  monthText := lowercase(SELECTSTR(2,dateString));  if strlen(monthText) <> 3 thenerror('Invalid month expression: ' + monthText);  mPos := STRPOS(months,monthText);  if mPos >0 then beginmonthValue := (mPos + 3) / 4;EVALUATE(yearValue,SELECTSTR(3,dateString));IF yearValue > 14 THEN  yearValue := yearValue + 1900ELSE  yearValue := yearValue + 2000;dateValue := DMY2DATE(dayValue,monthValue,yearValue);exit(dateValue);  end elseerror('No valid month given: ' + monthText);end;exit(0D);

to use the standard functionality of date validation and conversion it’s needed to change function MakeDateText in codeunit 1:

MakeDateText(VAR DateText : Text[250]) : Integer// additional local variables// mPos | Integer// monthText | Text | 10// monthValue | Integer// Text Constant: Text022 | jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,decPosition := 1;Length := STRLEN(DateText);ReadCharacter(' ',DateText,Position,Length); // begin changesIF STRPOS(DateText,'-') > 0 THEN BEGIN  DateText := CONVERTSTR(DateText,'-',',');  monthText := lowercase(SELECTSTR(2,DateText));  if strlen(monthText) <> 3 thenerror('Invalid month expression: ' + monthText);  mPos := STRPOS(Text022,monthText);  IF (mPos > 0) THEN BEGINmonthValue := (mPos + 3) / 4;// optional: add a leading 0, if needed; simple use format(monthValue) should also workmonthText := PADSTR('',2 - STRLEN(FORMAT(monthValue)),'0') + FORMAT(monthValue);DateText := SELECTSTR(1,DateText) + '-' + monthText + '-' + SELECTSTR(3,DateText);  END ELSEerror('No valid month given: ' + monthText);END;// end changes IF NOT FindText(PartOfText,DateText,Position,Length) THEN...

cheers

Process decimal values with different regional settings

When importing/processing decimal values, the decimal separator is . or , according to the regional setting. Assuming you want to import decimal values with US Standard regional setting, then the separator is a point (.).

To process such decimal values, you can work with the NAV Command GLOBALLANGUAGE. That is the in NAV at runtime used language.

impValue := 0.25;impValStr := format(impValue);// check for GER and FR languageif (GLOBALLANGUAGE = 1031) OR (GLOBALLANGUAGE = 1036) then  impValStr := Convertstr(impValStr,'.'','); // replaces the point by a comma

Additional you can use the Command WINDOWSLANGUAGE. This command returns the regional system setting (the language of the OS). In common with that command you can handle every case.

for language codes follow
http://technet.microsoft.com/en-us/library/dd346950.aspx

links:
http://msdn.microsoft.com/en-us/library/dd338772.aspx
http://msdn.microsoft.com/en-us/library/dd301095.aspx