Updates to my Object Renumbering Tool

Back in the end of 2014 I published a renumbering tool for NAV objects.  Using DotNet I was able to increase the renumbering speed for text object files dramatically.

Since then I have been asked if I could upgrade it to work with IDs and Field Numbers.

Now I have.

What’s more, it is also on GitHub.

The Process functions are the following;

  • Read Object Lines – Creates renumbering lines base on the objects in the selected object file.
  • Suggest IDs – Suggest new object numbers in the range from 50.000 based on the available objects in the current license.
  • Read from Excel – Reads object renumbering lines from Excel Sheet created with the Write to Excel process.
  • Write to Excel – Writes current renumbering lines to a new Excel Sheet to me managed within Excel and reread into the renumbering lines.
  • Renumber Using Lines – Prompts for a file to read and for a new file to save with renumbered objects based on the rules in the renumbering lines.
  • Renumber Using Controls – Prompts for a file to read and for a new file to save with renumbered objects based on the rules in the control IDs setup.

I have done some fixes to the renumbering function and have added support for the EventSubscriber.

Go to GitHub to download Page and Table 50000, try this out and submit improvements.

When I am processing an object file I have it open in my text editor.  When I see something to renumber I update the control ranges and execute the renumbering process, reading and writing to the same object file.  My editor will reload the file and I can see the results immediately.

 

Inspired by a Microsoft Developer

Yes it happens.

When reading the C/AL code in NAV written by other developers you normally pick up smart way to do things.  Yesterday I got one.

In my solutions I have been using a function to check if a string is numeric and another function to extract the numeric value from a string.

IsNumeric

All good and well.  Working fine so far.  But if you can write each function with a single line, would that not be better ?

NewIsNumeric

Thanks for the inspiration Microsoft.

Potential hole in handling default dimension

Since NAV 2013 we have been using Dimension Set instead of the Dimension Entry tables.  One of the function that was changed was GetDefaultDimID in Codeunit 408.

In NAV 2009 and older this would return a set of dimension for any of the dimension entry tables.  In NAV 2013 and newer this will return the Dimension Set Id and updated Global Dimension Codes.

Here; “updated” is the key word.  One could think that when a function is returning these codes you should be able to trust that it does.  If we look at the CreateDim function in the Sales Line table the code is

[code]
SourceCodeSetup.GET;
TableID[1] := Type1;
No[1] := No1;
TableID[2] := Type2;
No[2] := No2;
TableID[3] := Type3;
No[3] := No3;
"Shortcut Dimension 1 Code" := ”;
"Shortcut Dimension 2 Code" := ”;
GetSalesHeader;
"Dimension Set ID" :=
DimMgt.GetDefaultDimID(
TableID,No,SourceCodeSetup.Sales,
"Shortcut Dimension 1 Code","Shortcut Dimension 2 Code",
SalesHeader."Dimension Set ID",DATABASE::Customer);
DimMgt.UpdateGlobalDimFromDimSetID("Dimension Set ID","Shortcut Dimension 1 Code","Shortcut Dimension 2 Code");[/code]

But a better version might be

[code]
SourceCodeSetup.GET;
TableID[1] := Type1;
No[1] := No1;
TableID[2] := Type2;
No[2] := No2;
TableID[3] := Type3;
No[3] := No3;
GetSalesHeader;
"Shortcut Dimension 1 Code" := SalesHeader."Shortcut Dimension 1 Code";
"Shortcut Dimension 2 Code" := SalesHeader."Shortcut Dimension 2 Code";
"Dimension Set ID" :=
DimMgt.GetDefaultDimID(
TableID,No,SourceCodeSetup.Sales,
"Shortcut Dimension 1 Code","Shortcut Dimension 2 Code",
SalesHeader."Dimension Set ID",DATABASE::Customer);[/code]

The reason is that the function DimMgt.GetDefaultDimID will update the “Shortcut Dimension 1 Code” and “Shortcut Dimension 2 Code” only if they are changed. If the Dimension Set used in the Sales Header contains dimension values for “Shortcut Dimension 1 Code” or “Shortcut Dimension 2 Code” we need to add the DimMgt.UpdateGlobalDimFromDimSetID function to make sure that all the correct data is in place.

The lesson is; if using InheritFromDimSetID in the function DimMgt.GetDefaultDimID then make sure it is followed by calling DimMgt.UpdateGlobalDimFromDimSetID.

I have however suggested to Microsoft that they make a change to the DimMgt.GetDefaultDimID function to close this hole. The original version is

[code]
GetGLSetup;
IF InheritFromDimSetID > 0 THEN
GetDimensionSet(TempDimSetEntry0,InheritFromDimSetID);
TempDimBuf2.RESET;
TempDimBuf2.DELETEALL;
IF TempDimSetEntry0.FINDSET THEN
REPEAT
TempDimBuf2.INIT;
TempDimBuf2."Table ID" := InheritFromTableNo;
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Code" := TempDimSetEntry0."Dimension Code";
TempDimBuf2."Dimension Value Code" := TempDimSetEntry0."Dimension Value Code";
TempDimBuf2.INSERT;
UNTIL TempDimSetEntry0.NEXT = 0;

NoFilter[2] := ”;
FOR i := 1 TO ARRAYLEN(TableID) DO BEGIN
IF (TableID[i] <> 0) AND (No[i] <> ”) THEN BEGIN
DefaultDim.SETRANGE("Table ID",TableID[i]);
NoFilter[1] := No[i];
FOR j := 1 TO 2 DO BEGIN
DefaultDim.SETRANGE("No.",NoFilter[j]);
IF DefaultDim.FINDSET THEN
REPEAT
IF DefaultDim."Dimension Value Code" <> ” THEN BEGIN
TempDimBuf2.SETRANGE("Dimension Code",DefaultDim."Dimension Code");
IF NOT TempDimBuf2.FINDFIRST THEN BEGIN
TempDimBuf2.INIT;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Code" := DefaultDim."Dimension Code";
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END ELSE BEGIN
IF DefaultDimPriority1.GET(SourceCode,DefaultDim."Table ID") THEN BEGIN
IF DefaultDimPriority2.GET(SourceCode,TempDimBuf2."Table ID") THEN BEGIN
IF DefaultDimPriority1.Priority < DefaultDimPriority2.Priority THEN BEGIN
TempDimBuf2.DELETE;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END;
END ELSE BEGIN
TempDimBuf2.DELETE;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END;
END;
END;
IF GLSetupShortcutDimCode[1] = TempDimBuf2."Dimension Code" THEN
GlobalDim1Code := TempDimBuf2."Dimension Value Code";
IF GLSetupShortcutDimCode[2] = TempDimBuf2."Dimension Code" THEN
GlobalDim2Code := TempDimBuf2."Dimension Value Code";
END;
UNTIL DefaultDim.NEXT = 0;
END;
END;
END;
TempDimBuf2.RESET;
IF TempDimBuf2.FINDSET THEN BEGIN
REPEAT
DimVal.GET(TempDimBuf2."Dimension Code",TempDimBuf2."Dimension Value Code");
TempDimSetEntry."Dimension Code" := TempDimBuf2."Dimension Code";
TempDimSetEntry."Dimension Value Code" := TempDimBuf2."Dimension Value Code";
TempDimSetEntry."Dimension Value ID" := DimVal."Dimension Value ID";
TempDimSetEntry.INSERT;
UNTIL TempDimBuf2.NEXT = 0;
NewDimSetID := GetDimensionSetID(TempDimSetEntry);
END;
EXIT(NewDimSetID);[/code]

and the modified would be

[code]
GetGLSetup;
IF InheritFromDimSetID > 0 THEN
GetDimensionSet(TempDimSetEntry0,InheritFromDimSetID);
TempDimBuf2.RESET;
TempDimBuf2.DELETEALL;
IF TempDimSetEntry0.FINDSET THEN
REPEAT
TempDimBuf2.INIT;
TempDimBuf2."Table ID" := InheritFromTableNo;
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Code" := TempDimSetEntry0."Dimension Code";
TempDimBuf2."Dimension Value Code" := TempDimSetEntry0."Dimension Value Code";
TempDimBuf2.INSERT;
UNTIL TempDimSetEntry0.NEXT = 0;

NoFilter[2] := ”;
FOR i := 1 TO ARRAYLEN(TableID) DO BEGIN
IF (TableID[i] <> 0) AND (No[i] <> ”) THEN BEGIN
DefaultDim.SETRANGE("Table ID",TableID[i]);
NoFilter[1] := No[i];
FOR j := 1 TO 2 DO BEGIN
DefaultDim.SETRANGE("No.",NoFilter[j]);
IF DefaultDim.FINDSET THEN
REPEAT
IF DefaultDim."Dimension Value Code" <> ” THEN BEGIN
TempDimBuf2.SETRANGE("Dimension Code",DefaultDim."Dimension Code");
IF NOT TempDimBuf2.FINDFIRST THEN BEGIN
TempDimBuf2.INIT;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Code" := DefaultDim."Dimension Code";
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END ELSE BEGIN
IF DefaultDimPriority1.GET(SourceCode,DefaultDim."Table ID") THEN BEGIN
IF DefaultDimPriority2.GET(SourceCode,TempDimBuf2."Table ID") THEN BEGIN
IF DefaultDimPriority1.Priority < DefaultDimPriority2.Priority THEN BEGIN
TempDimBuf2.DELETE;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END;
END ELSE BEGIN
TempDimBuf2.DELETE;
TempDimBuf2."Table ID" := DefaultDim."Table ID";
TempDimBuf2."Entry No." := 0;
TempDimBuf2."Dimension Value Code" := DefaultDim."Dimension Value Code";
TempDimBuf2.INSERT;
END;
END;
END;
END;
UNTIL DefaultDim.NEXT = 0;
END;
END;
END;
TempDimBuf2.RESET;
IF TempDimBuf2.FINDSET THEN BEGIN
REPEAT
IF GLSetupShortcutDimCode[1] = TempDimBuf2."Dimension Code" THEN
GlobalDim1Code := TempDimBuf2."Dimension Value Code";
IF GLSetupShortcutDimCode[2] = TempDimBuf2."Dimension Code" THEN
GlobalDim2Code := TempDimBuf2."Dimension Value Code";
DimVal.GET(TempDimBuf2."Dimension Code",TempDimBuf2."Dimension Value Code");
TempDimSetEntry."Dimension Code" := TempDimBuf2."Dimension Code";
TempDimSetEntry."Dimension Value Code" := TempDimBuf2."Dimension Value Code";
TempDimSetEntry."Dimension Value ID" := DimVal."Dimension Value ID";
TempDimSetEntry.INSERT;
UNTIL TempDimBuf2.NEXT = 0;
NewDimSetID := GetDimensionSetID(TempDimSetEntry);
END;
EXIT(NewDimSetID);[/code]

Where I have moved the lines that update GlobalDim1Code and GlobalDim2Code to the loop in the end of the function. By doing this the call to DimMgt.UpdateGlobalDimFromDimSetID is no longer needed and that extra loop through the Dimension Set Entries will save some time.

Run a Table in NAV

One of the things we developers and consultants miss the most from the “Classic Times” is the ability to run a table to edit the data. Sure we can run a table from the Developement Environment but we are not always working with direct access to the SQL database.

My solution is to have a Page running on the object table (Table2000000001). From the page I can start Pages, Reports, Codeunits and XML Ports directly with a simple line of code, but to start a Table is more complex.

The first solution was to use HYPERLINK on the result from the GETURL function. That works fine in a local environment, but when you have installed multiple Dynamics NAV versions or running Dynamics NAV from a ClickOnce installation things start to break.

To fix this I stop using HYPERLINK and start the Dynamics NAV client with arguments to run a table. First step is to find the current client path. This path can be the usual System Drive path but it can also be a User Application Path if using ClickOnce. The function to locate the client path uses DotNet.

[code] PROCEDURE GetClientPath@1100408003() : Text;
VAR
ClientAssembly@1100408001 : DotNet "’mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Reflection.Assembly" RUNONCLIENT;
ClientPath@1100408000 : DotNet "’mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.IO.Path" RUNONCLIENT;
BEGIN
ClientAssembly := ClientAssembly.GetExecutingAssembly;
EXIT(ClientPath.GetDirectoryName(ClientAssembly.Location));
END;[/code]

And based on the client Path I look for the ClientConfiguration.config file and start the client with the GETURL results.

[code] LOCAL PROCEDURE ViewRecords@1100408000();
VAR
AddinMgt@1100408000 : Codeunit 10000207;
PathHelper@1100408008 : DotNet "’mscorlib’.System.IO.Path";
ClientFileHelper@1100408007 : DotNet "’mscorlib’.System.IO.File" RUNONCLIENT;
ClientProcess@1100408005 : DotNet "’System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Diagnostics.Process" RUNONCLIENT;
ClientProcessWindowStyle@1100408004 : DotNet "’System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Diagnostics.ProcessWindowStyle" RUNONCLIENT;
ClientProcessStartInfo@1100408003 : DotNet "’System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Diagnostics.ProcessStartInfo" RUNONCLIENT;
StartCommand@1100408002 : Text;
ArgumentList@1100408006 : Text;
ClientPath@1100408001 : Text;
ClientConfigurationPath@1100408009 : Text;
BEGIN
ClientPath := AddinMgt.GetClientPath;
ClientConfigurationPath := PathHelper.Combine(ClientPath,’ClientUserSettings.config’);
IF ClientFileHelper.Exists(ClientConfigurationPath) THEN
ArgumentList := STRSUBSTNO(‘-settings:"%1" ‘,ClientConfigurationPath);
ArgumentList += GETURL(CLIENTTYPE::Windows, COMPANYNAME, OBJECTTYPE::Table, ID);
ClientPath := PathHelper.Combine(ClientPath,’Microsoft.Dynamics.Nav.Client.exe’);
ClientProcessStartInfo := ClientProcessStartInfo.ProcessStartInfo(ClientPath);
ClientProcessStartInfo.Arguments := ArgumentList;
ClientProcessStartInfo.WindowStyle := ClientProcessWindowStyle.Normal;
ClientProcess := ClientProcess.Start(ClientProcessStartInfo);
END;[/code]

To repeat my last comment, just use DotNet 🙂

User friendly error messages in NAV

One of the most challenging thing when programming is to have proper error handling. This is one of the weakness of the standard NAV code. We are now seeing this change when Microsoft introduced the simplified NAV.

An example of this can be found in report 1306.

Rep1306

Here Microsoft have created a dedicated function for error handling.

CompInfoErrorHandling

This type of error handling will tell the user what the problem is and how to fix that problem.

ErrorHandlingQuestion

The flow is; when printing the invoice NAV checks to see if payment information has been entered into the Company Information table. If not a question will be asked if the user wants to update the payment information. If the user agrees then the Company Information page will be displayed and the invoice printing will continue after that page is closed.

I wanted to adapt to this method – perhaps a new error handling pattern – when I write an add-on or a customization.  Here is an video from Mark Brummel in line with this new pattern.

I have added this kind of error handling but in some cases a little bit more is required.  As an example, I am doing a series of tests when the Payment Method Code is validated in a Sales Header.  The problem here is that NAV is in the middle of a transaction when I have that question to ask.

To solve this I use a function that is intended for testing only, so using it is not really supported by Microsoft. Perhaps, if they read this blog they will see a useful application for this function in the standard solution.

AssertErrorTest

Here I have two options for you to consider. Doing an ASSERTERROR ERROR(”) will rollback the transaction without stopping the code execution. By doing this before the CONFIRM question I make sure that the users will not be blocking the database while deciding what to do.

FixNow

Pressing Yes will open the page.

ReportSelection

After fixing this error the user can retry the previous action.  I will most likely use a code like this.

RepairReportSelection

Use this carefully.

Selection Filter to Clipboard

I have now seen three posts about using Excel to help create a Pipe Filter.  The latest one from Mohana is similar to the way I normally do this.  His post is a followup from the original video from Kerry Rosvold and an alternative from Mark Brummel.

If this is something you frequently need to do, why don’t use NAV and skip the Excel part ?

Lets imagine how that could look like.

CustomerList

We add a button the ribbon, select what ever we like from the customer list and click the button.

Clipboard

Now it just a matter of using the filter. Paste it where ever you need it.

FilteredCustomerList

And this is with just a few lines of code.

CustomerListModification

Page21 Delta file

Insert network printers automatically

In my current upgrade project I need to upgrade a solution I did for Classic NAV where I used the “‘Windows Script Host Object Model’.WshNetwork” Automation object to add a network printer automatically.

I looked a round and did not find any dotnet object capable of adding a network printer.  However, there was a c# code I found that was able to do this.  So, I created a NAV Add-in with this code

[code lang=”csharp”]using System;
using System.Runtime.InteropServices;

namespace NAVPrinterAdd_in
{
public static class PrinterControls
{
[DllImport("winspool.drv")]
static extern bool AddPrinterConnection(string pName);

public static bool AddNetworkPrinter(string networkPrinterPath, ref string networkMessage)
{
bool result;
try
{
result = AddPrinterConnection(networkPrinterPath);
networkMessage = "";
}
catch (Exception e)
{
result = false;
networkMessage = e.Message;
}
return result;
}
}
}

[/code]

The Add-in is attached. Next I created a Codeunit in NAV.

AddPrinter

And used in Codeunit 1, FindPrinter trigger.

codeunit1

Now I can make sure that the correct printer is installed for the client and also for the server when printing from NAS session.

NAVPrinterAdd-in

Browse for folder dialog

I have in several cases needed to allow a user to select a folder.  I was surprised to see that Microsoft did not have a browse for folder function in Codeunit 419.

I hereby suggest that Microsoft add this function to Codeunit 419.

[code] PROCEDURE BrowseForFolder@47(VAR FolderName@1000 : Text;Description@1001 : Text) : Boolean;
VAR
FolderBrowserDialog@1002 : DotNet "’System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Windows.Forms.FolderBrowserDialog" RUNONCLIENT;
DialagResult@1003 : DotNet "’System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089′.System.Windows.Forms.DialogResult" RUNONCLIENT;
BEGIN
FolderBrowserDialog := FolderBrowserDialog.FolderBrowserDialog;
FolderBrowserDialog.Description := Description;
FolderBrowserDialog.SelectedPath := FolderName;
DialagResult := FolderBrowserDialog.ShowDialog;

IF DialagResult.CompareTo(DialagResult.OK) = 0 THEN BEGIN
FolderName := FolderBrowserDialog.SelectedPath;
EXIT(TRUE);
END;
EXIT(FALSE);
END;
[/code]

Please add block possibility to Permission table

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

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

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

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

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

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

Please help me by voting for the suggestion.

A Table Hook to minimize the upgrade work

In a line with the methods introduced by Partner Ready Software I am now upgrading old code to NAV 2013 R2 with the goal to modify the standard objects a little as possible.

I don’t recall Mark, Waldo, Gary or Vjeko speaking about a Table Hook but I decided to go that way anyway.  As an example I have about twenty new field for the Customer Table.  Instead of creating these fields in table 18 I create a new table that I call Customer Hook.  The Hook Table must include the fields from the primary key and the primary key it self from the Master Table.

CustomerHook

The only modification I do in table 18 is a new function

GetHook

In a code where I need to access the new custom fields I use this function to get the Table Hook to work with.

In the Hook table I have a few functions.

HookFunctions

Next I created a Codeunit to hook to the global triggers in Codeunit 1.  This Codeunit forwards the triggers from Codeunit 1 to the Hook Table.

DbHookCodeunit

I need to add four lines to Codeunit 1.

Codeunit1Changes

When all this is ready I am able to create or extend the Customer Page.  Here it is easy to mix together fields from the Customer Table and the Customer Hook Table.

PageFields

For the fields from the Hook Table I can add a Table Relation and a Validate code.

CodeValidate

On the Page I create a local function

SaveFunction

and finally make sure that I get the Hook table matching the Customer on the Page.

GetHookOnPage