Connecting to NAV Web Services from PHP

Prerequisites

Please read this post to get an explanation on how to modify the service tier to use NTLM authentication and for a brief explanation of the scenario I will implement in PHP.

BTW. Basic knowledge about PHP is required to understand the following post:-)

Version and download

In my sample I am using PHP version 5.2.11, which I downloaded from http://www.php.net, but it should work with any version after that.

In order to make this work you need to make sure that SOAP and CURL extensions are installed and enabled in php.ini.

PHP does not natively have support for NTLM nor SPNEGO authentication protocols, so we need to implement that manually. Now that sounds like something, which isn’t straightforward and something that takes an expert.   Fortunately there are a lot of these experts on the internet and I found this post (written by Thomas Rabaix), which explains about how what’s going on, how and why. Note that this implements NTLM authentication and you will have to change the Web Services listener to use that.

License of the code

The code can be used freely as long as you include Thomas’ copyright notice in the code.

/*
* Copyright (c) 2008 Invest-In-France Agency http://www.invest-in-france.org
*
* Author : Thomas Rabaix
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

“My” Version

I have modified Thomas’ version slightly (primarily removing debug echo’s etc.).

I have also changed the way the username and password is specified to be a script constant:

define(‘USERPWD’, ‘domainuser:password’);

The Stream wrapper now looks like:

class NTLMStream
{
private $path;
private $mode;
private $options;
private $opened_path;
private $buffer;
private $pos;
/**
* Open the stream
*
* @param unknown_type $path
* @param unknown_type $mode
* @param unknown_type $options
* @param unknown_type $opened_path
* @return unknown
*/
public function stream_open($path, $mode, $options, $opened_path) {
$this->path = $path;
$this->mode = $mode;
$this->options = $options;
$this->opened_path = $opened_path;
$this->createBuffer($path);
return true;
}
/**
* Close the stream
*
*/
public function stream_close() {
curl_close($this->ch);
}
/**
* Read the stream
*
* @param int $count number of bytes to read
* @return content from pos to count
*/
public function stream_read($count) {
if(strlen($this->buffer) == 0) {
return false;
}
$read = substr($this->buffer,$this->pos, $count);
$this->pos += $count;
return $read;
}
/**
* write the stream
*
* @param int $count number of bytes to read
* @return content from pos to count
*/
public function stream_write($data) {
if(strlen($this->buffer) == 0) {
return false;
}
return true;
}
/**
*
* @return true if eof else false
*/
public function stream_eof() {
return ($this->pos > strlen($this->buffer));
}
/**
* @return int the position of the current read pointer
*/
public function stream_tell() {
return $this->pos;
}
/**
* Flush stream data
*/
public function stream_flush() {
$this->buffer = null;
$this->pos = null;
}
/**
* Stat the file, return only the size of the buffer
*
* @return array stat information
*/
public function stream_stat() {
$this->createBuffer($this->path);
$stat = array(
‘size’ => strlen($this->buffer),
);
return $stat;
}
/**
* Stat the url, return only the size of the buffer
*
* @return array stat information
*/
public function url_stat($path, $flags) {
$this->createBuffer($path);
$stat = array(
‘size’ => strlen($this->buffer),
);
return $stat;
}
/**
* Create the buffer by requesting the url through cURL
*
* @param unknown_type $path
*/
private function createBuffer($path) {
if($this->buffer) {
return;
}
$this->ch = curl_init($path);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($this->ch, CURLOPT_HTTPAUTH, CURLAUTH_NTLM);
curl_setopt($this->ch, CURLOPT_USERPWD, USERPWD);
$this->buffer = curl_exec($this->ch);
$this->pos = 0;
}
}

The NTLM SOAP Client also uses the USERPWD constant defined above and looks like:

class NTLMSoapClient extends SoapClient {
function __doRequest($request, $location, $action, $version) {
$headers = array(
‘Method: POST’,
‘Connection: Keep-Alive’,
‘User-Agent: PHP-SOAP-CURL’,
‘Content-Type: text/xml; charset=utf-8’,
‘SOAPAction: “‘.$action.'”‘,
);
$this->__last_request_headers = $headers;
$ch = curl_init($location);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true );
curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_NTLM);
curl_setopt($ch, CURLOPT_USERPWD, USERPWD);
$response = curl_exec($ch);
return $response;
}

    function __getLastRequestHeaders() {
return implode(“n”, $this->__last_request_headers).”n”;
}
}

Putting this into the PHP script now allows you to connect to NAV Web Services system service in PHP and output the companies available on the service tier:

// we unregister the current HTTP wrapper
stream_wrapper_unregister(‘http’);
// we register the new HTTP wrapper
stream_wrapper_register(‘http’, ‘NTLMStream’) or die(“Failed to register protocol”);

// Initialize Soap Client
$baseURL = ‘
http://localhost:7047/DynamicsNAV/WS/’;
$client = new NTLMSoapClient($baseURL.’SystemService’);

// Find the first Company in the Companies
$result = $client->Companies();
$companies = $result->return_value;
echo “Companies:<br>”;
if (is_array($companies)) {
foreach($companies as $company) {
echo “$company<br>”;
}
$cur = $companies[0];
}
else {
echo “$companies<br>”;
$cur = $companies;
}

Note that is return value is an array if there are multiple companies, but a company name if there is only one. I have NO idea why this is or whether I can write the code differently to avoid this.

Now I have the company I want to use in $cur and the way I create a URL to the Customer page is by doing:

$pageURL = $baseURL.rawurlencode($cur).’/Page/Customer’;
echo “<br>URL of Customer page: $pageURL<br><br>”;

and then I can create a Soap Client to the Customer Page:

// Initialize Page Soap Client
$page = new NTLMSoapClient($pageURL);

and using this, I read customer 10000 and output the name:

$params = array(‘No’ => ‘10000’);
$result = $page->Read($params);
$customer = $result->Customer;
echo “Name of Customer 10000:”.$customer->Name.”<br><br>”;

Last, but not least – lets create a filter and read all customers in GB that has Location Code set to RED or BLUE:

$params = array(‘filter’ => array(
array(‘Field’ => ‘Location_Code’,
‘Criteria’ => ‘RED|BLUE’),
array(‘Field’ => ‘Country_Region_Code’,
‘Criteria’ => ‘GB’)
),
‘setSize’ => 0);
$result = $page->ReadMultiple($params);
$customers = $result->ReadMultiple_Result->Customer;

Note that Bookmark is an optional parameter and doesn’t need to be specified.

Now echo the customers and restore the http protocol – again, the return value might be an array and might not.

echo “Customers in GB served by RED or BLUE warehouse:<br>”;

if (is_array($customers)) {
foreach($customers as $cust) {
echo $cust->Name.”<br>”;
}
}
else {
echo $customers->Name.”<br>”;
}

// restore the original http protocole
stream_wrapper_restore(‘http’);

All of the above will output this when the script is opened in a browser (on my machine running NAV 2009SP1 W1)

image_thumb[3]

I hope this is helpful.

Good luck

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Connecting to NAV Web Services from …

I promised to write some posts about how to connect to NAV Web Services from various other programming languages/platforms and I guess it is about time I kept my promise.

I will create a couple of posts on how to connect to NAV Web Services from:

  • PHP
  • Java
  • C# using Web Reference
  • C# using Service Reference
  • Javascript
  • Visual Basic using Web Reference
  • Visual Basic using Service Reference
  • Windows Mobile
  • Microsoft Dynamics NAV 2009SP1
  • and more…

Scenario

For all these I will create some code that:

  • Connects to NAV Web Services System Service and enumerates the companies in the database
  • Constructs a URL to the Customer Page (based on the first company in the list)
  • Get the name of Customer number 10000
  • Get the Customers in GB that have Location Code set to RED or BLUE.

Here is an example of the output of the PHP web site:

image

Note, that the code is not necessarily perfect PHP or Java code and the intention is not to teach people how to code in these languages (I would be a bad teacher) but more to overcome some of the obstacles when trying to work with NAV.

Authentication

The very first obstacle is authentication. As you probably know, NAV 2009 only supported SPNEGO (Negotiate) authentication and both PHP and Java doesn’t currently have any support natively for that.

In NAV 2009 SP1 we added a key in the Service Tier configuration file called WebServicesUseNTLMAuthentication. If you set this key to true the Web Services listener will only use NTLM authentication and as such be easier accessible from other systems.

The config file you need to modify is placed in the directory where the Service Tier executable is and is called CustomSettings.config. The section you are looking for is:

    <!–
Turns on or off NTLM authentication protocol for Web Services
false: Use SPNEGO (recommended)
true: Use NTLM only
–>
<add key=”WebServicesUseNTLMAuthentication” value=”true”></add>

Note that .net works with both values of this settings.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Edit In Excel R2 – Part 2 (out of 2) – the final pieces

It is time to collect the pieces.

The full Edit In Excel R2 solution looks like this

image

Slightly more complicated than the first version – but let me try to explain the pieces

NAVEditInExcel is the COM object, which we use from within NAV. This actually hasn’t changed a lot, the only small change is, that the EditInExcel method now takes a base URL, a company, a page and a view (compared to just a page and a view earlier).
NAVPageDynamicWebReference is the Dynamic Web Reference class and the NAVPageServiceHelper class – described here.
NAVPageFieldInfo contains the NAVFieldInfo class hierarchy for handling type weak pages, described here and used in the Conflict resolution dialog here.
NAVPageMergeForm is the conflict resolution dialog, described here.
NAVTemplate is the actual Excel Add-In which of course now makes use of Dynamic Page References and conflict resolution. It really haven’t changed a lot since the version described here – the major change is the pattern for handling conflict resolution.
EditInExcel Setup is the Client Setup program, this setup program needs to be run on all Clients
EditInExcelDemo is the Server Setup program, this setup program contains the Client Setup msi and places it in the ClientSetup folder for the ComponentHelper (which you can read about here) to autodeploy to clients. This setup also contains the .fob with the EditInExcel objects.

The Client Setup Program

Lets have a closer look at the Client Setup Program

image

This setup project includes primary output from the COM component and the Excel Add-in and calculated dependencies from that.

Note, that when deploying add-ins you have to add the .vsto and the .manifest files to the setup project yourself, the dependency finder doesn’t discover those. Also note, that all the vsto runtime dll’s etc are excluded from the install list, as we do not want to copy those DLL’s.

Instead I have built in a Launch condition for VSTO runtime 3.0, which is done in 2 steps:

image

First a Search on the Target Machine for component ID {AF68A0DE-C0CD-43E1-96DD-CBD9726079FD} (which is the component installation ID for VSTO 3.0 Runtime) and a launch condition stating that that search needs to return TRUE – else a message will appear with a URL for installing VSTO, which is:

http://www.microsoft.com/downloads/details.aspx?FamilyId=54EB3A5A-0E52-40F9-A2D1-EECD7A092DCB&displaylang=en

One more thing needed in the Client Setup program is to register the COM object. Now the Setup actually has a property you can set, indicating that the object should be registered as COM, but I couldn’t get that to work, so I added custom install actions to the NAVEditInExcel COM object:

image

and the code for the class, which is called by the installer looks like:

[RunInstaller(true)]
public partial class RegasmInstaller : Installer
{
public RegasmInstaller()
: base()
{
}

    public override void Commit(IDictionary savedState)
{
base.Commit(savedState);
Regasm(false);
}

    public override void Rollback(IDictionary savedState)
{
base.Rollback(savedState);
}

    public override void Uninstall(IDictionary savedState)
{
base.Rollback(savedState);
Regasm(true);
}

    private void Regasm(bool unregister)
{
string parameters = “/tlb /codebase”;
if (unregister)
parameters += ” /unregister”;
string regasmPath = RuntimeEnvironment.GetRuntimeDirectory() + @”regasm.exe”;
string dllPath = this.GetType().Assembly.Location;
if (!File.Exists(regasmPath))
throw new InstallException(“Registering assembly failed”);
if (!File.Exists(dllPath))
return;

        Process process = new Process();
process.StartInfo.CreateNoWindow = true;
process.StartInfo.UseShellExecute = false; // Hides console window
process.StartInfo.FileName = regasmPath;
process.StartInfo.Arguments = string.Format(“”{0}” {1}”, dllPath, parameters);
process.Start();

        // When uninstalling we need to wait for the regasm to finish,
// before continuing and deleting the file we are unregistering
if (unregister)
{
process.WaitForExit(10000);
try
{
System.IO.File.Delete(System.IO.Path.ChangeExtension(dllPath, “tlb”));
}
catch
{
}
}
}
}

All of the above is captured in the NAVEditInExcelR2.msi – which is the output from the Edit In Excel Setup project. Running this .msi on a client will check pre-requisites, install the right DLL’s, register the COM and you should be good to go.

The Server Setup Program

The Server Setup program actually just needs to place the Client Setup Program in a ClientSetup folder and the .fob (NAV Objects) in the ServerSetup folder.

There are no pre-requisites, no actions no nothing – just copy the files.

After Copying the files on the Server – you need to import the .fob, run the setup code unit and you should be good to go.

Note, that this requires ComponentHelper1.03 (which you can read about here and download here) to run.

Wrapping up…

So, what started out as being a small garage project, ended up being somewhat more complicated and way more powerful. It runs with Office 2007 and Office 2010 (even though you cannot modify the project when Office 2010 beta2 is installed) and even though you might not need the actual Edit In Excel functionality – there are pieces of this that can be used for other purposes.

The source for the entire thing can be downloaded here and the EditInExcel Demo msi can be downloaded here.

 

Happy holidays

 

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Auto Deployment of Client Side Components – take 2

Updated the link to the ComponentHelper msi on 12/11/2009

Please read my first post about auto deployment of Client side components here before reading this.

As you know, my first auto deployment project contained a couple of methods for automatically adding actions to pages, but as one of my colleagues in Germany (Carsten Scholling) told me, it would also need to be able to add fields to tables programmatically in order to be really useful.

In fact, he didn’t just tell me that it should do so, he actually send me a couple of methods to perform that.

The method signatures are:

AddToTable(TableNo : Integer;FieldNo : Integer;VersionList : Text[30];FieldName : Text[30];FieldType : Integer;FieldLength : Integer;Properties : Text[800]) : Boolean

and

AddTheField(TableNo : Integer;FieldNo : Integer;FieldName : Text[30];FieldType : Integer;FieldLength : Integer) SearchLine : Text[150]

And it can be used like:

// Add the fields
SearchLineLat := ComponentHelper.AddTheField(DATABASE::Customer, 66030, ‘Latitude’,  FieldRec.Type::Decimal, 0);
SearchLineLong := ComponentHelper.AddTheField(DATABASE::Customer, 66031, ‘Longitude’,  FieldRec.Type::Decimal, 0);

which just add’s the fields without captions or

// Add Latitude to Customer table
ComponentHelper.AddToTable(DATABASE::Customer, 66030, ‘VirtualEarthDemo1.01’, ‘Latitude’, FieldRec.Type::Decimal, 0,
‘CaptionML=[DAN=Breddegrad;DEU=Breitengrad;ENU=Latitude;ESP=Latitud;FRA=Latitude;ITA=Latitudine;NLD=Breedte];DecimalPlaces=6:8’);

Remember, that the table will be left uncompiled after doing this.

AddToTable actually calls AddTheField and after that it modifies the metadata to set the caption on the field:

AddToTable(TableNo : Integer;FieldNo : Integer;VersionList : Text[30];FieldName : Text[30];FieldType : Integer;FieldLength : Integer;Properties : Text[800]) : Boolean
changed := FALSE;
SearchLine := AddTheField(TableNo, FieldNo, FieldName, FieldType, FieldLength);
IF SearchLine <> ” THEN
BEGIN
GetTableMetadata(TableNo, Metadata);
IF AddToMetadataEx(TableNo, Object.Type::Table, Metadata, SearchLine, ”, ‘;’ + Properties, TRUE, FALSE) THEN BEGIN
SetTableMetadata(TableNo, Metadata, VersionList);
changed := TRUE;
END;
END;

AddTheField is the actual “magic”:

AddTheField(TableNo : Integer;FieldNo : Integer;FieldName : Text[30];FieldType : Integer;FieldLength : Integer) SearchLine : Text[150]
Field.SETRANGE(TableNo, TableNo);
Field.SETRANGE(“No.”, FieldNo);
SearchLine := ”;

IF Field.ISEMPTY THEN BEGIN
Field.TableNo := TableNo;
Field.”No.” := FieldNo;
Field.FieldName := FieldName;
Field.Type := FieldType;
Field.Class := Field.Class::Normal;
Field.Len := FieldLength;
Field.Enabled := TRUE;
Field.INSERT;

  Field.FINDFIRST;

  Len[1] := 4;
Len[2] := 20;
Len[3] := 15;

  IF STRLEN(FORMAT(FieldNo))   > Len[1] THEN Len[1] := STRLEN(FORMAT(FieldNo));
IF STRLEN(FieldName)         > Len[2] THEN Len[2] := STRLEN(FieldName);
IF STRLEN(Field.”Type Name”) > Len[3] THEN Len[3] := STRLEN(Field.”Type Name”);

  SearchLine := ‘    { ‘ + PADSTR(FORMAT(FieldNo), Len[1]) + ‘;  ;’ +
PADSTR(FieldName, Len[2]) + ‘;’ + PADSTR(Field.”Type Name”, Len[3]);
END;

The new ComponentHelper 1.03 msi can be downloaded here and my upcoming posts (e.g. Edit In Excel R2) will require this. If you only want to download the objects you can do so here (there is no changes in the NAVAddInHelper source (compared to the first post – that can be downloaded here).

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Dynamic references to NAV Page Web Services in C# – take 2

In this post from April, I explained how to make dynamic references to page based Web Services, but the post really left the developer with a lot of manual work to do using reflection.

So – I thought – why not create a couple of helper classes which makes it easier.

Basically I have created a generic NAVPageServiceHelper class, which encapsulates all the heavy lifting of reflection and leaves the developer with a set of higher level classes he can use.

The service helper will have a collection of classes explaining various information about the fields and has methods for getting or setting the value (and setting the corresponding _Specified automatically as well).

The primary reason for making this is of course to make Edit In Excel bind to any page without changing anything, but the method can be used in a lot of other scenarios.

2 projects: NAVPageFieldInfo and NAVPageDynamicWebReference

I split the PageServiceHelper and the PageFieldInfo into two seperate projects. NAVPageFieldInfo just contains the FieldInfo classes for all the supported field types and a collection class.

NAVPageFieldInfo is the abstract base class
BooleanFieldInfo is the field info class for a boolean field
OptionFieldInfo is the field info class for an option field
IntFieldInfo is the field info for…

You get it – all in all, the following types are supported:

String, Decimal, DateTime, Int, Option, Boolean

Furthermore, there is a class called NAVFields, which derives from List<NAVFieldInfo>, for keeping a collection of the fields.

NAVFields has a method called PopulateFieldsCollection, which takes an object type and a fields enum type and based on this, instantiates all the NAVFieldInfo classes – let’s look at the code.

/// <summary>
/// Populate Fields Collection with NAVPageFieldInfo for all properties in the record
/// Should works with any NAV 2009 Page exposed as WebService
/// </summary>
/// <param name=”objType”>Type of Object (typeof(Customer), typeof(Vendor), …)</param>
/// <param name=”fieldsType”>Type of the Enum holding the property names</param>
private void PopulateFieldsCollection(Type objType, Type fieldsType)
{
// Key property is not part of the Enum
// Add it manually as the first field
AddField(“Key”, objType);

    // Run through the enum and add all fields
foreach (string field in Enum.GetNames(fieldsType))
{
AddField(field, objType);
}
}

/// <summary>
/// Add a NAVPageFieldInfo for a field to the fields collection
/// </summary>
/// <param name=”field”>Field name</param>
/// <param name=”objType”>Type of Object in which the field is (typeof(Customer), typeof(Vendor), …)</param>
private void AddField(string field, Type objType)
{
field = VSName(field);
PropertyInfo pi = objType.GetProperty(field, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (pi != null)
{
NAVPageFieldInfo nfi = NAVPageFieldInfo.CreateNAVFieldInfo(objType, field, pi, objType.Namespace);
if (nfi != null)
{
// If we encounter unknown Field Types, they are just ignored
this.Add(nfi);
}
}
}

As you can see, the AddField method calls a static method on NAVPageFieldInfo to get a FieldInfo class of the right type created. That method looks like:

/// <summary>
/// Create a NAVPageFieldInfo object for a specific field
/// </summary>
/// <param name=”field”>Name of the property</param>
/// <param name=”pi”>PropertyInfo for the property on the record object</param>
/// <param name=”ns”>Namespace for the record object (namespace for the added WebServices proxy class)</param>
/// <returns>NAVPageFieldInfo or null if the type isn’t supported</returns>
public static NAVPageFieldInfo CreateNAVFieldInfo(Type objType, string field, System.Reflection.PropertyInfo pi, string ns)
{
if (pi.PropertyType == typeof(string))
{
// String Property – is it the KeyField
if (field == “Key”)
return new KeyFieldInfo(field, pi, null);
else
return new StringFieldInfo(field, pi, null);
}
PropertyInfo piSpecified = objType.GetProperty(field + “Specified”, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (pi.PropertyType == typeof(decimal))
{
// Decimal Property
return new DecimalFieldInfo(field, pi, piSpecified);
}
if (pi.PropertyType == typeof(int))
{
// Integer Property
return new IntFieldInfo(field, pi, piSpecified);
}
if (pi.PropertyType == typeof(bool))
{
// Boolean Property
return new BooleanFieldInfo(field, pi, piSpecified);
}
if (pi.PropertyType == typeof(DateTime))
{
// DateTime Property
return new DateTimeFieldInfo(field, pi, piSpecified);
}
if (pi.PropertyType.Namespace == ns)
{
// Other Property Types, in the same namespace as the object
// These are enum’s – set up restrictions on OptionFields
return new OptionFieldInfo(field, pi, piSpecified, Enum.GetNames(pi.PropertyType));
}
return null;
}

No more magic!

And actually the constructor for NAVFields – takes the object type and the field type as parameters for the constructor:

public NAVFields(Type objType, Type fieldsType)
: base()
{
this.PopulateFieldsCollection(objType, fieldsType);
}

Meaning that all it takes to utilize the NAVFieldInfo subsystem is instantiating the NAVFields class, which doesn’t necessarily need a dynamic web reference helper, but could also be instantiated through:

NAVFields fields = new NAVFields(typeof(Customer), typeof(Customer_Fields));

If you have some code, which needs to access data loosely coupled, NAVFields is a great way to get going.

The other project is the NAVDynamicPageWebReference – which really is a combination of the Dynamic Web References post from April and a Page Service Helper class.

The way you get a reference to the Dynamic Web Reference is much like in the post from April:

Assembly customerPageRef = NAVPageDynamicWebReference.BuildAssemblyFromWSDL(
new Uri(“
http://localhost:7047/DynamicsNAV/WS/CRONUS%20International%20Ltd./Page/Customer”), 5000);

Based on this, you now instantiate the Service Helper with the Assembly and the name of the Page:

NAVPageServiceHelper serviceHelper = new NAVPageServiceHelper(customerPageRef, “Customer”);

Using the Page Service Helper

The Page Service Helper then uses NAVFields so that you can do stuff like:

foreach (NAVPageFieldInfo fi in serviceHelper.Fields)
Console.WriteLine(fi.field + ” ” + fi.fieldType.Name);

The properties currently in the Service Helper are:

Fields is a NAVFields (a list of NAVFieldInfo derived classes)
PrimaryKeyFields is an array NAVFieldInfo classes (from Fields) which makes out the primary key of the record
GetFieldsType returns the type of the Field enumeration
GetObjectType returns the type of the records handles through this Service
ReadMultiple reads the records matching an array of filters (calls the ReadMultiple on the Service)
CreateFilter creates a filter spec based on a field and a criteria
Read reads a record based on a primary key (creates a filter spec for the primary key and calls ReadMultiple)
Update updates a record (calls the Update method on the Service)
Create creates a record (calls the Create method on the Service)
Delete deletes a record matching a key
ReRead reads an updated instance of a record (calls the Read method on the Service with the key fields)
IsUpdated checks whether the record is updated (calls the IsUpdated method on the Service)
GetFiltersFromView creates an array of filter specs based on a view (from GETVIEW in AL Code)

An example of how to read customer 10000 and print the name would be:

object cust = serviceHelper.Read(“10000”);
Console.WriteLine(serviceHelper.Fields[“Name”].GetValue(cust));

Note, that you find the Field – and on the field, you call GetValue and specify the record instance.

If you need to Display the name of all customers with location code yellow you would write

ArrayList filters = new ArrayList();
filters.Add(serviceHelper.CreateFilter(“Location_Code”, “Yellow”));
object[] customers = serviceHelper.ReadMultiple(filters);
foreach (object customer in customers)
Console.WriteLine(serviceHelper.Fields[“Name”].GetValue(customer));

Or you could create a Customer by writing

object newcust = System.Activator.CreateInstance(serviceHelper.GetObjectType());
serviceHelper.Fields[“Name”].SetValue(newcust, “Freddy Kristiansen”, DBNull.Value);
newcust = serviceHelper.Create(newcust);
Console.WriteLine(serviceHelper.Fields[“No”].GetValue(newcust));

As mentioned before, the Page Service Helper was primarily created for making Edit In Excel and other projects, where you are using loosely coupled Page Web Service Access.

For a lot of other usages, this is overkill and you should rather use Web References in Visual Studio and have a strongly typed contract with the Web Service.

You can download the projects and the small test program here.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

What COMPANY to use?

As you know, when creating an application consuming NAV Web Services you need to specify the Company name as part of the URL to the Web Service, but what company should you be using?

Some applications are web front-ends placing data from the web application into NAV. For applications like this you typically would have a config file in which you specify what company things needs to go to. For these applications, this post adds no further value.

Other applications are integration applications, like a lot of the applications you can see on my blog:

  • Search
  • “My” gadgets
  • MAP
  • Edit In Excel

for all of these applications, it really doesn’t make sense to run with a different company than the users default company.

Example – if you search through your NAV data – you really want to search through the data in the active company – not just any company.

Wouldn’t it be nice if you could type in the URL

/Codeunit/Search”>http://localhost:7047/DynamicsNAV/WS/<default>/Codeunit/Search

and then the <default> would be replaced by the authenticated users default company – unfortunately this doesn’t work (I have suggested this feature for v7 though:-)). Instead, we have to do the work in the Web Service consuming application. Easiest solution is of course to create a Codeunit with a function, returning the default company of a user, call that and then build your URL for calling the Page / Codeunit web service.

A function like that could be:

GetDefaultCOMPANY() : Text[30]
Session.SETRANGE(“My Session”,TRUE);
Session.FINDFIRST;
WindowsLogin.SETRANGE(ID,Session.”User ID”);
WindowsLogin.FINDFIRST;
UserPers.SETRANGE(“User SID”,WindowsLogin.SID);
UserPers.FINDFIRST;
EXIT(UserPers.Company);

The problem with this approach is (as you probably already figured out) that every call to a Web Service will require 2 roundtrips instead of one and for Page based Web Service access there really isn’t much you can do better.

For Codeunit based Web Service access you can however avoid a lot of these roundtrips by using a very simple pattern in the way you write your functions. I have rewritten my search method to return a Text[30] and start off with the following lines of code:

company := GetDefaultCOMPANY();
IF company <> COMPANYNAME THEN
EXIT(company);

and the consumer will have to build up the URL for the Web Service in code with whatever company (the first in the list of companies would be just fine), call the web service and if it returns a different company than the one used to invoke the web service, build a new URL and try again.

In the Search gadget this would look like (the lines in Red are the important changes)

// the “real” search function
function doSearch(searchstring) {
    specifiedCompany = GetCompany();
usedCompany = specifiedCompany;
if (specifiedCompany == “default”) {
if (myCompany == “”) {
Companies = GetCompanies();
if (Companies != null)
myCompany = Companies[0].text;
}
usedCompany = myCompany;
}

    // Get the URL for the NAV 2009 Search Codeunit
var URL = GetBaseURL() + encodeURIComponent(usedCompany) + “/Codeunit/Search”;

    // Create XMLHTTP and send SOAP document
xmlhttp = new ActiveXObject(“Msxml2.XMLHTTP.6.0”);
xmlhttp.open(“POST”, URL, false, null, null);
xmlhttp.setRequestHeader(“Content-Type”, “text/xml; charset=utf-8”);
xmlhttp.setRequestHeader(“SOAPAction”, “DoSearch”);
xmlhttp.Send(‘<?xml version=”1.0″ encoding=”utf-8″?><soap:Envelope xmlns:soap=”‘ + SoapEnvelopeNS + ‘”><soap:Body><DoSearch xmlns=”‘ + CodeunitSearchNS + ‘”><searchstring>’ + searchstring + ‘</searchstring><result></result></DoSearch></soap:Body></soap:Envelope>’);

    // Find the result in the soap result and return the rsult
xmldoc = xmlhttp.ResponseXML;
xmldoc.setProperty(‘SelectionLanguage’, ‘XPath’);
xmldoc.setProperty(‘SelectionNamespaces’, ‘xmlns:soap=”‘ + SoapEnvelopeNS + ‘” xmlns:tns=”‘ + CodeunitSearchNS + ‘”‘);

    userCompany = xmldoc.selectSingleNode(‘//tns:return_value’);
myCompany = userCompany.text;

    if ((specifiedCompany == “default”) && (myCompany != usedCompany)) {
// Default company has changed – research
return doSearch(searchstring);
}

   … do the actual searching

}

In this sample I use three variables:

specifiedCompany is the company specified in the config file (default means use users default company)

usedCompany is the company used to invoke the last WS method

myCompany is my current belief of the users current company, which gets replaced if a method returns a new default company.

Using a pattern like this will help lowering the number of round trips and still allow your consuming application to use the users default company.

This “trick” is only possible in NAV 2009 SP1. NAV 2009 RTM will change the users default company to the company you use to invoke the Web Service with – which again will cause the above function to always return the same company name as the one you invoke the Web Service with.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Extending page Web Services (and creating a Sales Order again)

It has been working in the same way since NAV 2009, but I still get asked often how this works, so why not write up a quick post on this. I also realize that my prior post on how to create Sales Orders through Web Services was made very complex due to compatibility with NAV 2009.

This post only works in NAV 2009 SP1 and will show how to extend the Order page with a Post function and show how to Create a Sales Order from C# and post it.

Extending the page

First of all, we need to create a codeunit with the function, we want to add to the Order page.

image

Then we expose this codeunit with the same name as the page we want to extend, without putting a check in the published column

image

Note: All functions in the codeunit needs to have the first parameter be of the same type as the base record as the page you want to extend, else the page will no longer be available and you will get an error in the event log on the Service Tier.

Now taking a look at the WSDL in a browser will show us the new function as a first class citizen

image

and we can start using this.

Creating a Sales Order through Web Services

This might seem like repeating myself from a prior post, but that post did contain a lot of other information, which really isn’t necessary if you only target SP1.

Creating an order is a 3 step process:

  1. Create the Order Header
  2. Fill out the Order Header and create the Order lines
  3. Fill out the Order lines

Creating the Order header

Is really simple

Order_Service service = new Order_Service();
service.UseDefaultCredentials = true;

Order order = new Order();
service.Create(ref order);

After this we have a Order Number and an empty order – exactly like leaving the order No. field on the Sales Order Page.

Fill out the Order Header and create the Order lines

In this sample I will just fill out the Sell_to_Customer_No – a number of the other Order Header fields will be auto-updated when updating the order

order.Sell_to_Customer_No = “10000”;

Then we need to create the Order lines – in this sample I will create 5. BTW – It is NOT trivial to add an order line after the fact, so I suggest you add the needed number of lines in one go:

order.SalesLines = new Sales_Order_Line[5];
for (int i = 0; i < 5; i++)
order.SalesLines[i] = new Sales_Order_Line();
service.Update(ref order);

Fill out the Order lines

In this sample, I will just create 5 lines with green ROME guest chairs.

for (int i = 0; i < 5; i++)
{
order.SalesLines[i].Type = OrderPageRef.Type.Item;
order.SalesLines[i].No = “1960-S”;
order.SalesLines[i].Quantity = 1;
}
service.Update(ref order);

That’s it – the order is created and you can find it in the Client.

And at last… – Post the order

Having created the order, now it is time to post the order

service.PostOrder(order.Key);

As you can see, the function takes a Record parameter, but we give it a Key.

Note, that calling a function does not make an implicit Update – meaning that if you have done changes to the record in C# and call the function, you will get an error when calling update later. Reason – the PostOrder function has changed the record and will tell you that the record was changed by another user.

After calling a function on a page you will need to Re-read the record if you need to do more work.

That’s it

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Synchronize A/D users to NAV

During my work with demos like Edit In Excel, I wanted to make sure that these things would work in all localized versions of NAV 2009 SP1 – meaning that I needed to install 14 different databases and 14 running Service Tier’s. Having done that, I also wanted to allow my colleagues who needed to check something, access to these service tiers.

For a geek (like me:-)), that problem looks like something you need to write an application for, even though it probably takes more time than it would be to add the users one by one whenever needed, but it certainly is more fun to write the application – and… maybe somebody else can actually use the ideas from this to do something cool.

How to do?

Basically what I want to do is, to enumerate the Remote Desktop Users of the computer and make sure that these users are SUPER in NAV. Now, I can do this in NAV calling out to a COM object enumerating my users – but that really wouldn’t help me, because I would have to start NAV with every service tier or database and launch that action.

So, first I created a codeunit in NAV, exposed this codeunit as Web Services. Then I created a console application running on a schedule on my server, which enumerates the users and invoke the Web Service function to check all users are created in NAV.

Of course the scheduled application has to run with elevated permissions and that user needs to be able to change users in NAV as well.

The NAV code

The codeunit contains two functions:

ImportUserSID(SID : Text[119];Role : Code[20]) : Boolean
IF NOT Winlogin.GET(SID) THEN
EXIT(FALSE);
Winlogin.INIT;
Winlogin.SID := SID;
Winlogin.INSERT();
IF NOT WinAccess.GET(SID) THEN
BEGIN
WinAccess.INIT;
WinAccess.”Login SID” := SID;
WinAccess.”Role ID” := Role;
WinAccess.INSERT;
END;
EXIT(TRUE);

and

SynchronizeUsers()
DATABASE.SYNCHRONIZEALLLOGINS();

BTW. Calling SynchronizeAllLogins running through Web Services didn’t seem to work when called through Web Services and running enhanced database security – whether this is a bug or a problem in my setup – I don’t know, but I am going to file a bug on it.

As you can see, the ImportUserSID does not modify the role if the user is already in the login table – it could of course be modified to do that easily, but it wasn’t necessary for my usage.

The C# code for enumerating an A/D group

Lets just look at the code:

DirectoryEntry dir = new DirectoryEntry(“WinNT://localhost/Administrators”);
Console.WriteLine(“Enumerating users”);
foreach (object obj in (IEnumerable)dir.Invoke(“members”))
{
DirectoryEntry user = new DirectoryEntry(obj);
Console.WriteLine(user.Path);
}
Console.WriteLine(“Done”);
Console.ReadLine();

Running this on my computer outputs the following:

image

The next step is, to get the SID’s for each user – now you would think that there should be an SID function on the user object returning a string, but unfortunately it isn’t that simple. Luckily somebody invented the Internet – and luckily somebody was kind enough to post some information for us to use.

Have a look at http://www.netomatix.com/GetUserSid.aspx where I found the following code, which seems to work for the purpose.

On the user object you have a property collection. One of these properties is called objectSid, which is a byte[] and that can be transformed into a SID string using the following function:

private static string ConvertByteToStringSid(Byte[] sidBytes)
{
StringBuilder strSid = new StringBuilder();
strSid.Append(“S-“);
try
{
// Add SID revision.
strSid.Append(sidBytes[0].ToString());
// Next six bytes are SID authority value.
if (sidBytes[6] != 0 || sidBytes[5] != 0)
{
string strAuth = String.Format
(“0x{0:2x}{1:2x}{2:2x}{3:2x}{4:2x}{5:2x}”,
(Int16)sidBytes[1],
(Int16)sidBytes[2],
(Int16)sidBytes[3],
(Int16)sidBytes[4],
(Int16)sidBytes[5],
(Int16)sidBytes[6]);
strSid.Append(“-“);
strSid.Append(strAuth);
}
else
{
Int64 iVal = (Int32)(sidBytes[1]) +
(Int32)(sidBytes[2] << 8) +
(Int32)(sidBytes[3] << 16) +
(Int32)(sidBytes[4] << 24);
strSid.Append(“-“);
strSid.Append(iVal.ToString());
}

        // Get sub authority count…
int iSubCount = Convert.ToInt32(sidBytes[7]);
int idxAuth = 0;
for (int i = 0; i < iSubCount; i++)
{
idxAuth = 8 + i * 4;
UInt32 iSubAuth = BitConverter.ToUInt32(sidBytes, idxAuth);
strSid.Append(“-“);
strSid.Append(iSubAuth.ToString());
}
}
catch (Exception ex)
{
return “”;
}
return strSid.ToString();
}

Using this function, you can now write out the SID’s for each user:

DirectoryEntry dir = new DirectoryEntry(“WinNT://localhost/Administrators”);
Console.WriteLine(“Enumerating users”);
foreach (object obj in (IEnumerable)dir.Invoke(“members”))
{
DirectoryEntry user = new DirectoryEntry(obj);
Console.WriteLine(user.Path);
System.DirectoryServices.PropertyCollection col = user.Properties;
byte[] sidBytes = col[“objectSid”].Value as byte[];
if (sidBytes != null)
{
string strSid = ConvertByteToStringSid(sidBytes);
if (!string.IsNullOrEmpty(strSid))
{
Console.WriteLine(“SID=” + strSid);
}
}
}
Console.WriteLine(“Done”);
Console.ReadLine();

Of course we are not in the business of writing SID’s for users in a console application, but you should now have the building blocks for creating whatever mechanism to add A/D users to NAV, exposing the codeunit we talked about at first and then calling these web services functions from the console application.

Good luck

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

 

 

 

Auto deployment of Client Side Components

NOTE: There is an updated post on Auto deployment of Client Side Components here.

When you install the RoleTailored Client on a number of clients, you might need to install a number of Client side components as well. This might not sound as too much of a problem when you need to install the client anyway – but lets say you install an ISV Add-on with a live customer, who already have 100 clients install – and now you need to install the objects to the database – AND you need to run to 100 computers and install Client side components.

Yes, you can do this with system management policies, but not all customers are running SMS and it would just be way easier if everything could be handled from the ISV Add-On and the Client Components could be auto deployed.

When doing this – it is still important, that IF the customer is running SMS and decide to deploy the Client Side components through system policies – then the auto deployment should just pick this up and accept that things are Ok.

Two kinds of Client Side Components

NAV 2009 SP1 supports Add-Ins (Client Extensibility Controls) and Client side COM components (as NAV 2009) and the way these components are installed is very different.

Add-Ins needs to be placed in the Add-Ins folder under the RoleTailored Client folder on the Client and COM components can be installed wherever on the Client, but needs to registered in the registry with regasm.

Both Add-Ins and COM components might rely on other client side components, so it is important that we don’t just create a way of copying files to the Client – but we should instead create a way of launching a setup program on the client, which then installs the components. In my samples, I have one Setup program for every component, but an ISV could easily package all components together in one installation program and install them all in one go.

To install a client side component really isn’t that difficult – use FILE.DOWNLOAD with an MSI and that’s it. But how do we detect whether or not the component is installed already?

We cannot keep a list on the server side, since the computer might get re-installed or restored – we need a way of discovering whether a component is installed.

Detecting whether a Client side COM component is installed

I will start with the COM component (since it will take a COM component to check whether an Add-In is installed). The COM component needs a CREATE statement to be initialized and if you check the return value of the CREATE statement – you know whether or not the COM component is executable. If not we launch a FILE.DOWNLOAD

IF NOT CREATE(mycomponent, TRUE, TRUE) THEN
FILE.DOWNLOAD(mycomponentinstaller);

Almost too simple right?

Now – I know that some people will say – well, what if I have an updated version of the COM component and it needs to be deployed?

My answer to that would be to change the COM signature, in effect making it a different COM component and allow them to be installed side-by-side. This would in effect mean that you might have multiple versions of COM components installed on a client, but they typically don’t take up a lot of space, and they don’t run if nobody uses them.

You could also create a function for checking the version number of the component like:

IF NOT CREATE(mycomponent, TRUE, TRUE) THEN
FILE.DOWNLOAD(mycomponentinstaller)
ELSE IF NOT mycomponent.CheckVersion(100) THEN
FILE.DOWNLOAD(mycomponentinstaller);

problem with this approach is, that NAV keeps a lock on the Client side component (event if you CLEAR(mycomponent)) due to performance reasons and your mycomponentinstaller will have to close the NAV client in order to update the component.

I like the solution better, where you just create a new GUID and thus a new component – so that is what I will describe here.

Detecting whether an Add-In is installed on the Client

If you have installed the Server pieces of the Virtual Earth Integration (look here), but have a Client without the VEControl Add-In, this is how the FactBox will look:

image

Not very informative when you were expecting this:

image

But as you might know, we actually didn’t write any code to plugin the control and the Control Add-In error above is handled by the Client without actually notifying the Service Tier that anything is missing.

What we need to do, is to create one line of code in the INIT trigger of all pages, which uses an Add-In:

ComponentHelper.CheckAddInNameKey(‘FreddyK.LargeVEControl’,’1c9f7ad47dba024b’);

and then of course create a function that actually checks that the Add-In is there and does a FILE.DOWNLOAD(addininstaller) if it isn’t.

Problem here is that we need a COM component in order to check the existence of an Add-In, and this COM component will have to run Client side (how else could it inspect the Add-ins folder – doh).

The INIT trigger is executed before anything is sent off to the Client and thus we can install the component and continue opening the page after we have done that. BTW the FILE.DOWNLOAD is NOT going to wait until the user actually finishes the setup program, so we will have to bring up a modal dialog telling the user to confirm that he has completed the setup.

BTW as you probably have figured out by now, the above line requires a registration of Add-Ins like:

ComponentHelper.RegisterAddIn(‘FreddyK.LargeVEControl’,’1c9f7ad47dba024b’,’NAV Large Virtual Earth Control’, ‘NavVEControl.msi’);

In order to specify what file to download. Now I could have added this to the Check function to avoid a table – but I actually don’t think it belongs there.

The ComponentHelper

So, what I have done is to collect some functionality that I find I use all the time in various samples in a Component called the ComponentHelper.

The functions are:

  1. Installation of Client side COM components (used by the majority of samples)
  2. Installation of Client side Add-Ins (used by all samples with Add-ins)
  3. Ability to Escape and Unescape strings (the method Web Services uses for encoding of company name – used in the Virtual Earth Integration)
  4. Ability to register a codeunit or page as Web Service from code (used by all samples using Web Services)
  5. Global information about the URL to my IIS and Web Service tier (used in Edit In Excel and Virtual Earth Integration)
  6. Modify metadata programmatically (all samples)

In fact I am hoping that these basic pieces of functionality will find their way into the base product in the future, where they IMO belong.

Installation of Client side COM components

Every time you use a self built COM component (in this case the NAVAddInHelper), which you want to auto-deploy, you should create a function like this:

LoadAddInHelper(VAR NAVAddInHelper : Automation “‘NAVAddInHelper’.NAVAddInHelper”) Ok : Boolean
Ok := FALSE;
WHILE NOT CREATE(NAVAddInHelper,TRUE,TRUE) DO
BEGIN
IF NOT AskAndInstallCOMComponent(‘NAV AddIn Helper’, ‘NAVAddInHelper.msi’) THEN
EXIT;
END;
Ok := TRUE;

and always invoke this when you want to create an instance of the Component (instead of having CREATE(NAVAddInHelper,TRUE,TRUE) scattered around the code.

AskAndInstallCOMComponent(Description : Text[80];InstallableName : Text[80]) Retry : Boolean
Retry := FALSE;
IF CONFIRM(STRSUBSTNO(TXT_InstallCOMComponent, Description)) THEN
BEGIN
Retry := InstallComponent(InstallableName);
END;

InstallComponent(InstallableName : Text[80]) Retry : Boolean
Retry := TRUE;
toFile := InstallableName;
fromFile := APPLICATIONPATH + ‘ClientSetup’+InstallableName;
IF NOT FILE.EXISTS(fromFile) THEN
BEGIN
fromFile := APPLICATIONPATH + ‘..ClientSetup’+InstallableName;
END;
IF FILE.DOWNLOAD(fromFile, InstallableName, ”, ”, toFile) THEN
BEGIN
Retry := CONFIRM(TXT_PleaseConfirmComplete);
END;

as you can see from the code, the function will try to create the component until it succeeds or the user says No, I do not want to install the component. At this time I would like to mention a small bug in NAV 2009 SP1 – when you try to CREATE a COM component client side and it isn’t there, the Client will still ask you whether or not you want to run a client side component, but since the Control isn’t installed – it doesn’t know what to call it, meaning that you will get:

image

Now it is OK for the user to cancel this window because he doesn’t know what it is, but if he says Never Allow (silly choice to give the user:-)), he will have to delete personalization settings for automation objects to get this working again.

image

BTW If the user declines running a COM component – our code will see this as the component is not installed and ask him to install it.

Installation of Client side Add-Ins

To check whether an Add-in is installed, we first check whether it is registered in the Client’s add-in table.

CheckAddInNameKey(AddInName : Text[220];PublicKeyToken : Text[20]) Found : Boolean
Found := FALSE;
IF NOT AddIn.GET(AddInName,PublicKeyToken) THEN
BEGIN
MESSAGE(STRSUBSTNO(TXT_AddInNotRegisterd, AddInName, PublicKeyToken));
EXIT;
END;
Found := CheckAddIn(AddIn.”Control Add-in Name”, AddIn.”Public Key Token”, AddIn.Description);

Without anything here – nothing works. After this we check our own table (in which we have information about what executable to download to the client)

CheckAddIn(AddInName : Text[220];PublicKeyToken : Text[20];Description : Text[250]) Found : Boolean
IF Description = ” THEN
BEGIN
Description := AddInName;
END;
Found := FALSE;
IF LoadAddInHelper(NAVAddInHelper) THEN
BEGIN
WHILE NOT NAVAddInHelper.CheckAddIn(AddInName, PublicKeyToken) DO
BEGIN
IF NOT InstallableAddIn.GET(AddInName, PublicKeyToken) THEN
BEGIN
IF NOT CONFIRM(STRSUBSTNO(TXT_AddInNotFound, Description)) THEN
BEGIN
EXIT(FALSE);
END;
END
ELSE
EXIT(AskAndInstallAddIn(Description, InstallableAddIn.InstallableName));
END;
Found := TRUE;
END;

and last but not least – the method that installs the Add-In

AskAndInstallAddIn(Description : Text[80];InstallableName : Text[80]) Retry : Boolean
Retry := FALSE;
IF CONFIRM(STRSUBSTNO(TXT_InstallAddIn, Description)) THEN
BEGIN
Retry := InstallComponent(InstallableName);
END;

BTW, the method to register Add-Ins to this subsystem is

RegisterAddIn(“Control Name” : Text[220];”Public Key Token” : Text[20];Description : Text[128];InstallableName : Text[80])
IF NOT AddIn.GET(“Control Name”, “Public Key Token”) THEN
BEGIN
AddIn.INIT();
AddIn.”Control Add-in Name” := “Control Name”;
AddIn.”Public Key Token” := “Public Key Token”;
AddIn.Description := Description;
AddIn.INSERT(TRUE);
END;
IF NOT InstallableAddIn.GET(“Control Name”, “Public Key Token”) THEN
BEGIN
InstallableAddIn.INIT();
InstallableAddIn.”Control Add-in Name” := “Control Name”;
InstallableAddIn.”Public Key Token” := “Public Key Token”;
InstallableAddIn.InstallableName := InstallableName;
InstallableAddIn.INSERT(TRUE);
END;

As you can see I could have extended the AddIn table – but I decided to go for adding a table instead, it doesn’t really matter.

Ability to Escape and Unescape strings

In the Virtual Earth sample, I need to construct a URL, which contains the company name from NAV. Now with NAV 2009SP1 we use standard Escape and Unescape of strings in the URL, so I have added functions to ComponentHelper to do this. In fact, they just call a function in the C# COM component, which contains these functions.

Ability to register a codeunit or page as Web Service from code

Instead of having to ask partners and/or users to register web services in the Web Service table or form, I have created this small function in the ComponentHelper to do this.

RegisterWebService(isPage : Boolean;”Object ID” : Integer;”Service Name” : Text[80];Published : Boolean)
IF isPage THEN
BEGIN
ObjType := WebService.”Object Type”::Page;
END ELSE
BEGIN
ObjType := WebService.”Object Type”::Codeunit;
END;

IF NOT WebService.GET(ObjType, “Service Name”) THEN
BEGIN
WebService.INIT();
WebService.”Object Type” := ObjType;
WebService.”Object ID” := “Object ID”;
WebService.”Service Name” := “Service Name”;
WebService.Published := Published;
WebService.INSERT();
COMMIT;
END ELSE
BEGIN
IF (WebService.”Object ID” <> “Object ID”) OR (WebService.Published<>Published)  THEN
BEGIN
WebService.”Object ID” := “Object ID”;
WebService.Published := Published;
WebService.MODIFY();
COMMIT;
END;
END;

Global information about the URL to my IIS and Web Service tier

Again – a number of the samples I create will integrate from the RoleTailored Client to an application or a web site, which then again uses Web Services. I found out, that I needed a central way to find the URL of the right Web Service listener and the best way was to create a table in which I store the base URL (which would be ://WS/”>://WS/”>http://<server>:<port>/<instance>/WS/ (default http://localhost:7047/DynamicsNAV/WS/).

Also in the Virtual Earth I spawn up a browser (with HYPERLINK) and I need a location for the intranet server on which an application like the MAP would reside.

Modify Metadata programmatically

I found that all my samples worked fine in the W1 version of NAV 2009 SP1, but as soon as I started to install them on other localized version, the pages on which I added actions etc. had been modified by local functionality and since there is no auto merge of pages, people would have to merge page metadata or find themselves loosing local functionality when they installed my samples.

I have added 4 functions:

GetPageMetadata(Id : Integer;VAR Metadata : BigText)

SetPageMetadata(Id : Integer;Metadata : BigText)

AddToMetadata(Id : Integer;VAR Metadata : BigText;Before : Text[80];Identifier : Text[80];Properties : Text[800]) result : Boolean

AddToPage(Id : Integer;VersionList : Text[30];Before : Text[80];Identifier : Text[80];Properties : Text[800]

where the last function just call the three other (Get, Add, Set metadata).

I am not very proud of the way these functions are made – they just search for a line in the exported text file and inserts some metadata but they meet the needs.

As an example on how these functions are used you will find:

// Read Page Metadata
ComponentHelper.GetPageMetadata(PAGE::”Customer Card”, Metadata);

// Add Map Factbox
ComponentHelper.AddToMetadata(PAGE::”Customer Card”, Metadata, ‘    { 1900383207;1;Part   ;’,
‘    { 66031;1  ;Part      ;’,
‘ SubFormLink=No.=FIELD(No.); PagePartID=Page66030 }’)
OR

// Add View Area Map Action
ComponentHelper.AddToMetadata(PAGE::”Customer Card”, Metadata, ‘      { 82      ;1   ;ActionGroup;’, ‘      { 66030   ;2   ;Action    ;’,
‘ CaptionML=[ENU=View Area Map]; OnAction=VAR MAP : Codeunit 66032; BEGIN MAP.OpenCustomerMAPInBrowser(Rec); END; }’);

// Write Page Metadata back
ComponentHelper.SetPageMetadata(PAGE::”Customer Card”, Metadata);

So basically – it reads the metadata for the page, checks whether the action already has been added (the string ‘      { 66030   ;2   ;Action    ;’ exists already). If not it searches for the string ‘      { 82      ;1   ;ActionGroup;’ and inserts the action below that. Not pretty – but it works.

The Visual Studio piece

As mentioned earlier a couple of functions are needed in a client side COM component.

The Escape and Unescape functions really doesn’t do anything:

public string EscapeDataString(string str)
{
return Uri.EscapeDataString(str);
}

public string UnescapeDataString(string str)
{
return Uri.UnescapeDataString(str);
}

and the essence of the CheckAddIn is the code found in the LoadAddIn function of the AddIn class:

Assembly assembly = Assembly.LoadFrom(dll);

this.publicKey = “”;
foreach (byte b in assembly.GetName().GetPublicKeyToken())
{
this.publicKey += string.Format(“{0:x2}”, b);
}

Type[] types = assembly.GetTypes();
foreach (Type type in types)
{
foreach (System.Attribute att in System.Attribute.GetCustomAttributes(type))
{
ControlAddInExportAttribute expAtt = att as ControlAddInExportAttribute;
if (expAtt != null && !string.IsNullOrEmpty(expAtt.Name))
{
if (!isAddIn)
{
this.controlNames = new List<string>();
isAddIn = true;
}
this.controlNames.Add(expAtt.Name);
}
}
}

Which loads an Add-In, finds the public key token and the registered controls. The rest is really simple – check whether one of the Add-Ins in fact is the one we are looking for – else install it…

The Visual Studio solution also contains a setup project for generating the .msi file which needs to be placed in the ClientSetup folder.

Putting it all together

So, now we have a .fob file and an .msi file which we need to install on the Service Tier – so why don’t we create a Setup project, which contains this .fob (install that in a ServerSetup folder) and the .msi (install that in the ClientSetup folder).

Doing this makes installing the ComponentHelper a 3 step process:

  1. Install ComponentHelper.msi on the Service Tier
  2. Import a .fob from the ServerSetup folder
  3. Run a codeunit which registers the necessary stuff

In fact I am trying to make all the demos and samples installable like the ComponentHelper itself – so that anybody can download cool samples and get a sexy Microsoft Dynamics NAV 2009 SP1 – to work with.

ComponentHelper1.01.zip (which contains ComponentHelper1.01.msi) can be downloaded here.

If you don’t fancy downloading the .msi (for whatever reason) – the source to NAVAddHelper can be downloaded here and the ComponentHelper objects can be downloaded here.

 

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Multiple Service Tiers – SP1

Right around the release of Microsoft Dynamics NAV 2009, I wrote a blog entry with some .bat files on how to create multiple Service Tiers when working with NAV 2009.

The blog post is here: http://blogs.msdn.com/freddyk/archive/2008/10/29/multiple-service-tiers.aspx

Now NAV 2009 SP1 is about to be released, it is time for a small update. One of the files of the package is a CustomSettings.template file, which really is just the CustomSettings.config with a few values replaced with variable names, so that we can replace those automagically.

Now in SP1, the CustomSettings.config has changed – new keys have been added and we also support named instances in the database.

SP1 will actually run with the old config file, so we could just ignore the entire thing and continue as if nothing happened – the .bat files will still work in SP1.

However – if we want to take advantage of the named instances in SQL Server or we want to have the additional keys available for modifying we need to change something.

I have created a new CustomSettings.template based on the SP1 config file – copy the config file and change the following keys:

    <add key=”DatabaseServer” value=”#DBSERVER#”></add>
<add key=”DatabaseInstance” value=”#DBINSTANCE#”></add>
<add key=”DatabaseName” value=”#DATABASE#”></add>
<add key=”ServerInstance” value=”#INSTANCE#”></add>

and extended the createservice.bat file to also allow a database instance to be specified, meaning that the usage is now:

CreateService name [dbserver] [“dbinstance”] [“dbname”] [demand|auto|disabled] [both|servicetier|ws]

The new .zip file is available for download here.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV