Connecting to NAV Web Services from C# using Web Reference

Prerequisites

Please read this post to get a brief explanation of the scenario I will implement in C# using Web References.

For C# we can leave the Service Tier running Negotiate or we can use Ntlm as PHP and Java. In this example I will assume that the Service Tier is running SPNEGO (which is the default)

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

Version and download

I am using Visual Studio 2008 professional with SP1 when writing this sample, to be honest I have NOT tried to see whether this will work in the Express versions of Visual Studio, but they do have Service Reference and Web Reference so I cannot see why not.

What is the difference between a Web Reference and a Service Reference?

In short, the Web Reference is a .net 2.0 compatible Web Service reference, the Service Reference is a .net 3.5 WCF based Service Reference.

Add Web Reference is a wrapper over wsdl.exe and can be used to create proxies for .NET 1.1 or 2.0 clients. Of course this means when you are pointing to a WCF service you have to be pointing to an endpoint that uses basicHttpBinding.

Add Service Reference is a wrapper over svcutil.exe and also creates clients proxies. These proxies, however, can only be consumed by .NET 3.5 clients.

How to add Web References

Select Add Service Reference

image

Click Advanced

image

Click Add Web Reference

image

Type in the URL and specify the namespace for the SystemService Web Reference

image

and for the Customer Page Web Reference

image

When adding a Web Reference, Visual Studio will create a config file in which it stores stuff like the URL for the Reference. In my samples I will set the URL in code and due to this, the config file is not needed.

Authentication

In the following sample I use Windows Authentication. In Web References you just need to set the property UseDefaultCredentials in the service class to true, then .net will automatically try to use your windows credentials to connect to the Web Service.

If you want to connect to a Web Reference using a specific username/password you need to exchange this line:

someService.UseDefaultCredentials = true;

with this:

someService.Credentials = new NetworkCredential(“user”, “password”, “domain”);

The code

First a couple of using statements (including the two reference namespaces) and the main body of a console app:

using System;
using System.Net;
using testApp.CustomerPageRef;
using testApp.SystemServiceRef;

namespace testApp
{
class Program
{
static void Main(string[] args)
{
// main program code
        }
    }
}

The main code follows

First, connect to the System Web Service and list all companies:

string baseURL = “http://localhost:7047/DynamicsNAV/WS/”;

SystemService systemService = new SystemService();
systemService.Url = baseURL + “SystemService”;
systemService.UseDefaultCredentials = true;

Console.WriteLine(“Companies:”);
string[] companies = systemService.Companies();
foreach (string company in companies)
Console.WriteLine(company);
string cur = companies[0];

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:

string customerPageURL = baseURL + Uri.EscapeDataString(cur) + “/Page/Customer”;
Console.WriteLine(“nURL of Customer Page: ” + customerPageURL);

and then I can create a Service Class to the Customer Page:

Customer_Service customerService = new Customer_Service();
customerService.Url = customerPageURL;
customerService.UseDefaultCredentials = true;

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

Customer cust10000 = customerService.Read(“10000”);
Console.WriteLine(“nName of Customer 10000: ” + cust10000.Name);

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

Customer_Filter filter1 = new Customer_Filter();
filter1.Field = Customer_Fields.Country_Region_Code;
filter1.Criteria = “GB”;

Customer_Filter filter2 = new Customer_Filter();
filter2.Field = Customer_Fields.Location_Code;
filter2.Criteria = “RED|BLUE”;

Console.WriteLine(“nCustomers in GB served by RED or BLUE warehouse:”);
Customer_Filter[] filters = new Customer_Filter[] { filter1, filter2 };
Customer[] customers = customerService.ReadMultiple(filters, null, 0);
foreach (Customer customer in customers)
Console.WriteLine(customer.Name);

Console.WriteLine(“nTHE END”);
Console.ReadLine();

All of the above will output the following to a console prompt (on my machine running NAV 2009SP1 W1)

image

I hope this is helpful.

Good luck

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Connecting to NAV Web Services from Java

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 Java.

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

Version and download

Java does not natively have support for the SPNEGO authentication protocol, but it does have support for NTLM authentication, so you will have to change the Web Services listener to use that.

I downloaded the Java developer toolkit from http://www.java.com/en/download/manual.jsp and installed it under c:\sun – and made sure that c:\sun\SDK\bin is added to the path.

wsimport – import WSDL

To make Java able to see and use webservices I use wsimport to create proxy classes in Java. In a directory, I have created two empty folders (generated and source) and use the following command:

C:\java>wsimport -d generated -s source http://localhost:7047/DynamicsNAV/WS/SystemService

this will create a series of .class files under

C:\java\generated\schemas\dynamics\microsoft\nav\system

and a set of source files under

C:\java\source\schemas\dynamics\microsoft\nav\system

note, that the folder structure is the namespace of the web service.

javac – compile the program

Assuming that I create a test program in c:\java\test.java, the following command will compile the program:

c:\java>c:\sun\sdk\jdk\bin\javac -cp .;generated test.java

this creates a c:\java\test.class, which we can run.

java – run the program

The statement used to run the java class is:

c:\java>c:\sun\sdk\jdk\bin\java -cp .;generated test

enough about setup – lets code…

Authentication

By default, java supports Windows Authentication and if the class is running in context of a windows user, these credentials are automatically used to authenticate towards NAV Web Services.

If you want to use different credentials (connect as a different user) you will need to create a class derived from Authenticator and implement getPasswordAuthentication like:

static class MyAuthenticator extends Authenticator {
public PasswordAuthentication getPasswordAuthentication() {
return (new PasswordAuthentication(“domainuser”, new char[] {‘p’,’a’,’s’,’s’,’w’,’o’,’r’,’d’}));
}
}

and then as the very first statement in the class

Authenticator.setDefault(new MyAuthenticator());

test.java

First of all, I have a number of imports. Not sure that they all are needed and then a main function body:

import java.net.*;
import java.util.*;
import java.io.*;
import javax.xml.namespace.QName;
import schemas.dynamics.microsoft.nav.system.*;
import schemas.dynamics.microsoft.page.customer.*;

public class test {
public static void main(String[] args) {

        // main program code

    }
}

The main code is:

try {
String baseURL = “http://localhost:7047/DynamicsNAV/WS/”;

The following code allows you to connect to NAV Web Services system service in Java and output the companies available on the service tier:

  URL systemServiceURL = new URL(baseURL + “SystemService”);
QName systemServiceQName = new QName(“urn:microsoft-dynamics-schemas/nav/system/”, “SystemService”);
SystemService systemService = new SystemService(systemServiceURL, systemServiceQName);
SystemServicePort systemPort = systemService.getSystemServicePort();
List companies = systemPort.companies();
System.out.println(“Companies:”);
for(String company : companies) {
System.out.println(company);
}
String cur = companies.get(0);

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:

  URL customerPageURL = new URL(baseURL+URLEncoder.encode(cur,”UTF-8″).replace(“+”,”%20″)+”/Page/Customer”);
QName customerPageQName = new QName(“urn:microsoft-dynamics-schemas/page/customer”, “Customer_Service”);

  System.out.println(“nURL of Customer page:” + customerPageURL.toString());

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

  CustomerService customerService = new CustomerService(customerPageURL, customerPageQName);
CustomerPort customerPort = customerService.getCustomerPort();

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

  Customer cust = customerPort.read(“10000”);
System.out.println(“nName of Customer 10000:” + cust.getName());

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

  CustomerFilter filter1 = new CustomerFilter();
filter1.setField(CustomerFields.fromValue(“Location_Code”));
filter1.setCriteria(“RED|BLUE”);

  CustomerFilter filter2 = new CustomerFilter();
filter2.setField(CustomerFields.fromValue(“Country_Region_Code”));
filter2.setCriteria(“GB”);

  List filters = new ArrayList();
filters.add(filter1);
filters.add(filter2);

  System.out.println(“nCustomers in GB served by RED or BLUE warehouse:”);
CustomerList customers = customerPort.readMultiple(filters, null, 0);
for (Customer c : customers.getCustomer()) {
System.out.println(c.getName());
}

  System.out.println(“nTHE END”);

} catch(MalformedURLException e) {
} catch(UnsupportedEncodingException e) {
}

All of the above will output the following in a prompt (on my machine running NAV 2009SP1 W1)

image

I hope this is helpful.

Good luck

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

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

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

Displaying Company information in Card pages

I received a question from a customer, who is running multiple companies and often have multiple instances of NAV open with different companies. On the main page, they do have information about what company is the active:

image

but when they have opened a number of Task Pages in the various instances of NAV, they cannot distinguish one from the other.

Example:

image

This image tells you the page and the Customer name – and you can easily identify the right page when Alt+TAB’ing between pages, but if you have multiple companies this doesn’t help you a lot.

So what determines the caption?

The fields used in the caption on a page is determined by:

DataCaptionExpr on the page. This is an expression, which can use fields, functions etc. to build up a caption. If that isn’t defined, the client looks for

DataCaptionFields on the page. This is a collection of fields, which are used to build the caption by adding them together with a character 183 (middle dot) between them. If that isn’t defined, the client looks for

DataCaptionFields on the table, which basically is the same as DataCaptionFields on the page.

In a standard NAV, there is no DataCaptionExpr nor DataCaptionFields defined on the Customer Card, but on the Customer table you find:

image

In order to add the Company name behind the caption you will need to change the DataCaptionExpr on the Customer Card to f.ex.

“No.” + ‘ · ‘ + Name + ‘ [‘+COMPANYNAME+’]’

which would cause the Customer Card to look like

image

You can of course select to change the expression to whatever you like – or maybe create some function, which automagically returns a caption, only real flipside is that you need to modify the card pages, on which you need this functionality. In the end this is probably not a very large number.

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