Technical upgrade from NAV 2013 R2 to NAV 2015

I just made a backup of a live NAV 2013 R2 database to do an upgrade to NAV 2015.  Since the system is live we have several servers running when the backup is created.

The backup was restored in a new environment and the database opened with a NAV 2015 Developement Client.  The database is put in single user mode and converted.  After the conversion a process to upgrade objects starts.  Here is where things start to behave in an unwanted way.

chooseInstance

The database is in a Single User Mode since it is still in the upgrade transaction and no NAV Server instance is connected.  However, in the table “Server Instance” all the old NAV Server instances are listed.  To get through this I needed to press N like a thousand times…

In a test upgrade process like this we need to add another step to the usual upgrade procedure.  After the restore is completed make sure that the tables “Server Instance” and “Active Session” are empty.

After a restore to a new environment it is also good to clean unneeded users from the database.  Only leave what is actually needed.

users

In this case the NAVLIGHT\srvNAV is the service user.  Also make sure that the Compatibility Level is as new as possible and the Recovery Model in line with the backup process.

dbproperty

Also, when moving from one environment to another the users connected to the old domain are obsolete.  Going to Users and trying to remove them results in an error and the user is only disabled.

userdelete

In some cases this could be enough.  It is possible to completely remove the user, just do some housekeeping first.  Find the user in “User Personalization” and Edit the record.  On the Action ribbon we can clean some of the user trails.

clearpersonalization

Finally make sure that the record is deleted from “User Personalization”.

After this cleanup work the user can be safely deleted.

 

Waiting for a server warmup – no more

One of the problems NAV users face is that the client is to slow after the NAV service starts.

The reason for this is that NAV uses just-in-time compilation of the source code.  The source code is compiled on the server when the server needs to use it.

If you want the server to be warm when the demoing NAV you need to make sure that the server compiles all needed code before showing off.

On the Microsoft Dynamics NAV 2015 Azure Demo Template you can see that Microsoft has added a WarmUp script to take care of this problem.  This solution includes a solution that will open a list of pages with the web client to make sure that the server compiles the basic functionality for the demo.

If you want to play with this yourself on your own installation just download the zipped warmup folder (WarmupNAV) and start investigating.

The function call was ambiguous. No matching method was found.

One of the problems with using DotNet in C/AL is that sometimes the C/AL compiled does not have enough information for select between different methods in the DotNet object.

As you might have gathered from my last posts, I am using DotNet more and more.  It saves a lot of code writing and it is fast.

Today I was handling a web service that is delivering decimal value formatted with the is-IS method.  The decimal character is a comma.  I wanted to make sure that my C/AL code would be able to handle that no matter what the underlying language or region settings where.

Getting the decimal value was pretty straight forward.  Just used the method Microsoft uses in Codeunit 10 with a predefined is-IS.  Now for the other way around.  Found a method for the System.String to format a decimal to a text based on the same is-IS.

StringFormat1

But here comes the problem.

StringFormatError

Ok, Vjeko sais everything can be solved with reflection – well perhaps not everything, but this problem sure can.

I listed all the methods for the System.String and found the one I wanted to use.  Did not have to use the System.Activator since this is a static class.

StringWithReflection

Yes, a few more lines but they are effective and there is always fun to get challanged.  So, if you ever get this error message again, reflection should be able to solve the problem.

here is the ISLFormatAndEvaluate code to download.

Apply a Cumulative Update with Powershell

Microsoft has changed the way they ship Cumulative Updates.  Now we download the whole DVD image along with the application changes.

This post is intended as additional information for New Developer Tools for Dynamics NAV 2013 R2 and Install Client and Server update with PowerShell posts.

To update a code that is built on a RTM version or an older CU version I use the Merge-NAVToNewCU script package.  I create a temporary folder and export three set of objects.  One file for the original unchanged objects (38457Objects.txt), One file for the new CU objects (40938Objects.txt) and finally the modified version (38457customizedObjects.txt).  When I export I normally skip the MenuSuites that I don’t have permission to import.

Microsoft sometimes adds new fields to tables, fields that are outside of my permission to insert.  Therefore, when I have completed the compare and have my new customized object (40938customizedObjects.txt) I start by importing the object fob file directly from the Microsoft Dynamics NAV Application folder found in the CU package.  I accept the default action, replace most objects and merge tables.  This will create all the new fields needed by the update.  After I am able to import the new customized objects text file and compile.

[code lang=”powershell”]$rootFolderName = $PSScriptRoot
$oldVersion = ‘38457’
$newVersion = ‘40938’

Import-Module "${env:ProgramFiles(x86)}\Microsoft Dynamics NAV\80\RoleTailored Client\Microsoft.Dynamics.Nav.Model.Tools.psd1" -force
Import-Module (Join-Path $rootFolderName ‘Merge-NAVVersionListString script.ps1’) -force

$diffFolderName = (Join-Path $rootFolderName ($oldVersion + ‘to’ + $newVersion + ‘diff’))
$oldObjects = (Join-Path $rootFolderName ($oldVersion + ‘objects.txt’))
$newObjects = (Join-Path $rootFolderName ($newVersion + ‘objects.txt’))
$newFolder = (Join-Path $rootFolderName ($newVersion + ‘update’))
$customObjects = (Join-Path $rootFolderName ($oldVersion + ‘customizedobjects.txt’))
$newCustomizedObjects = (Join-Path $rootFolderName ($newVersion + ‘customizedobjects.txt’))
$newCustomizedFolder = (Join-Path $rootFolderName ($newVersion + ‘customized’))
if (!(Test-Path $diffFolderName))
{
mkdir $diffFolderName
}
if (!(Test-Path $newFolder))
{
mkdir $newFolder
}
if (!(Test-Path $newCustomizedFolder))
{
mkdir $newCustomizedFolder
}
Write-Host "Comparing customized and original…"
Compare-NAVApplicationObject -Original $oldObjects -Modified $customObjects -Delta $diffFolderName | Where-Object CompareResult -eq ‘Identical’ | foreach { Remove-Item (Join-Path $diffFolderName ($_.ObjectType.substring(0,3) + $_.Id + ‘.delta’)) }
Write-Host "Splitting new objects…"
Split-NAVApplicationObjectFile $newObjects $newFolder
Write-Host "Removing unchanged new objects…"
Get-ChildItem -Path $newFolder | foreach { if (!(Test-Path ((Join-Path $diffFolderName $_.BaseName) + ‘.delta’))) { Remove-Item $_.FullName } }
Write-Host "Updating new objects…"
Update-NAVApplicationObject -Target $newFolder -Delta $diffFolderName -Result $newCustomizedFolder -DateTimeProperty FromModified -ModifiedProperty FromModified -VersionListProperty FromModified -DocumentationConflict ModifiedFirst
Write-Host "Updating customized object version list…"
Get-ChildItem -Path (Join-Path $newCustomizedFolder ‘*.txt’)| foreach { if (Test-Path (Join-Path $newFolder $_.Name)) {Set-NAVApplicationObjectProperty -Target $_.FullName -VersionListProperty (Merge-NAVVersionListString -source (Get-NAVApplicationObjectProperty -Source $_.FullName).VersionList -target (Get-NAVApplicationObjectProperty -Source (Join-Path $newFolder $_.Name)).VersionList) }}
Write-Host "Joining customized object to a single file…"
Join-NAVApplicationObjectFile -Source (Join-Path $newCustomizedFolder ‘*.txt’) -Destination $newCustomizedObjects
Write-Host "If you have conflicts then you need to manually fix conflicting code changes"
[/code]

I have also created a set of scripts to update the Binaries.  For some time now Microsoft has not shipped the binary upgrade folders with the CU package.  Now they deliver the whole DVD, including the new updated Demo Database.  The scripts therefore must copy the files from the DVD instead of copying from the binary upgrade folders.

When copying from the DVD we must make sure that we don’t overwrite the configuration files for the server and the web client. This is the server update script. All server on the computer will be stopped, updated and restarted.

[code lang=”powershell”]$NAVDVDFilePath = ‘\\STORAGE\NAV 2015\NAV.8.0.40938.IS.DVD’
$NotToCopy = @(‘Tenants.config’,’CustomSettings.config’)
Write-Verbose "Copying NAV Server Update…"
$ClientKBFolder = Join-Path $NAVDVDFilePath ‘ServiceTier\program files\Microsoft Dynamics NAV\80\Service’
$navInstallationDirectory = Join-Path ${env:ProgramFiles} ‘Microsoft Dynamics NAV\80\Service’
if (Test-Path $navInstallationDirectory)
{
Import-Module (Join-Path $ClientKBFolder ‘Microsoft.Dynamics.Nav.Management.dll’) -DisableNameChecking | Out-Null
$RunningInstances = Get-NAVServerInstance | Where-Object { $_.State -eq "Running" }
Write-Verbose "Stopping Server Instances…"
Get-NAVServerInstance | Where-Object { $_.State -eq "Running" } | Set-NAVServerInstance -Stop
Start-Sleep -s 5

Write-Verbose "Running file copy command…"
Get-ChildItem -Path $ClientKBFolder | % {Copy-Item $_.FullName $navInstallationDirectory -Recurse -Force -Exclude $NotToCopy}

Write-Verbose "Done updating files…"
foreach ($RunningInstance in $RunningInstances)
{
$InstanceName = $RunningInstance.ServerInstance.ToString()
Write-Verbose "Starting Server Instance $InstanceName"
Get-NAVServerInstance -ServerInstance $InstanceName | Set-NAVServerInstance -Start
}
}[/code]

This is the web server update script

[code lang=”powershell”]$NAVDVDFilePath = ‘\\STORAGE\NAV 2015\NAV.8.0.40938.IS.DVD’
$NotToCopy = (‘web.config’,’instanceweb.config’,’Header.png’,’About.png’,’Splash.png’)
function Copy-LatestFile
{
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[string]$SourceDirectoryPath,
[parameter(Mandatory=$true)]
[string]$DestinationDirectoryPath
)
Write-Verbose "Running file copy command…"
$sourcefiles = Get-ChildItem $SourceDirectoryPath -Recurse
$destfiles = Get-ChildItem $DestinationDirectoryPath -Recurse
foreach ($sourcefile in $sourcefiles)
{
$copyfile = $false
foreach($destfile in $destfiles)
{
if ($destfile.Name -eq $sourcefile.Name)
{
$copyfile = $true
break
}
}
if (!($copyfile) -or ($NotToCopy -match $sourcefile.BaseName))
{
write-verbose "not copying $sourcefile…"
}
else
{
write-verbose "copying $sourcefile…"
Copy-Item $sourcefile.FullName $destfile.FullName -Force
}
}
}
Write-Verbose "Copying Web Client Update…"
$ClientKBFolder = Join-Path $NAVDVDFilePath ‘WebClient\Microsoft Dynamics NAV\80\Web Client’
$navInstallationDirectory = Join-Path ${env:ProgramFiles} ‘Microsoft Dynamics NAV\80\Web Client’
if (Test-Path $navInstallationDirectory)
{
Copy-LatestFile -SourceDirectoryPath $ClientKBFolder -DestinationDirectoryPath $navInstallationDirectory
}
[/code]

And finally the client update script.

[code lang=”powershell”]$NAVDVDFilePath = ‘\\STORAGE\NAV 2015\NAV.8.0.40938.IS.DVD’
$NotToCopy = (‘Header.png’,’About.png’,’Splash.png’)
function Copy-LatestFile
{
[CmdletBinding()]
param (
[parameter(Mandatory=$true)]
[string]$SourceDirectoryPath,
[parameter(Mandatory=$true)]
[string]$DestinationDirectoryPath
)

Write-Verbose "Running file copy command…"
$sourcefiles = Get-ChildItem $SourceDirectoryPath -Recurse -File
$destfiles = Get-ChildItem $DestinationDirectoryPath -Recurse -File

foreach ($sourcefile in $sourcefiles)
{
$copyfile = $false
foreach($destfile in $destfiles)
{
if ($destfile.Name -eq $sourcefile.Name)
{
$copyfile = $true
break
}
}
if (!($copyfile) -or ($NotToCopy -match $sourcefile.BaseName))
{
write-verbose "not copying $sourcefile…"
}
else
{
write-verbose "copying $sourcefile…"
Copy-Item $sourcefile.FullName $destfile.FullName -Force
}
}
}

Write-Verbose "Copying RTC Update…"
$ClientKBFolder = Join-Path $NAVDVDFilePath ‘RoleTailoredClient\program files\Microsoft Dynamics NAV\80\RoleTailored Client’
$navInstallationDirectory = Join-Path ${env:ProgramFiles(x86)} ‘Microsoft Dynamics NAV\80\RoleTailored Client’
if (Test-Path $navInstallationDirectory)
{
Copy-LatestFile -SourceDirectoryPath $ClientKBFolder -DestinationDirectoryPath $navInstallationDirectory
}
Write-Verbose "Copying Office 14 Update…"
$ClientKBFolder = Join-Path $NAVDVDFilePath ‘Outlook\program files\Microsoft Dynamics NAV\80\OutlookAddin’
$navInstallationDirectory = Join-Path ${env:ProgramFiles(x86)} ‘Microsoft Office\Office14’
if (Test-Path $navInstallationDirectory)
{
Copy-LatestFile -SourceDirectoryPath $ClientKBFolder -DestinationDirectoryPath $navInstallationDirectory
}
Write-Verbose "Copying Office 15 Update…"
$ClientKBFolder = Join-Path $NAVDVDFilePath ‘Outlook\program files\Microsoft Dynamics NAV\80\OutlookAddin’
$navInstallationDirectory = Join-Path ${env:ProgramFiles(x86)} ‘Microsoft Office\Office15’
if (Test-Path $navInstallationDirectory)
{
Copy-LatestFile -SourceDirectoryPath $ClientKBFolder -DestinationDirectoryPath $navInstallationDirectory
}
[/code]

The ClickOnce distribution is always based on the client folder and the script in Install Client and Server update with PowerShell can be used for that purpose.

I am using these binary update scripts in the Instance and Tenant Administration tool and I normally maintain the ClickOnce distribution from there.

ServerManagement

 

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.

Sharing a NAV Web Client server

A part of the NAV 2015 Web Client is to be able to use part of the host header to decide wich tenant to connect to.  For example the domain name kappi.example.com will look for that Alternate Id in the tenant registration to find the correct tenant Id to connect to.  This requires a small change in the WebClient web.config file.

This week I was installing a new NAV environment on Azure.  I wanted to be able to use the Remote App to access and manage the server machines and since I can’t create a NAT rule to use more than one machine for a single port I must install the Remote Desktop Gateway and the Remote Desktop Web Access on that same Web Server.

I use a different host name for the Remote Desktop deployment and by defining the host header in the Default Site binding I make sure that all my Remote Desktop Services are working correctly.

In the Web Client binding I clear the host header to make sure that everything else is directed to NAV.

When starting up the Web Client everything looks fine until the NAV Role Center starts.  Then I get the error “Communication with the server failed, and the content cannot be displayed. Refresh the page or open a new browser window.” and everything stops.

CommunicationFailed

To fix this I needed to update the web.config file in the WebClient folder.  There I found the line

<serviceHostingEnvironment aspNetCompatibilityEnabled=”true” />

and modified to

<serviceHostingEnvironment aspNetCompatibilityEnabled=”true” multipleSiteBindingsEnabled=”true” />

Now everything works fine.

 

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 🙂

Just use DotNet

Day by day I am moving closer to DotNet programming in Dynamics NAV. More and more of the things I like to do are more easily solved with DotNet than with native C/AL code.

In most cases I can use the standard DotNet types but in some cases I need to build a small DotNet Class to solve the problem.

An example of this landed on my desk yesterday. A colleague of mine needed to be able to print text to the local label printer. Perhaps this can be solved with a simple report but in this case something more was needed. I asked the Internet – how do I print a text file with c# code. Got some answers and selected the one I liked the most.

Normally I rewrite the c# code with DotNet objects in C/AL but in this case I could not. The reason was that I needed the DotNet objects to be executed on the client side, and using DotNet to print requires an event handler. DotNet events are not supported on the client side so I needed to create a class.

I create a c# Class Project in Visual Studio and can use the code I found with only a few modifications.

[code lang=”csharp”] public class NAVTextFilePrinter
{
public void PrintText(string printerName, string fontName, float fontSize, string[] linesToPrint)
{
Font printFont = new Font(fontName, fontSize);
PrintDocument docToPrint = new PrintDocument();
docToPrint.DocumentName = "NAV Text File";
docToPrint.PrinterSettings.PrinterName = printerName;
docToPrint.PrintPage += (s, ev) =>
{
int count = 0;
float yPos = 0;
float leftMargin = ev.MarginBounds.Left;
float topMargin = ev.MarginBounds.Top;

foreach (string line in linesToPrint)
{
yPos = topMargin + (count * printFont.GetHeight(ev.Graphics));
ev.Graphics.DrawString(line, printFont, Brushes.Black, leftMargin, yPos, new StringFormat());
count++;
}
ev.HasMorePages = false;
};
docToPrint.Print();
}
}
[/code]

The class is compiled with DotNet 4.5 and added to the Client Add-ins folder. If you are using NAV version 2015 or newer just add it to the Server Add-ins folder.

Using this in NAV is easy

PrintNAVText

Microsoft.Dynamics.Nav.Client.TextFilePrinter can be downloaded from here.

I like to use DotNet when I am handling text – in general. The DotNet object System.String has a lot of functions that can be useful. Just use

String := String.Copy(NAVString);

and your NAV String can now be handled with all the functionality available with DotNet.  Good example is the Renumbering Tool I created a few months ago.

Another example here – where I need to create all the directory tree leading to the file I need to copy.

CreateDirectoryTree

One of my favorite is the Global Variable Store Codeunit, where I use the DotNet Dictionary to store variables globally. This can be used to minimize the footprint of your code changes. For example, if you need to pass a new variable to a function and you don’t want to change the function – just store the variable before you call the function and retrieve it inside the function. It is even possible to pass a whole record this way.

Downloadable Global Variable Store Codeunit

 

Clean up your trail

Boy, have these last months been busy.

Now I am working on a management solution for our cloud offering.  This solution is going to give the control of the services and the tenants to a NAV user interface.  I am running Powershell scripts from NAV (thanks Waldo) and things are looking good so far.

I extended the Powershell functionality to be able to read an XML response like Waldo describes here.

One of the things I need to do is to maintain files for this management solution.  For example a NAVData file, SQL backup file and a NAV license file.  When ever I execute a Powershell script from NAV I write these files to a temporary file path and point Powershell over there.

I can’t be sure that every Powershell execution is a success and I can’t leave the temporary files around.  I must delete them.  The solution in my case is to create a single instance Codeunit and apply a DotNet List object.

I only needed a server version, but with DotNet this can be extended to handle the client side as well.

VariableStore

 

So, every time I create a temporary file I add the file to a DotNet List.  Even if the execution fails the temporary file is still in the list.  When the execution finishes all the listed temporary files are removed.  If the execution fails then the next successful execution will also remove the previous temporary file.

As you can imagine this type of a Codeunit can be used in many scenarios.  By using DotNet Dictionary it is easy to store parameters with names in one place and retrieve them in another.

For example if you need to add a parameter to a standard function, then adding the parameter to the Dictionary before you execute the function and retrieving it within the function will leave the function footprint unchanged and your customization upgradeable.

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.