Connecting to NAV Web Services from the Cloud–part 5 out of 5

If you haven’t already read part 4 (and the prior parts) you should do so here, before continuing to read this post.

In this post, I am going to create a small Windows Phone 7 application, which basically will be a phone version of the sidebar gadgets from this post. Continue reading

Web Services Infrastructure and how to Create an Internal Proxy

I am NOT an expert in how to setup a secure network and I do NOT know a lot about firewalls, DMZ setup and all of these things, but I have seen a lot in my 25 years of working with computers and the following (absolutely non-exhaustive) gives a good picture of a common network situation of companies, who wants to interact with customers and partners through Web Applications and/or Web Services. Continue reading

Connecting to NAV Web Services from Silverlight 3

Please read this post to get a brief explanation of the scenario I will implement in Silverlight. Yes, yes – I know it isn’t a fancy graphical whatever as Silverlight should be, but to be honest – I would rather do something crappy on purpose than trying to do something fancy and everybody would find it crappy anyway:-)

Getting started with Silverlight

http://silverlight.net/getstarted – is your friend. Go to the web site and click this button:

image_5

Or click the image above directly.

Within a few seconds you will find yourself installing all the free tools you need to start developing Silverlight applications.

On the getstarted web site you will also find videos and walkthroughs on how to develop in Silverlight.

Silverlight is .net and c# so really guys… – how hard can it be?

That was what I thought!

So I just downloaded the Silverlight development platform and started coding and as soon as I tried to connect to NAV Web Services I ran into the showstopper:

image_2 (1)

Meaning that for a Silverlight application to be able to communicate with NAV Web Services – it needs to be deployed in the same location as NAV Web Services – http://localhost:7047 – that doesn’t really sound like a good idea.

On MSDN i found this article explaining about this in detail: http://msdn.microsoft.com/en-us/library/cc645032(VS.95).aspx

Silverlight needs permission by the Web Services host to access the Web Service – it kind of seems like overkill due to the fact that our web services are authenticated with Windows Authentication but I guess there are other services where this makes sense.

To make a long story short – if connecting to http://localhost:7047/DynamicsNAV/WS/SystemService – then Silverlight will first try to download http://localhost:7047/clientaccesspolicy.xml and check whether this is OK, but as you can imagine – NAV doesn’t do that:-(

clientaccesspolicy.xml

So if NAV doesn’t support that – how do we get around this obstacle? (of course you know that there is a way – else you wouldn’t be reading this and I wouldn’t be writing it)

The trick is just to create a small windows service that does nothing but host this file. We are lucky that the endpoint of NAV Web Services is http://localhost:7047/DynamicsNAV – and everything underneath that – so I should be able to create a WCF Service hosting just the xml file on http://localhost:7047

NAV Policy Server

I have created a small project called the NAV Policy Server. It is a Windows Service, hosting a WCF Service that will service a “allow all” version of clientaccesspolicy.xml, making Silverlight 3 able to connect to NAV Web Services.

You can read here about how to create a Windows Service (including how to create Setup functionality in the Service). The main program of the Windows Service is here:

using System; 
using System.ComponentModel; 
using System.ServiceProcess; 
using System.ServiceModel; 
using System.ServiceModel.Description; 
using System.Xml; 
using System.Reflection; 
using System.IO;

namespace NAVPolicyServer 
{ 
    public partial class NAVPolicyService : ServiceBase 
    { 
        ServiceHost host;

        public NAVPolicyService() 
        { 
            InitializeComponent();

            string WebServicePort = "7047"; 
            bool WebServiceSSLEnabled = false;

            // Read configuration file 
            XmlDocument doc = new XmlDocument(); 
            doc.Load(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase), "CustomSettings.config")); 
            XmlNode webServicePortNode = doc.SelectSingleNode("/appSettings/add[@key='WebServicePort']"); 
            WebServicePort = webServicePortNode.Attributes["value"].Value; 
            XmlNode webServiceSSLEnabledNode = doc.SelectSingleNode("/appSettings/add[@key='WebServiceSSLEnabled']"); 
            WebServiceSSLEnabled = webServiceSSLEnabledNode.Attributes["value"].Value.Equals("true", StringComparison.InvariantCultureIgnoreCase);

            // Base listening address 
            string BaseURL = (WebServiceSSLEnabled ? Uri.UriSchemeHttps : Uri.UriSchemeHttp) + Uri.SchemeDelimiter + System.Environment.MachineName + ":" + WebServicePort;

            // Initialize host 
            this.host = new ServiceHost(new PolicyRetriever(), new Uri(BaseURL)); 
            this.host.AddServiceEndpoint(typeof(IPolicyRetriever), new WebHttpBinding(false ? WebHttpSecurityMode.Transport : WebHttpSecurityMode.None), "").Behaviors.Add(new WebHttpBehavior()); 
        }

        protected override void OnStart(string[] args) 
        { 
            if (host.State != CommunicationState.Opened && host.State != CommunicationState.Opening) 
            { 
                host.Open(); 
            } 
        }

        protected override void OnStop() 
        { 
            if (host.State != CommunicationState.Closed && host.State != CommunicationState.Closing) 
            { 
                host.Close(); 
            } 
        } 
    } 
}

As you can see, the Service needs to be installed in the Service Tier directory of the Web Service listener you want to enable for Silverlight as it reads the CustomSettings.config file to find the port number and whether or not it uses SSL.

After this it creates a ServiceHost bases on the PolicyRetriever class with a WebHttpBinding endpoint at the base URL, here http://machine:7047. In the endpoint you specify the interface (IPolicyRetriever) this endpoint services and this interface is implemented by the PolicyRetriever class.

The actual code is something I found on Carlos’ blog – http://blogs.msdn.com/carlosfigueira/archive/2008/03/07/enabling-cross-domain-calls-for-silverlight-apps-on-self-hosted-web-services.aspx

The IPolicyRetriever interface is the contract and it looks like:

[ServiceContract] 
public interface IPolicyRetriever 
{ 
    [OperationContract, WebGet(UriTemplate = "/clientaccesspolicy.xml")] 
    Stream GetSilverlightPolicy(); 
    [OperationContract, WebGet(UriTemplate = "/crossdomain.xml")] 
    Stream GetFlashPolicy(); 
}

As you can see we host two files – clientaccesspolicy.xml for Silverlight and crossdomain.xml for flash.

The PolicyRetriever class (the Service) itself is implemented as a singleton and looks like:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] 
public class PolicyRetriever : IPolicyRetriever 
{ 
    public PolicyRetriever() 
    { 
    }

    /// <summary> 
    /// Create a UTF-8 encoded Stream based on a string 
    /// </summary> 
    /// <param name="result"></param> 
    /// <returns></returns> 
    private Stream StringToStream(string result) 
    { 
        WebOperationContext.Current.OutgoingResponse.ContentType = "application/xml"; 
        return new MemoryStream(Encoding.UTF8.GetBytes(result)); 
    }

    /// <summary> 
    /// Fetch policy file for Silverlight access 
    /// </summary> 
    /// <returns>Silverlight policy access xml</returns> 
    public Stream GetSilverlightPolicy() 
    { 
        string result = @"<?xml version=""1.0"" encoding=""utf-8""?> 
<access-policy> 
    <cross-domain-access> 
        <policy> 
            <allow-from http-request-headers=""*""> 
                <domain uri=""*""/> 
            </allow-from> 
            <grant-to> 
                <resource path=""/"" include-subpaths=""true""/> 
            </grant-to> 
        </policy> 
    </cross-domain-access> 
</access-policy>"; 
        return StringToStream(result); 
    }

    /// <summary> 
    /// Fetch policy file for Flash access 
    /// </summary> 
    /// <returns>Flash policy access xml</returns> 
    public Stream GetFlashPolicy() 
    { 
        string result = @"<?xml version=""1.0""?> 
<!DOCTYPE cross-domain-policy SYSTEM ""http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd""> 
<cross-domain-policy> 
    <allow-access-from domain=""*"" /> 
</cross-domain-policy>"; 
        return StringToStream(result); 
    } 
}

The way you make a WCF service a singleton is by specifying an instance of the class to the ServiceHost and set InstanceContextMode to single in the ServiceBehavior Attribute.

That is actually all it takes, installing and starting this service will overcome the connection issue.

The NAVPolicyServer solution can be downloaded here and the compiled .msi (installable) can be downloaded here.

Now… – Connecting to NAV Web Services from Silverlight

Having overcome the connection issue – it is really just to write our Silverlight application.

Create a Silverlight application, insert a StackPanel and a ListBox named output in the .xaml file, add service references and write code.

You will quickly notice, that there is nothing called Add Web Reference – only Add Service Reference – and when you have done do, you will notice that all the functions that you normally invoke are missing…

This is because Silverlight only supports Asynchronous Service access – so much for just creating my standard flow of my app.

Another thing that has changed significantly is what you need to do in order to make a Service Reference work. If you look at my earlier posts with C# and Service References, you can see that I need to setup the binding manually and add endpoints etc. Even if I wanted to do it in a config file (like here), you needed to make a lot of changes to the config file (adding behaviors etc.)

In Silverlight you just add the Service Reference and start partying like:

SystemService_PortClient systemService = new SystemService_PortClient(); 
systemService.CompaniesAsync();

works right away, no changes needed – THAT’s nice. In my sample I do however build the URL up dynamically, meaning that my construction of the systemService looks like:

SystemService_PortClient systemService = new SystemService_PortClient("SystemService_Port", new EndpointAddress(baseURL + "SystemService"));

Which basically just tells it to read the configuration section and overwrite the endpoint address – still pretty simple.

Async

Whenever you call CompaniesAsync – it returns immediately and after a while the event connected to CompaniesCompleted is triggered. The way I like to do this is to do a inline delegate as an event trigger and just specify my code right there.

My scenario should first list the companies, calculate a customer page URL, read customer 10000 and then read customers with location code BLUE or RED in GB.

public partial class MainPage : UserControl 
{ 
    private string baseURL = "http://localhost:7047/DynamicsNAV/WS/"; 
    private string customerPageURL;

    public MainPage() 
    { 
        InitializeComponent();

        SystemService_PortClient systemService = new SystemService_PortClient("SystemService_Port", new EndpointAddress(baseURL + "SystemService")); 
        systemService.CompaniesCompleted += delegate(object sender, CompaniesCompletedEventArgs e) 
        { 
            display("Companies:"); 
            for (int i = 0; i < e.Result.Length; i++) 
                display(e.Result[i]); 
            string cur = e.Result[0];

            this.customerPageURL = baseURL + Uri.EscapeDataString(cur) + "/Page/Customer"; 
            display(" "); 
            display("URL of Customer Page:"); 
            display(customerPageURL);

            FindCustomer10000(); 
        };
        systemService.CompaniesAsync(); 
    }

    void display(string s) 
    { 
        this.output.Items.Add(s); 
    }
}

As you can see, I do not call the FindCustomer10000 before I am done with step 1.

I could have inserted that call after the call to CompaniesAsync – but then the customerPageURL variable would not be initialized when starting to connect to the customer page.

FindCustomer10000 looks like:

private void FindCustomer10000() 
{ 
    Customer_PortClient readCustomerService = new Customer_PortClient("Customer_Port", new EndpointAddress(customerPageURL)); 
    readCustomerService.ReadCompleted += delegate(object sender, ReadCompletedEventArgs e) 
    { 
        display(" "); 
        display("Name of Customer 10000: " + e.Result.Name);

        FindCustomers(); 
    };
    readCustomerService.ReadAsync("10000"); 
}

Again – when we have data and we are done – call FindCustomers, which looks like:

private void FindCustomers() 
{ 
    Customer_PortClient readMultipleCustomerService = new Customer_PortClient("Customer_Port", new EndpointAddress(customerPageURL)); 
    readMultipleCustomerService.ReadMultipleCompleted += delegate(object sender, ReadMultipleCompletedEventArgs e) 
    { 
        display(" "); 
        display("Customers in GB served by RED or BLUE warehouse:"); 
        foreach (Customer customer in e.Result) 
            display(customer.Name);
        display(" "); 
        display("THE END");
    }; 
    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"; 
    Customer_Filter[] filters = new Customer_Filter[] { filter1, filter2 }; 
    readMultipleCustomerService.ReadMultipleAsync(filters, null, 0); 
}

If you try to move the call to FindCustomers up after the call to FindCustomer10000 then you will see that it isn’t always determined which of the two methods complete first, meaning that the order of things in the listbox will be “random”.

As you can see, the NAVPolicyServer is really the thing that makes this easy and possible – I will send a mail to my colleague who is the Program Manager for Web Services and ask him to include a way of serving policies from NAV automatically – until then, you will need the policy server (which is free and available right here).

Running the Silverlight application will perform the following output:

image_6

BTW – the Silverlight application can be downloaded here.

Hopefully this can be used to create some cool visual Silverlight applications:-)

Good luck

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Connecting to NAV Web Services from Windows Mobile 6.5

It is kind of embarrassing that I write a post about how to connect to NAV Web Services from Windows Mobile 6, when Windows Mobile 6.5 has been out for almost half a year (that’s how much a gadget person I am:-))

I just downloaded the 6.5 SDK from here and tried out the exact same application as I wrote for Windows Mobile 6 in this post and everything seems to work just fine, so please follow the steps in this post to see how to connect from Windows Mobile 6.5 to NAV Web Services.

image_2 (2)

Is it faster?

My tests on 6.5 is only on the emulator – but as far as I can see, it is definitely faster. The 1.5 second from Windows Mobile 6 is now down to 0.9 second and 0.8 second with PreAuthenticate set to true.

I will still do some more investigations on performance from Windows Mobile Web Services.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Connecting to NAV Web Services from Windows Mobile 6

I have created my very first Windows Mobile App!

image_2 (3)

This is running in an Emulator using the Professional SDK.

I also tried to deploy the solution to my physical device (my Smartphone), which also worked:

Smartphone_2

To be honest, the biggest challenge is to setup everything so that you can get going.

A couple of useful links to get going

Location to download Windows Mobile 6 SDK: http://www.microsoft.com/downloads/details.aspx?FamilyID=06111a3a-a651-4745-88ef-3d48091a390b&DisplayLang=en

The Windows Mobile 6 SDK is not included in Visual Studio 2008, you will have to download and install it.

 

Create your first WM6 App: http://code.msdn.microsoft.com/WM6YourFirstApp

A good video to help you get started.

 

Windows Mobile Development Center: http://msdn.microsoft.com/en-us/windowsmobile/default.aspx

This contains a lot of good links for getting started and how to do things.

 

Security Configuration Manager: C:\Program Files\Windows Mobile 6 \SDK\Tools\Security\Security Powertoy

When you have installed the SDK – go to this location and install the security configuration manager to be able to setup your device so that you can deploy your solution and debug it.

 

Note: I did struggle quite a bit to get network access up running on the device and on the emulator, but once I got the emulator setup to have network access (connected to Internet – not Work) and I had access through the firewall to my host machine – then everything worked fine.

The scenario

Please read this post to get a brief explanation of the scenario I will implement on a Windows Mobile Device.

.net 3.5

We will use 3.5 of the compact .net framework to build our application and whether you select Professional (first picture) or Standard (second picture) really doesn’t matter. First thing I do is to create two Web References from my app to the two Web Services i use in my scenario – SystemService (SystemServiceRef) and Customer Page (CustomerPageRef).

These Web References are pretty similar to .net 2.0 Web References from the normal .net framework (look this post). One thing to note is, that you do not have UseDefaultCredentials in the compact framework so you need to specify user and password when connecting to NAV Web Services.

The project type is a Device Application and the code on the form is:

using System; 
using System.Windows.Forms; 
using SmartDeviceProject4.SystemServiceRef; 
using SmartDeviceProject4.CustomerPageRef; 
using System.Net;

namespace SmartDeviceProject4 
{ 
    public partial class Form1 : Form 
    { 
        public Form1() 
        { 
            InitializeComponent();

            string baseURL = ":7047/DynamicsNAV/WS/";'>http://<IP>:7047/DynamicsNAV/WS/"; 
            NetworkCredential credentials = new NetworkCredential(user, password, domain);

            SystemService systemService = new SystemService(); 
            systemService.Credentials = credentials; 
            systemService.Url = baseURL + "SystemService"; 
            systemService.PreAuthenticate = true;

            display("Companies:"); 
            string[] companies = systemService.Companies(); 
            foreach (string company in companies) 
                display(company); 
            string cur = companies[0];

            string customerPageURL = baseURL + Uri.EscapeDataString(cur) + "/Page/Customer"; 
            display(""); 
            display("URL of Customer Page:"); 
            display(customerPageURL);

            Customer_Service customerService = new Customer_Service(); 
            customerService.Credentials = credentials; 
            customerService.Url = customerPageURL; 
            customerService.PreAuthenticate = true;

            Customer customer10000 = customerService.Read("10000"); 
            display(""); 
            display("Name of customer 10000:"); 
            display(customer10000.Name);

            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";

            display(""); 
            display("Customers 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) 
                display(customer.Name);

            display(""); 
            display("THE END"); 
        }

        private void display(string s) 
        { 
            this.textBox1.Text += s + "\r\n"; 
        } 
    } 
}

As you can see 99% of the code is similar to the post about C# and Web References (found here). Major differences are that the baseURL of course isn’t localhost (since localhost would be the mobile device itself) and I have to setup credentials in the beginning.

But… It is Very Slow!

Having done this and finally have everything working, you will probably find that Web Services from a mobile device is extremely slow.

Call a service to get Customer 10000 takes approx. 1.5 second – and it is not getting faster if you do it 100 times.

If you set service.preAuthenticate to true – then the time is down to 1.2 second, but still – slower than I would like.

I tried to create a standard .net Web Service on my host computer (asmx web service – just the Hello World sample) and tried to call this method 100 times and in this case, the time was down to around 0.5 second pr. call – still very slow, but more acceptable.

When running some of the other applications a call to a webservice (including authorization) is only around 0.04 seconds on my computer so we are looking at around 30 times slower pr. web service call from a mobile device.

I also tried to make my Hello World application return a 10k string – this didn’t affect the performance at all – and when upping the size of the string to 40k – the time climbed to around 0.7 second pr. call – it seems like the biggest problem is latency (only guessing).

I will do some more investigation on this – including contacting the mobile team in Microsoft to figure out why and how to fix this (if possible).

For now the solution seems to be to create some proxy (with a very limited instruction set = one method for each high level thing the mobile device is capable of doing) running with no authentication and then have the mobile devices communicate with that – maybe using some kind of poor mans authentication – or simply having IP security on the Web Service.

I hope this is helpful.

Good luck

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Logging the XML generated from .net or received from NAV WS

When working with Web Services using languages who doesn’t natively have Web Services support (like Javascript and NAV self) you have to create a SOAP envelope yourself in the correct format.

Of course you can do so by looking at the WSDL, understanding SOAP and using theory – or… – you can create a small C# application, invoke the Web Service you want to and see what XML .net creates for this.

You can also see what XML you get back before .net makes this into classes and feed you something high level.

I only know how to do this with a .net 3.5 application using Service References. I don’t know how to hook into an application using Web References.

I will use the application I created in this post, and basically what we have to do is, to create and plug-in a behavior that prints out the XML.

Endpoint Behaviors

An Endpoint Behavior is a class implementing the IEndpointBehavior interface. This interface consists of 4 methods:

  • public void AddBindingParameters(ServiceEndpoint endpoint,
    System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
  • public void ApplyClientBehavior(ServiceEndpoint endpoint,
    System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
  • public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
    System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
  • public void Validate(ServiceEndpoint endpoint)

In my implementation I will leave all empty except for ApplyClientBehavior, where I get access to the ClientRuntime object.

This object has a collection of MessageInspectors – and this is where I want to hook up.

A Client Message Inspector

A Client Message Inspector is a class implementing the IClientMessageInspector interface. This interface consists of 2 methods:

  • public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply,
    object correlationState)
  • public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request,
    System.ServiceModel.IClientChannel channel)

and what I want to do in both these methods is to print the contents of the reply/request to the console.

Now there is a special consideration to take here – as soon as you have taken a copy of the message, you have to replace the message with a new one (even if it is a copy of the old one) – else you will get the following error:

image

Actually a very explanatory error message, but kind of weird.

The Code

I created a .cs file and added the following two classes to the file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;
using System.Xml;
using System.IO;

namespace testAppWCF
{
public class MyBehavior : IEndpointBehavior
{

        #region IEndpointBehavior Members

        public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}

        public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(new MyMessageInspector());
}

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
}

        public void Validate(ServiceEndpoint endpoint)
{
}

        #endregion
}

    public class MyMessageInspector : IClientMessageInspector
{
#region IClientMessageInspector Members

        public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
reply = buffer.CreateMessage();
Message msg = buffer.CreateMessage();
StringBuilder sb = new StringBuilder();
XmlWriter xw = XmlWriter.Create(sb);
msg.WriteBody(xw);
xw.Close();
Console.WriteLine(“Received:n{0}”, msg.ToString());
Console.WriteLine(“Body:n{0}”, sb.ToString());
}

        public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
{
MessageBuffer buffer = request.CreateBufferedCopy(Int32.MaxValue);
request = buffer.CreateMessage();
Console.WriteLine(“Sending:n{0}”, buffer.CreateMessage().ToString());
return null;
}

        #endregion
}
}

Note that the AfterReceiveReply takes special consideration as the actual body comes as a stream and in order to output that I create a XmlWriter and write the body to a string through that one before outputting it to the console.

Adding the behavior to the endpoint

In the main application, we then have to add this to the endpoint, which is done by adding the following line:

systemService.Endpoint.Behaviors.Add(new MyBehavior());

to the code after initialization of the systemService and

customerService.Endpoint.Behaviors.Add(new MyBehavior());

to the code after initialization of the customerService.

F5

When running the application, our console now looks somewhat different.

image

If you look closely you can find Companies: at the top and a list of the companies 3/4’s down.

Everything in between is what is send AND what is received from Web Services. Of course dumping this to the Console isn’t necessarily useful but I assume that you can find ways to dump this to something else if you need.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Connecting to NAV Web Services from C# using Service Reference (code version)

You should read the post about connecting to NAV Web Services from C# using Service Reference (config file version) before continuing here.

Code is king

As you saw in the other post, the config file was pretty complicated and although it is editable by hand and as such could be modified at installtime or whatever, I actually prefer to capture a number of these settings in code and only have very specific values in a config file (like f.ex. the base URL).

In NAV you would never have the full Service URL on all services in a config file. This would mean that you would have to change company in a number of locations in a config file – that just doesn’t fly.

If we have a look at the config file once more, you will see that there is a Binding configuration, specifying a BasicHttpBinding with a couple of settings. If we want to create this binding in code, it would look like:

// Create a NAV comatible binding
BasicHttpBinding navWSBinding = new BasicHttpBinding();
navWSBinding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly;
navWSBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows;

Having this binding class, we can now create a systemService Service Client simply by:

SystemService_PortClient systemService = new SystemService_PortClient(navWSBinding, new EndpointAddress(baseURL + “SystemService”));

Specifying the endpoint address to the constructor.

The only other thing we need to specify is the endpoint behavior to allow delegation. This is done in code by:

systemService.ClientCredentials.Windows.AllowedImpersonationLevel =
System.Security.Principal.TokenImpersonationLevel.Delegation;
systemService.ClientCredentials.Windows.AllowNtlm = true;

Using code like this actually makes the app.config obsolete and you can delete the config file totally when running the below application.

The entire application

The following is a listing of the full console application using code to set all properties and no app.config is necessary (nor used at all):

using System;
using System.Net;
using testAppWCF.SystemServiceRef;
using testAppWCF.CustomerPageRef;
using System.ServiceModel;

namespace testAppWCF
{
class Program
{
static void Main(string[] args)
{
string baseURL = “
http://localhost:7047/DynamicsNAV/WS/”;

            // Create a NAV compatible binding
BasicHttpBinding navWSBinding = new BasicHttpBinding();
navWSBinding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly;
navWSBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows;

            // Create the SystemService Client
SystemService_PortClient systemService = new SystemService_PortClient(navWSBinding, new EndpointAddress(baseURL + “SystemService”));
systemService.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Delegation;
systemService.ClientCredentials.Windows.AllowNtlm = true;

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

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

            // Create the Customer Page Service Client
Customer_PortClient customerService = new Customer_PortClient(navWSBinding, new EndpointAddress(customerPageURL));
customerService.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Delegation;
customerService.ClientCredentials.Windows.AllowNtlm = true;

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

            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();
}
}
}

If you need to specify a different username / password it is done in the same way as it was done for Service References using config files.

This application will output exactly the same as the application using Service References and a config file, in the end the config file is just a number of settings which will be used to instantiate a number of classes from – giving the same result.

I hope this is helpful.

Good luck

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Connecting to NAV Web Services from C# using Service Reference (config file version)

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.

In this post I will describe how to use Service References, where all settings are stored in the .config file.

How to add a Service Reference

Select Add Service Reference

image_thumb[13]

Type the URL and namespace for the SystemService Service Reference:

image

and for the Customer Page Service Reference:

image

The .config file

After having added these Service References, all the properties and settings about the references are stored in app.config (which gets autocreated by Visual Studio). The Proxy classes are generated and everything seems fine until you start using it.

You have to change a couple of things in the app.config file before using these.

Under every binding configuration setting you will find a section like this:

<security mode=”None”>
<transport clientCredentialType=”None” proxyCredentialType=”None”
realm=””>
<extendedProtectionPolicy policyEnforcement=”Never” />
</transport>
<message clientCredentialType=”UserName” algorithmSuite=”Default” />
</security>

this does not match whats needed for NAV Web Services. NAV Web Services absolutely do not run without security. You will have to change this section with:

<security mode=”TransportCredentialOnly”>
<transport clientCredentialType=”Windows”  />
</security>

which matches the security mode and transport of the binding used by NAV when using Windows Authentication (SPNEGO). If the Service Tier is setup to run Ntlm – the ClientCredentialType needs to be “Ntlm” in the config file.

Furthermore you will have to add a behavior indicating that the Web Service Listener is allowed to use Delegation on your credentials (between </bindings> and <client>:

<behaviors>
<endpointBehaviors>
<behavior name=”allowDelegation”>
<clientCredentials>
<windows allowedImpersonationLevel=”Delegation”
allowNtlm=”true”/>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>

and last, but not least you will have to add this behavior to all endpoints like:

<endpoint address=”http://localhost:7047/DynamicsNAV/WS/CRONUS%20International%20Ltd/Page/Customer”
          binding=”basicHttpBinding”
bindingConfiguration=”Customer_Binding”
behaviorConfiguration=”allowDelegation”
contract=”CustomerPageRef.Customer_Port”
name=”Customer_Port” />

If we strip away all the unnecessary defaults and modify a couple of things by hand, the ENTIRE config file could look like this:

<?xml version=”1.0″ encoding=”utf-8″ ?>
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name=”NavWSBinding”>
<security mode=”TransportCredentialOnly”>
<transport clientCredentialType=”Windows”  />
</security>
</binding>
</basicHttpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name=”allowDelegation”>
<clientCredentials>
<windows allowedImpersonationLevel=”Delegation”
allowNtlm=”true”/>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint address=”
http://localhost:7047/DynamicsNAV/WS/CRONUS%20International%20Ltd/Page/Customer”
                binding=”basicHttpBinding”
bindingConfiguration=”NavWSBinding”
behaviorConfiguration=”allowDelegation”
contract=”CustomerPageRef.Customer_Port”
name=”Customer_Port” />
<endpoint address=”
http://localhost:7047/DynamicsNAV/WS/SystemService”
                binding=”basicHttpBinding”
bindingConfiguration=”NavWSBinding”
behaviorConfiguration=”allowDelegation”
contract=”SystemServiceRef.SystemService_Port”
name=”SystemService_Port” />
</client>
</system.serviceModel>
</configuration>

Confused?

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 testAppWCF2.SystemServiceRef;
using testAppWCF2.CustomerPageRef;

namespace testAppWCF2
{
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/”;

// Create the SystemService Client
SystemService_PortClient systemService = new SystemService_PortClient(“SystemService_Port”, baseURL + “SystemService”);

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

Note, that when creating a System Service Client, I specify the name of an endpoint configuration and a URL. I didn’t have to specify anything, then all defaults would be taken from the Config file, but I like to show how you can calculate the URL and specify that at runtime.

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 by specifying the config section and a URL again:

// Create the SystemService Client
Customer_PortClient customerService = new Customer_PortClient(“Customer_Port”, customerPageURL);

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

Customer customer10000 = customerService.Read(“10000”);
Console.WriteLine(“nName of Customer 10000: ” + customer10000.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();

As you can see the code is actually as simple as the Web Reference version, but the config file complicates things a lot. All of the above will output the following to a console prompt (on my machine running NAV 2009SP1 W1)

image

Authentication

BTW – this sample will by default run Windows Authentication. If you want to specify a different user you will need to set the ClientCredential property like this:

customerService.ClientCredentials.Windows.ClientCredential = new NetworkCredential(“user”, “password”, “domain”);

You would need to set this on each Service Client you create.

I hope this is helpful.

Good luck

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

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

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