Playing around with the DynamicsNAV:// protocol handler

Dynanmics 365 Business Central logo

If you have been using the Dotnet ClickOnce technology to roll out your Dynamics NAV / 365 Business Central Windows Clients, you know it have some limitations (click here to read all about the limitations).

But if you start pulling the ClickOnce technology apart, you will find that the client files are in fact present on your harddrive, but in a obscure directory – which can be hard to locate (actually you can just start the client and go to task manager. Right click the running program and select to open the directory).

What if someone could make a small program or script that would locate all installed ClickOnce Dynamics NAV / 365 Business Central Windows Clients, present you with a list to choose from and then simply start the selected client with the appropriate parameters?

Maybe we can even register this small program or script as the protocol handler for the DynamicsNAV:// protocol?

Well, here is my first attempt of a PowerShell script I call NAVBCProtocolHandlerHelper.ps1 :-):

# Version: 2019-08-06

$SearchPath = "C:\Users\$($env:UserName)\AppData\Local\Apps\2.0\"

$WindowsClientFileName = "Microsoft.Dynamics.Nav.Client.exe"
$ClientUserSettingsFileName = "ClientUserSettings.config"

if(($args[0] -eq "?") -or ($args[0] -eq "-?") -or ($args[0] -eq "-h") -or ($args[0] -eq "-help")) {
  Write-Host "#######################################################################" -ForegroundColor Cyan
  Write-Host "# Microsoft Dynamics NAV/365 Business Central Protocol Handler Helper #" -ForegroundColor Cyan
  Write-Host "#######################################################################" -ForegroundColor Cyan
  Write-Host "#             Copyright 2019, Gert Lynge, www.dabbler.dk              #" -ForegroundColor Cyan
  Write-Host "#######################################################################" -ForegroundColor Cyan
  Write-Host "# This script is free to use, modify and distribute as long as these  #" -ForegroundColor Cyan
  Write-Host "# lines are kept intact. NO WARRANTY! Use at your own risk!           #" -ForegroundColor Cyan
  Write-Host "#######################################################################" -ForegroundColor Cyan
  Write-Host ""
  Write-Host "Examples:" -ForegroundColor Cyan
  Write-Host ""
  Write-Host "Install NAV/365BC Protocol Handler:" -ForegroundColor Cyan
  Write-Host "$($MyInvocation.MyCommand.Definition) -InstallProtocolHandler"
  Write-Host "Be warned: This will override any existing DynamicsNAV:// protocol handler" -ForegroundColor Red
  Write-Host ""
  Write-Host "Remove NAV/365BC Protocol Handler:" -ForegroundColor Cyan
  Write-Host "$($MyInvocation.MyCommand.Definition) -RemoveProtocolHandler"
  Write-Host "Be warned: This will remove any DynamicsNAV:// protocol handler installed" -ForegroundColor Red
  Write-Host ""
  Write-Host "Show list of NAV/365BC ClickOnce Windows Clients and let you select one to start:" -ForegroundColor Cyan
  Write-Host "$($MyInvocation.MyCommand.Definition)"
  Write-Host ""
  Write-Host "Show this help page:" -ForegroundColor Cyan
  Write-Host "$($MyInvocation.MyCommand.Definition) -help"
  exit
}
elseif(($args[0] -eq "InstallProtocolHandler") -or ($args[0] -eq "-InstallProtocolHandler")) {
  $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
  if(-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    Throw "Installing the DynamicsNAV:// Protocol handler requires this script to be run as Administrator"
  }

  New-PSDrive -Name HKCR -PSProvider Registry -Root "HKEY_CLASSES_ROOT" -ErrorAction SilentlyContinue | Out-Null
  New-Item -Path "HKCR:\DynamicsNAV" -Force | Out-Null
  New-ItemProperty -Path "HKCR:\DynamicsNAV" -Name "(Default)" -Value "Dynamics NAV Protocol" -Force | Out-Null
  New-ItemProperty -Path "HKCR:\DynamicsNAV" -Name "URL Protocol" -Force | Out-Null

  New-Item -Path "HKCR:\DynamicsNAV\DefaultIcon" -Force | Out-Null
  New-ItemProperty -Path "HKCR:\DynamicsNAV\DefaultIcon" -Name "(Default)" | Out-Null

  New-Item -Path "HKCR:\DynamicsNAV\Shell" -Force | Out-Null
  New-ItemProperty -Path "HKCR:\DynamicsNAV\Shell" -Name "(Default)" | Out-Null

  New-Item -Path "HKCR:\DynamicsNAV\Shell\Open" -Force | Out-Null
  New-ItemProperty -Path "HKCR:\DynamicsNAV\Shell\Open" -Name "(Default)" | Out-Null

  New-Item -Path "HKCR:\DynamicsNAV\Shell\Open\Command" -Force | Out-Null
  New-ItemProperty -Path "HKCR:\DynamicsNAV\Shell\Open\Command" -Name "(Default)" -Value "PowerShell.exe -WindowStyle Hidden -File ""$($MyInvocation.MyCommand.Definition)"" ""%1""" | Out-Null

  Write-Host "Protocol Handler DynamicsNAV:// installed" -ForegroundColor Green
  exit
}
elseif(($args[0] -eq "RemoveProtocolHandler") -or ($args[0] -eq "-RemoveProtocolHandler")) {
  $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
  if(-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    Throw "Removing the DynamicsNAV:// Protocol handler requires this script to be run as Administrator"
  }

  New-PSDrive -Name HKCR -PSProvider Registry -Root "HKEY_CLASSES_ROOT" -ErrorAction SilentlyContinue | Out-Null
  Remove-Item -Path "HKCR:\DYNAMICSNAV" -Recurse -Force | Out-Null

  Write-Host "Protocol Handler DynamicsNAV:// removed" -ForegroundColor Green
  exit
}


$WindowsClients = New-Object System.Data.DataTable
$WindowsClients.Columns.Add("Product","System.String") | Out-Null
$WindowsClients.Columns.Add("Server","System.String") | Out-Null
$WindowsClients.Columns.Add("Port","System.String") | Out-Null
$WindowsClients.Columns.Add("Instance","System.String") | Out-Null
$WindowsClients.Columns.Add("Tenant","System.String") | Out-Null
$WindowsClients.Columns.Add("Authentication","System.String") | Out-Null
$WindowsClients.Columns.Add("Version","System.Version") | Out-Null
$WindowsClients.Columns.Add("Path","System.String") | Out-Null

foreach($File in Get-ChildItem -Path "$SearchPath$WindowsClientFileName" -Recurse | Select-Object FullName) {
  $Path = (Split-Path -Path $File.FullName -Parent)

  if(Test-Path "$Path\$ClientUserSettingsFileName" -PathType Leaf) {
    [xml]$XmlDocument = Get-Content -Path "$Path\$ClientUserSettingsFileName"
    $ChildNodes = $XmlDocument.configuration.appSettings.ChildNodes

    $WindowsClient = $WindowsClients.NewRow()
    $WindowsClient.Path = $Path
    $WindowsClient.Version = [System.Version][System.Diagnostics.FileVersionInfo]::GetVersionInfo($File.FullName).FileVersion
    $WindowsClient.Server = ($ChildNodes | Where-Object {($_.key -eq "Server")}).value
    $WindowsClient.Port = ($ChildNodes | Where-Object {($_.key -eq "ClientServicesPort")}).value
    $WindowsClient.Instance = ($ChildNodes | Where-Object {($_.key -eq "ServerInstance")}).value
    $WindowsClient.Tenant = ($ChildNodes | Where-Object {($_.key -eq "TenantId")}).value
    $WindowsClient.Authentication = ($ChildNodes | Where-Object {($_.key -eq "ClientServicesCredentialType")}).value
    $WindowsClient.Product = ($ChildNodes | Where-Object {($_.key -eq "ProductName")}).value
    $WindowsClients.Rows.Add($WindowsClient)
  }
}
$WindowsClients = $WindowsClients | Group-Object -Property Server,ClientServicesPort,ServerInstance,TenantId | `
                  foreach { $_.Group | Sort-Object @{Expression="Version";Descending="$True"} | Select-Object -First 1 }

$Picked = $WindowsClients | Out-GridView -PassThru -Title "Select which Microsoft Dynamics NAV/365 Business Central ClickOnce Windows Client to run"

if(-not $Picked) {
  exit
}

$Command = "$($Picked.Path)\$WindowsClientFileName"
if($args[0]) {
  & "$Command" "-protocolhandler" $args[0]
}
else {
  & "$Command"
}

Please run NAVBCProtocolHandlerHelper.ps1 from a powershell CLI with the -help parameter and read the possibilities. It has five ways of running:

  1. Show the help:
    NAVBCProtocolHandlerHelper.ps1 -help
  2. Install itself as the DynamicsNAV:// protocol handler (OVERWRITING the existing one):
    NAVBCProtocolHandlerHelper.ps1 -InstallProtocolHandler
  3. Remove ANY DynamicsNAV:// protocol handler (also ones the script did not install itself):
    NAVBCProtocolHandlerHelper.ps1 -RemoveProtocolHandler
  4. Show list of installed ClickOnce Windows Clients for Dynamics NAV/365 Business Central and lets you choose one. The chosen one is started:
    NAVBCProtocolHandlerHelper.ps1
  5. Started through the DynamicsNAV:// protocol.
    A list of installed ClickOnce Windows Clients for Dynamics NAV/365 Business Central will be shown. When you chose one, it is started and the URL is passed as a parameter to it (just like if it was called directly from the protocol handler):
    DynamicsNAV://<your parameters>

This is still work in progress and can be improved in many ways. Feel free to send me your comments and wishes.

You can use and modify this free of charge as long as you keep the blocks shown in the help intact.
This is completely without any warranty of any kind – so use at your own risk!
Parameters and functionalities are subject to changes as this is work in progress.

Bad Request – Error in query syntax.

PowerShell logo

No, this is not an error in WordPress on this server – “Bad Request – Error in query syntax.” is in fact the intended subject for this Blog post :-).

If you are running OData WebServices towards you Microsoft Dynamics NAV or 365 Business Central (DBC) and you get this error – and you have a forward slash ( “/” ) in the company name in question, then this Blog post is for you :-).

You have just discovered that Excel, PowerShell.exe and/or older version of PowerShell ISE cannot work with OData URLs with a forward slash in the …/Company(‘<my company name>’)/… part of the URL.

A obvious solution is to rename the company in NAV/DBC – but that is not really a neat solution, is it?

According to this Microsoft blog post, another far superior solution exists. NAV/DBC supports two syntaxes when stating the company name.

The “classic” one you use for OData version 3 when you got the error:

https://<my hostname>:<my odata port>/<my instance>/OData/Company('<my company')/<my webservice>

…or the corresponding OData version 4 one:

https://<my hostname>:<my odata port>/<my instance>/ODataV4/Company('<my company')/<my webservice>

…and another one for working around this error:

https://<my hostname>:<my odata port>/<my instance>/OData/<my webservice>?company='<my company>'

…or the corresponding OData version 4 one:

https://<my hostname>:<my odata port>/<my instance>/ODataV4/<my webservice>?company='<my company>'

Note that you can off cause add additonal parameters (selecting tenant or setting filters etc.) to the URL using the ampersand sign ( “&” ) – just like you do when construction any URL.

Congratulations – you solved your problem πŸ™‚

If you want to learn more about OData towards NAV/DBC, I suggest you visit these links:

Cent and Yen in Dynamics NAV 2018

Microsoft Dynamics NAV logo

If you run a Danish Dynamics NAV and get objects from outside Denmark, you might experience the Danish letter ΓΈ/Ø beeing replaced by Β’/Β₯ (the sign for Cent and Yen).

The fix?
Simply export all your objects as text, run them through the following powershell and import them again.

(((Get-Content -encoding Oem "InFile.txt") -replace ([char]8250),([char]251)) -replace ([char]157),([char]238)) | Set-Content -encoding Oem "OutFile.txt"

 

Finish off by a compile and syncronize all tables.

This is off cause on your own risk, so start by taking a full backup of your NAV system/server.


2018-12-20 Updated: powershell improved and now using unicode values of chars

“SQL Server does not exist or access denied” using Windows 10, build 1803

Microsoft SQL Server logo

If you are running your Microsoft Dynamics C5 or Microsoft Concorde XAL program files off a SMB1.x share, and your client is running Microsoft Windows 10, your C5/XAL might stop working if you upgrade your Windows 10 to build 1803.

This is really a “far out” bug, in build 1803 Microsoft apparently strengthened some security which makes it impossible to connect to a SQL Server through ODBC if – and only if – your program files are stored on a file sharing running SMB1.x (which means it will actually work if you copy your program files to a local drive – but that will off cause break things – some C5 and XAL files needs to be shared!).

And now we’re at it – this is not only hitting C5 and XAL, but other systems using ODBC as well: https://www.google.com/search?source=hp&ei=zfH6WqasBorGwQKImqroDw&q=sql+connection+problem+after+windows+update+1803&oq=sql+connection+problem+after+windows+update+1803

The solution is straight forward: make your file share run SMB2.0 or newer:

Hint: If you run this PowerShell command on your client, it will list all your network shares and the SMB-version in use for each of them:

Get-SmbConnection

Annoying non-rotating picture frame

Big Ben upside-down

If you got a cheap Chinese Picture Frame, you have probably noticed that half your pictures are upside-down, or rotated left or right.

It happens even if the pictures are shown correctly on your computer, so if you got loads of pictures on your frame, it can be quite a hassle to figure out which files are orientated wrong.

The cause of this is the EXIF attribute which stores the correct orientation of the picture. Or rather that most Picture Frames ignores this attribute, probably because they do not have the CPU power to rotate the picture anyway.

The easiest fix is to run this PowerShell script on all your pictures – it will read the EXIF attribute and actually rotate the picture so your Picture frame does not have to.

Have fun!


2020-04-11: Updated: Now the script also resizes images (while maintaining aspect ratio) so your huge images won’t fill up your picture frame (remember to keep a copy of the images if you need them in full resolution)

# 20200411 Gert Lynge. Source: http://www.dabbler.dk
#
[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") > $null

function Get-EXIFRotationAttribute($FilePath) {
# Returns:
# 1 = "Horizontal"
# 3 = "Rotate 180 degrees"
# 6 = "Rotate 90 degrees clockwise"
# 8 = "Rotate 270 degrees clockwise"
  $WiaImage = New-Object -ComObject Wia.ImageFile
  $WiaImage.LoadFile($FilePath)
  if($WiaImage.Properties.Exists("274")) {
    return ($WiaImage.Properties.Item("274").Value)
  }
  return -1
}

function RotateClockwise-JpegImage($Degrees,$FilePath) {
  $Image = [System.Drawing.image]::FromFile( $FilePath )
  switch($Degrees) {
    90  { $Image.rotateflip("Rotate90FlipNone") }
    180 { $Image.rotateflip("Rotate180FlipNone") }
    270 { $Image.rotateflip("Rotate270FlipNone") }
  }
  $Image.save($FilePath)
  $Image.Dispose()
  Write-Host -ForegroundColor Yellow "$FilePath rotated $Degrees degrees clockwise"
}

function ResizeImage() {
    param([String]$ImagePath, [Int]$Quality = 90, [Int]$targetSize, [String]$OutputLocation)
 
    Add-Type -AssemblyName "System.Drawing"
 
    $img = [System.Drawing.Image]::FromFile($ImagePath)
 
    $CanvasWidth = $targetSize
    $CanvasHeight = $targetSize
 
    #Encoder parameter for image quality
    $ImageEncoder = [System.Drawing.Imaging.Encoder]::Quality
    $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1)
    $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter($ImageEncoder, $Quality)
 
    # get codec
    $Codec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where {$_.MimeType -eq 'image/jpeg'}
 
    #compute the final ratio to use
    $ratioX = $CanvasWidth / $img.Width;
    $ratioY = $CanvasHeight / $img.Height;
 
    $ratio = $ratioY
    if ($ratioX -le $ratioY) {
        $ratio = $ratioX
    }

    if ($ratio -ne 1) { 
      $newWidth = [int] ($img.Width * $ratio)
      $newHeight = [int] ($img.Height * $ratio)
 
      $bmpResized = New-Object System.Drawing.Bitmap($newWidth, $newHeight)
      $graph = [System.Drawing.Graphics]::FromImage($bmpResized)
      $graph.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
 
      $graph.Clear([System.Drawing.Color]::White)
      $graph.DrawImage($img, 0, 0, $newWidth, $newHeight)
 
      #save to file
      $img.Dispose()
      $bmpResized.Save($OutputLocation, $Codec, $($encoderParams))
      $bmpResized.Dispose()
      
      Write-Host -ForegroundColor Yellow "$FilePath scaled to $newWidth * $newHeight"
    }
}

function Rotate-JpegImages($ImagePath) {
  write-host -ForegroundColor Yellow "Checking for jpg/jpeg images that needs rotation in path $ImagePath (including subdirectories)..."
  $JpegFiles = get-childitem -Recurse -Path "$ImagePath\*" -File -Include @("*.jpg","*.jpeg")
  ForEach($JpegFile IN $JpegFiles) {
    $FilePath = Join-Path -Path $ImagePath -ChildPath $JpegFile.Name

    $EXIFRotationAttribute = Get-EXIFRotationAttribute $FilePath
    switch($EXIFRotationAttribute) {
      6 { RotateClockwise-JpegImage 90 $FilePath }
      3 { RotateClockwise-JpegImage 180 $FilePath }
      8 { RotateClockwise-JpegImage 270 $FilePath }
    }

    ResizeImage -ImagePath $FilePath -targetSize 1280 -OutputLocation $FilePath

  }
}

# Rotate all images on D: (including files in subdirectories)
clear
Rotate-JpegImages "D:\"