Connecting to NAV Web Services from Javascript

Prerequisites

Please read this post to get a brief explanation of the scenario I will implement in Javascript.

BTW. Basic knowledge about Javascript and XML is required to understand the following post:-)

Browser compatibility

The sample in this post will work with Internet Explorer. I have only tried this with IE8, but according to documentation it should work from IE5 and up.

I actually also tried to download Mozilla FireFox 3.6 and Opera 10.10 – but I failed to make the script work – not due to incompatibility in Javascript, but I simply couldn’t get any of those browsers to authenticate towards NAV WebServices (whether I was running NTLM or SPNEGO – see this post).

This might be due to the XmlHttpRequest being unable to connect to Web Services X-Domain (explained here) – if you really need to make this work, it seems like a possible solution is to create a proxy (another Web Service in any serviceside language) which is hosted on the same site as the Javascript app and forwards all requests.

IE seems to not care if you have trusted the web site (which is the case here). Script based Web Services access should only be used in your local intranet anyway – you should never try to go across the internet and connect to a NAV server somewhere.

Other posts with Javascript code

On my blog there are a number of posts, where I use Javascript to connect to Web Services. All the Gadgets and the Virtual Earth web site are using Javascript to connect to Web Services and in all of these I am using the MSXML ActiveXObject and all of the other samples are using CodeUnit access. In this sample, I will show how this can be done without using ActiveX and going towards a page.

It’s all about XML

Javascript does not natively have support for strongly typed proxy class generation like C#, VB and Java nor does it have support for interpretation of Soap based webservices like PHP – it is all about XML.

In the end all of the other languages ends up creating a XML document (basically just a string), which is send over the wire to the the Web Service host who then replies back with a XML Document (again just a formatted string).

String manipulation is possible in Javascript and Javascript does have an object called XmlHttpRequest which can communicate with a XML Web Service host – we should be good.

The way NAV 2009 (and SP1) handles Web Services is using SOAP. You can read more about the basics here: http://en.wikipedia.org/wiki/SOAP. This image describes pretty well how your message is put into a body and inserted into an envelope, which the gets send.

image

In the scenario I am implementing here, there are 3 roundtrips to Web Services:

  1. Get the Companies supported on a Web Service Listener
  2. Get a specific Customer
  3. Get all Customers matching a specific filter

Get the Companies supported on a Web Service Listener

In C# we create a reference to the SystemService, which then creates some proxy classes and we just call a method called Companies on these classes.

Underneath that magic, .net will create a XML string that looks much like:

<Soap:Envelope xmlns:Soap=”http://schemas.xmlsoap.org/soap/envelope/“>
<Soap:Body>
<Companies xmlns=”urn:microsoft-dynamics-schemas/nav/system/”>
</Companies>

</Soap:Body>
</Soap:Envelope>

As you can see, the only thing I actually send is <Companies /> (with a namespace).

The return value from the Companies method is again

<Soap:Envelope xmlns:Soap=”http://schemas.xmlsoap.org/soap/envelope/&#8221;>
<Soap:Body>
<Companies_Result xmlns=”urn:microsoft-dynamics-schemas/nav/system/”>
<return_value>CRONUS International Ltd.</return_value>
<return_value>TEST</return_value>
</Companies_Result>
</Soap:Body>
</Soap:Envelope>

In a later post I will show how you can hook into .net and see what XML actually gets sent and what gets received underneath the nice .net surface.

Get a specific Customer

The XML for getting a specific customer looks like:

<Soap:Envelope xmlns:Soap=”http://schemas.xmlsoap.org/soap/envelope/&#8221;>
<Soap:Body>
<Read xmlns=”urn:microsoft-dynamics-schemas/page/customer”>
<No>10000</No>
</Read>
</Soap:Body>
</Soap:Envelope>

and the return XML from the NAV Customer Page could be:

<Soap:Envelope xmlns:Soap=”http://schemas.xmlsoap.org/soap/envelope/&#8221;>
<Soap:Body>
<Read_Result xmlns=”urn:microsoft-dynamics-schemas/page/customer”>
<Customer>
<Key>… some huge key …</Key>
<No>10000</No>
<Name>The Cannon Group PLC</Name>
<Address>192 Market Square</Address>
<Address_2>Address no. 2</Address_2>
… all the other fields …
</Customer>
</Read_Result>
</Soap:Body>
</Soap:Envelope>

I haven’t included all fields – you probably get the picture.

Get all Customers matching a specific filter

The XML for getting all customers matching a specific filter could be:

<Soap:Envelope xmlns:Soap=”http://schemas.xmlsoap.org/soap/envelope/&#8221;>
<Soap:Body>
<ReadMultiple xmlns=”urn:microsoft-dynamics-schemas/page/customer”>
<filter><Field>Country_Region_Code</Field><Criteria>GB</Criteria></filter>
<filter><Field>Location_Code</Field><Criteria>RED|BLUE</Criteria></filter>
</ReadMultiple>
</Soap:Body>
</Soap:Envelope>

and the returned XML something like

<Soap:Envelope xmlns:Soap=”http://schemas.xmlsoap.org/soap/envelope/&#8221;>
<Soap:Body>
<ReadMultiple_Result xmlns=”urn:microsoft-dynamics-schemas/page/customer”>
<ReadMultiple_Result>
<Customer>
… one customer …
</Customer>
<Customer>
… another customer …
</Customer>
<Customer>
… a third customer …
</Customer>
</ReadMultiple_Result>
</ReadMultiple_Result>
</Soap:Body>
</Soap:Envelope>

Enough about the XML – lets see some code

Instead of splitting up the script – I will specify the entire script here and do some explanation beneath.

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html>
<head>
<title></title>
<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″ />

var baseURL = ‘http://localhost:7047/DynamicsNAV/WS/’;
    var cur;
var SystemServiceURL = baseURL + ‘SystemService’;
var CustomerPageURL;

    var SoapEnvelopeNS = ‘http://schemas.xmlsoap.org/soap/envelope/’;
    var SystemServiceNS = ‘urn:microsoft-dynamics-schemas/nav/system/’;
var CustomerPageNS = ‘urn:microsoft-dynamics-schemas/page/customer’;

    // Function to Invoke a NAV WebService and return data from a specific Tag in the responseXML
function InvokeNavWS(URL, method, nameSpace, returnTag, parameters) {
var result = null;
try {
var xmlhttp;
if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp = new XMLHttpRequest();
}
else {// code for IE6, IE5
xmlhttp = new ActiveXObject(“Microsoft.XMLHTTP”);
}

            var request = ” +
” +
” +
parameters +
” +
” +
”;

            // Use Post and non-async
xmlhttp.open(‘POST’, URL, false);
xmlhttp.setRequestHeader(‘Content-type’, ‘text/xml; charset=utf-8’);
xmlhttp.setRequestHeader(‘Content-length’, request.length);
xmlhttp.setRequestHeader(‘SOAPAction’, method);

            // Setup event handler when readystate changes
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4) {
if (xmlhttp.status == 200) {
xmldoc = xmlhttp.responseXML;
xmldoc.setProperty(‘SelectionLanguage’, ‘XPath’);
xmldoc.setProperty(‘SelectionNamespaces’, ‘xmlns:tns=”‘ + nameSpace + ‘”‘);
result = xmldoc.selectNodes(‘//tns:’ + returnTag);
}
}
}

            // Send request will return when event has fired with readyState 4
xmlhttp.send(request);
}
catch (e) {
}
return result;
}

    // Get the Company list
function SystemService_Companies() {
return InvokeNavWS(SystemServiceURL, ‘Companies’, SystemServiceNS, ‘return_value’, ”);
}

    function CustomerPage_Read(no) {
return InvokeNavWS(CustomerPageURL, ‘Read’, CustomerPageNS, ‘Customer’,
” + no + ”);
}

    function CustomerPage_ReadMultiple(filters) {
return InvokeNavWS(CustomerPageURL, ‘ReadMultiple’, CustomerPageNS, ‘Customer’, filters);
}


</head>
<body>

    var companies = SystemService_Companies();
document.writeln(‘Companies:
‘);
for (var i = 0; i         document.writeln(companies[i].text + ‘
‘);
}
cur = companies[0].text;

    CustomerPageURL = baseURL + encodeURIComponent(cur) + ‘/Page/Customer’;
document.writeln(‘
URL of Customer Page: ‘ + CustomerPageURL + ‘
‘);

    var Customer10000 = CustomerPage_Read(‘10000’);
document.writeln(‘
Name of Customer 10000: ‘ +
Customer10000[0].childNodes[2].firstChild.nodeValue + ‘
‘);

    document.writeln(‘
Customers in GB served by RED or BLUE warehouse:
‘);
var Customers = CustomerPage_ReadMultiple(
‘Country_Region_CodeGB’+
                    ‘Location_CodeRED|BLUE’);
for (i = 0; i         document.writeln(Customers[i].childNodes[2].firstChild.nodeValue + ‘
‘);

    document.writeln(‘
THE END’);


</body>
</html>

This is the entire Default.htm file.

Most of the “magic” happens inside InvokeNavWS, which really just builds up the XML document based on the method, the namespace and the parameters for the method. This XML document is then sent to the URL and the XML document we get back is read into an XmlDoc and we use XPath to get the return value (the ReturnTag specifies which tags we are interested in).

On top of this method I have created some high level functions so that we can call CustomerPage_Read(‘10000’) and get a Customer back.

Note that Read and ReadMultiple returns a XML NodeList – and the childNodes under the top level nodes are the fields and under the fields you get the field value by saying firstChild.nodeValue.

In this sample I have hardcoded the Name field to be in childNodes[2], this is probably not the way to get the name – but then again, this is only to show how to get a connection up running.

The output of this on my installation looks like:

image

I hope this is helpful.

Good luck

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Using touchscreens with the RoleTailored Client

I LOVE the RoleTailored Client, I LOVE the fact that everything is metadata driven and i LOVE what this will give us (us being everybody using NAV) going forward. As a result of the investments leading to NAV 2009, NAV has by far the most modern UX and the new framework allows us to innovate faster and more consistent than any other ERP solution out there.

We can change the UX to follow Microsoft Office 2010 if we decide to, without having to do a wash through all pages and modify those to follow the UX. We can create new UI paradigms and allow the existing pages to be reused and we will make sure that the UX is consistent throughout NAV.

I do however also acknowledge that sometimes, love just isn’t enough – for some scenarios, the RoleTailored Client doesn’t make things easier for us and we need to consider what to do.

In this post I will try to explain a way to handle one of these scenarios – creating a page with buttons that can be used from a Touch Screen like:

image

As you might  guess – this requires Visual studio:-)

Button Panels

I have collected a number of screenshots from various applications using touch screens – and it is very common to have one or more panels of buttons and other information from NAV. It is no secret that you could of course just create a button panel like this in Visual Studio using WinForms and you would be on your way, but the problem here is, that you would put the decision on location, size, captions and visuals of the buttons into your Visual Studio solution.

You would have to have a way of describing the looks and the functionality of the button panel from NAV in order to capture your business logic in one place. Thinking more about this – I found myself trying to describe something I had seen before…

A “string” that would describe the visuals, the flow, the positions and the functionality of a panel – that sounds a lot like HTML and Javascript, so if I decided to go with a browser using HTML and Javascript – how in earth would I raise an event on the Service Tier from inside my browser?

Escaping from Javascript

I decided to go forward with the browser idea and try to find out how to escape from Javascript – and it turned out to be pretty simple actually.

On the WebBrowser Control there is a property called ObjectForScripting. By setting that property you are now able to escape to that object from Javascript using window.external.myfunction(myparameters);. In Fact – all the methods in the class you specify in ObjectForScripting are now available from Javascript.

Show me the code!!!

If you haven’t created Microsoft Dynamics Add-Ins before, you might want to read some of the basics on Christian’s blog, especially the following post explains the basics pretty well:

http://blogs.msdn.com/cabeln/archive/2009/05/06/add-ins-for-the-roletailored-client-of-microsoft-dynamicsnav-2009-sp1-part1.aspx

Assuming that you are now a shark in creating Add-Ins – we can continue:-)

Let’s first of all create the native WinForms Control. We can use the WebBrowser unchanged – although the WebBrowser comes with an error, which sometimes surfaces in NAV. If you set the DocumentText in the browser control before it is done rendering the last value of DocumentText – it will ignore the new value. Frankly I want an implementation where the last value wins – NOT the first value. I handle that by subscribing to the DocumentCompleted event and check whether there is a newer value available. I also don’t want to set the value in the WebBrowser if it hasn’t changed.

public class MyWebBrowser : WebBrowser
{
private string text;
private string html = Resources.Empty;

    /// <summary>
/// Constructor for WebBrowser Control
/// </summary>
public MyWebBrowser()
{
this.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(MyWebBrowser_DocumentCompleted);
}

    /// <summary>
/// Handler for DocumentCompleted event
/// If we are trying to set the DocumentText while the WebBrowser is rendering – it is ignored
/// Catching this event to see whether the DocumentText should change fixes that problem
/// </summary>
void MyWebBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
if (this.DocumentText != this.html)
{
this.DocumentText = this.html;
}
}

    /// <summary>
/// Get/Set the Text of the WebBrowser
/// </summary>
public override string Text
{
get
{
return text;
}
set
{
if (text != value)
{
text = value;
if (string.IsNullOrEmpty(value))
{
html = Resources.Empty;
}
else
{
html = text;
}
this.DocumentText = html;
}
}
}
}

and now the Add-In part of the Control.

[ControlAddInExport(“FreddyK.BrowserControl”)]
public class BrowserControl : StringControlAddInBase, IStringControlAddInDefinition
{
MyWebBrowser control;

    protected override Control CreateControl()
{
control = new MyWebBrowser();
control.MinimumSize = new Size(16, 16);

control.MaximumSize = new Size(4096, 4096);
control.IsWebBrowserContextMenuEnabled = false;
control.ObjectForScripting = new MyScriptManager(this);
control.ScrollBarsEnabled = false;
control.ScriptErrorsSuppressed = true;
control.WebBrowserShortcutsEnabled = false;
control.Dock = DockStyle.Fill;
return control;
}

    public void clickevent(int i, string s)
{
this.RaiseControlAddInEvent(i, s);
}

    public override bool AllowCaptionControl
{
get
{
return false;
}
}

    public override bool HasValueChanged
{
get
{
return false;
}
}

    public override string Value
{
get
{
return base.Value;
}
set
{
base.Value = value;
if (this.control != null)
this.control.Text = value;
}
}
}

Things to note:

  • I am using DockStyle.Fill to specify that the Control should take up whatever space is available.
  • ObjectForScripting is set to an instance of the MyScriptManager class
  • the clickevent method raises the Add-In Event on the Service Tier with the parameters coming from the caller.

The MyScriptManager could look like this:

[ComVisible(true)]
public class MyScriptManager
{
BrowserControl browserControl;

    public MyScriptManager(BrowserControl browserControl)
{
this.browserControl = browserControl;
}

    public void clickevent(int i, string s)
{
browserControl.clickevent(i, s);
}
}

and as you might have guessed – this allows Javascript in the WebBrowser to invoke statements like:

window.external.clickevent(i, s);

Note that you need to have ComVisible(true) on the ScriptManager class.

Of course you need to sign the DLL, copy the DLL to the Add-Ins folder and create an entry in the Client Add-Ins table.

You can download the source to the Visual Studio project here – and if you use this, the public key token for this add-in is 58e587b763c2f132 and the Control Add-In Name is FreddyK.BrowserControl.

Let’s put the BrowserControl to work for us

Assuming that we have built the BrowserControl, copied and registered it – we will not build a page with two fields:

image

and of course create two global Variables (HTML as BigText and Value as Decimal).

The pagetype of the page is set to CardPart (in order to avoid the menus – I know this kind of bends the rules of the RoleTailored Client, but since this is a page that wasn’t supposed to be – I think we should manage).

on the Value field – set the DecimalPlaces to 0:10 and on the browser field – set the ControlAddIn property to point to our Browser Control: FreddyK.BrowserControl;PublicKeyToken=58e587b763c2f132.

Now in the OnOpenPage of the page – put the following lines:

OnOpenPage()
CLEAR(HTML);
HTML.ADDTEXT(‘<html><body>Hello World</body><html>’);

this should give us the following page when running:

image

A couple of things to think about when writing the “real” code:

  • We do not want to work directly in our HTML global variable, since any change in this would cause the UI to request an update.
  • If we want to use images in the HTML code, these images needs to be copied to the Client Tier – I do that using DownloadTempFile from the 3-Tier Management codeunit (varibale called TT).

The code to download the 3 images used (normal button, wide button and tall button) could be:

buttonurl := ‘file:///’+CONVERTSTR(TT.DownloadTempFile(APPLICATIONPATH + ‘button.png’),”,’/’);
tallbuttonurl := ‘file:///’+CONVERTSTR(TT.DownloadTempFile(APPLICATIONPATH + ‘tallbutton.png’),”,’/’);
widebuttonurl := ‘file:///’+CONVERTSTR(TT.DownloadTempFile(APPLICATIONPATH + ‘widebutton.png’),”,’/’);

and the code to create the HTML/Javascript code could look like this:

CLEAR(TEMP);
TEMP.ADDTEXT(‘<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” ‘);
TEMP.ADDTEXT(‘”
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>’);
TEMP.ADDTEXT(‘<html xmlns=”
http://www.w3.org/1999/xhtml” >’);
TEMP.ADDTEXT(‘<head>’);

// Create Stylesheet for the visuals
TEMP.ADDTEXT(‘<style type=”text/css”>’);
TEMP.ADDTEXT(‘  td { width:64px; font-size:xx-large; background-image:url(”’+buttonurl+”’) }’);
TEMP.ADDTEXT(‘  tr { height:64px }’);
TEMP.ADDTEXT(‘  a { color:#000000; text-decoration:none }’);
TEMP.ADDTEXT(‘  body { margin:0px; background-color:#FAFAFA }’);
TEMP.ADDTEXT(‘</style>’);

// Create Javascript function for invoking AL Event
TEMP.ADDTEXT(”);
TEMP.ADDTEXT(‘  function click(i, s) {‘);
TEMP.ADDTEXT(‘    window.external.clickevent(i, s);’);
TEMP.ADDTEXT(‘  }’);
TEMP.ADDTEXT(”);

TEMP.ADDTEXT(‘</head>’);
TEMP.ADDTEXT(‘<body>’);

// Create Table with Controls
TEMP.ADDTEXT(‘<table cellpadding=”0″ cellspacing=”5″>’);
TEMP.ADDTEXT(‘<tr>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(7, ””)”>7</a></td>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(8, ””)”>8</a></td>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(9, ””)”>9</a></td>’);
tempstyle := ‘background-image:url(”’+tallbuttonurl+”’)’;
TEMP.ADDTEXT(‘<td style=”‘+tempstyle+'” rowspan=”2″ align=”center”><a href=”javascript:click(-1, ”+”)”>+</a></td>’);
TEMP.ADDTEXT(‘</tr>’);
TEMP.ADDTEXT(‘<tr>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(4, ””)”>4</a></td>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(5, ””)”>5</a></td>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(6, ””)”>6</a></td>’);
TEMP.ADDTEXT(‘</tr>’);
TEMP.ADDTEXT(‘<tr>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(1, ””)”>1</a></td>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(2, ””)”>2</a></td>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(3, ””)”>3</a></td>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(-1, ”-”)”>-</a></td>’);
TEMP.ADDTEXT(‘</tr>’);
TEMP.ADDTEXT(‘<tr>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(-1, ”.”)”>.</a></td>’);
TEMP.ADDTEXT(‘<td align=”center”><a href=”javascript:click(0, ””)”>0</a></td>’);
tempstyle := ‘width:133px; background-image:url(”’+widebuttonurl+”’)’;
TEMP.ADDTEXT(‘<td style=”‘+tempstyle+'” colspan=”2″ align=”center”><a href=”javascript:click(-1, ”=”)”>=</a></td>’);
TEMP.ADDTEXT(‘</tr>’);
TEMP.ADDTEXT(‘</table>’);

TEMP.ADDTEXT(‘</body>’);
TEMP.ADDTEXT(‘</html>’);
HTML := TEMP;

Meaning that every click on any button is routed back to the Add-In Event – and the actual calculator is then implemented in AL Code.

I am not going to go in detail about how to create a calculator, since this is pretty trivial and really not useful – the thing to take away from this sample is how to create button panels in HTML and have every button pressed routed to NAV for handling.

The Calculator .fob file (one page) and the 3 images used in this example can be downloaded here – but again – this is just a “stupid” example. I do think that the technology can come in handy in some cases.

Now, I am aware, that this is not going to solve all issues and you shouldn’t try to twist this to hold all your forms in order to be able to manage colors and font sizes – but it can be used in one-off pages, where you have a page that needs to be used in a warehouse or other locations where you might want huge fonts or touch screen button panels.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Integration to Virtual Earth – Part 4 (out of 4)

(a small change added that simplifies the SmallVEControl class definition)

With the release of NAV 2009 SP1 CTP2 (to MVPs, TAP and BAP) and the official release of the statement of Direction, I can now write about the last part of the integration to Virtual Earth.

People who hasn’t access to NAV 2009 SP1, will unfortunately have to wait until the official release until they can take advantage of this post.

Please not that you should read Part 1, Part 2 and Part 3 of the Integration to Virtual Earth – and you would have to have the changes to the app. described in these posts in order to make this work.

This post will take advantage of a functionality, which comes in NAV 2009 SP1 called Extensibility. Christian explains some basics about extensibility in a post, which you can find here.

The Goal

image

As you can see on the above picture, we have a control, which is able to show the map in NAV of the customer location, and as you select different customers in the list, the map changes.

The changes in the map happens without any user interference, so that the user can walk up and down in the list without being irritated. In the Actions menu in the part, we will put an action called Open In Browser, which will open up a map in a browser as explained in part 3.

Note that the Weather factbox is not shown here.

What is it?

The Control inside the Customer Map Factbox is basically just a browser control, in which we set a html document (pretty much like the one described in part 3) and leave it to the browser control to connect to Virtual Earth and retrieve the map. I do not connect to web services from the browser control, instead we transfer parameters of the current customer location to the control.

Although the internal implementation is a browser control, we don’t do html in NAV and we don’t give the control any URL’s or other fancy stuff. The way we make this work is to have the control databind to a Text variable (CustomerLocation), which gets set in OnAfterGetRecord:

CustomerLocation := ‘latitude=’+FORMAT(Latitude,0,9)+’&longitude=’+FORMAT(Longitude,0,9)+’&zoom=15’;

The factbox isn’t able to return any value and there isn’t any reason right now to trigger any events from the control.

So now we just need to create a control, which shows the string “latitude=50&longitude=2&zoom=15” differently than a dumb text.

How is the control build?

Let’s just go through the creation of the VEControl step by step.

1. Start Visual Studio 2008 SP1, create a new project of type Class Library and call it VEControl.

2. Add a reference System.Windows.Forms , System.Drawing and to the file C:\Program Files\Microsoft Dynamics NAV\60\RoleTailored Client\Microsoft.Dynamics.Framework.UI.Extensibility.dll – you need to browse and find it. Note that when you copy the VEControl.dll to it’s final location you don’t need to copy this DLL, since it will be loaded into memory from the Client before your DLL is called.

 

3. Open Project Properties, go to the Signing tab, and sign your DLL with a new key.

image

4. In the Build Events Tab add the following command to the Post-Build Event window:

copy VEControl.dll “C:\Program Files\Microsoft Dynamics NAV\60\RoleTailored Client\Add-ins”

this ensures that the Control gets installed in the right directory.

5. Delete the automatically generated class1.cs and add another class file called VEControl.cs

6. Add the following class to the file:

/// <summary>
/// Native WinForms Control for Virtual Earth Integration
/// </summary>
public class VEControl : WebBrowser
{
private string template;
private string text;
private string html = “<html><body></body></html>”;

    /// <summary>
/// Constructor for Virtual Earth Integration Control
/// </summary>
/// <param name=”template”>HTML template for Map content</param>
public VEControl(string template)
{
this.template = template;
this.DocumentCompleted += new WebBrowserDocumentCompletedEventHandler(VEControl_DocumentCompleted);
}

    /// <summary>
///
/// </summary>
/// <param name=”sender”></param>
/// <param name=”e”></param>
void VEControl_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
if (this.DocumentText != this.html)
{
this.DocumentText = this.html;
}
}

    /// <summary>
/// Property for Data Binding
/// </summary>
public override string Text
{
get
{
return text;
}
set
{
if (text != value)
{
text = value;
if (string.IsNullOrEmpty(value))
{
html = “<html><body></body></html>”;
}
else
{
html = this.template;
html = html.Replace(“%latitude%”, GetParameter(“latitude”, “0”));
html = html.Replace(“%longitude%”, GetParameter(“longitude”, “0”));
html = html.Replace(“%zoom%”, GetParameter(“zoom”, “1”));
}
this.DocumentText = html;
}
}
}

    /// <summary>
/// Get Parameter from databinding
    /// </summary>
/// <param name=”parm”>Parameter name</param>
/// <param name=”defaultvalue”>Default Value if the parameter isn’t specified</param>
/// <returns>The value of the parameter (or default)</returns>
private string GetParameter(string parm, string defaultvalue)
{
foreach (string parameter in text.Split(‘&’))
{
if (parameter.StartsWith(parm + “=”))
{
return parameter.Substring(parm.Length + 1);
}
}
return defaultvalue;
}
}

Note, that you will need a using statement to System.Windows.Forms.

This class gets initialized with a html template (our javascript code) and is able to get values like “latitude=50&longitude=2&zoom=15” set as the Text property and based on this render the right map through the template.

The reason for the DocumentCompleted event handler is, that if we try to set the DocumentText property in the browser before it is done rendering the prior DocumentText, it will just ignore the new value. We handle this by hooking up to the event and if the DocumentText is different from the value we have – then this must have happened and we just set it again. We are actually pretty happy that the control works this way, because the javascript is run in a different thread than our main thread and fetching the map control from Virtual Earth etc. will not cause any delays for us.

Now this is just a standard WinForms Control – how do we tell the Client that this is a control, that it can use inside the NAV Client?

The way we chose to implement this is by creating a wrapper, which is the one we register with the NAV Client and this wrapper is responsible for creating the “real” control. This allows us to use 3rd party controls even if they are sealed and/or we don’t have the source for them.

7. Add a html page called SmallVEMap.htm and add the following content

<html>
<head>
<title></title>
<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″ />
/fonta%20href=

var map = null;
var shape = null;
function GetMap() {
map = new VEMap(‘myMap’);

        var latitude = parseFloat(“%latitude%”);
var longitude = parseFloat(“%longitude%”);
var zoom = parseInt(“%zoom%”);
map.SetDashboardSize(VEDashboardSize.Tiny);

        var position = new VELatLong(latitude, longitude);
map.LoadMap(position, zoom, ‘r’, false);
shape = new VEShape(VEShapeType.Pushpin, position);
map.AddShape(shape);
}   

</head>
<body onload=”GetMap();” style=”margin:0; position:absolute; width:100%; height:100%; overflow: hidden”>

</body>
</html>

8. Add a Resource file to the project called Resources.resx, open it and drag the SmallVEMap.htm into the resources file.

9. Add a class called SmallVEControl.cs and add the following classes

[ControlAddInExport(“SmallVEControl”)]
public class SmallVEControl : StringControlAddInBase, IStringControlAddInDefinition
{
protected override Control CreateControl()
{
var control = new VEControl(Resources.SmallVEMap);
control.MinimumSize = new Size(200, 200);
control.MaximumSize = new Size(500, 500);
control.ScrollBarsEnabled = false;
control.ScriptErrorsSuppressed = true;
control.WebBrowserShortcutsEnabled = false;
return control;
}

    public override bool AllowCaptionControl
{
get
{
return false;
}
}
}

You need to add using statements to System.Drawing, Microsoft.Dynamics.Framework.UI.Extensibility, Microsoft.Dynamics.Framework.UI.Extensibility.WinForms and System.Windows.Forms.

The CreateControl is the method called by the NAV Client when it needs to create the actual winforms control. We override this method and create the VEControl and give it the html template.

The reason for overriding the AllowCaptionControl is to specify that our control will not need a caption (else the NAV Client will add a caption control in front of our control).

There are various other methods that can be overridden, but we will touch upon these when needed.

Build your solution and you should now have a VEControl.DLL in the Add-Ins directory under the RoleTailored Client.

And how do I put this control into use in the NAV Client?

First of all we need to tell the Client that the control is there!

We do that by adding an entry to the Client Add-In table (2000000069). You need to specify Control Add-In Name (which would be the name specified in the ControlAddInExport attribute above = SmallVEControl) and the public key token.

But what is the public key token?

Its is the public part of the key-file used to sign the assembly and as you remember, we just asked Visual Studio to create a new key-file so we need to query the key file for it’s public key and we do that by running

sn –T VEControl.snk

in a Visual Studio command prompt.

image

Note that this public key is NOT the one you need to use, unless you download my solution below.

image

Having the Control Registered for usage we need to create a new page and call it Customer Map Factbox. This page has SourceTable set to the Customer table and is contains one control, bound to a variable called CustomerLocation, which gets set in the OnAfterGetRecord.

image

The code in OnAfterGetRecord is

CustomerLocation := ‘latitude=’+FORMAT(Latitude,0,9)+’&longitude=’+FORMAT(Longitude,0,9)+’&zoom=15’;

The Customer Map Factbox is added as a part to the Customer Card and the Customer List and the SubFormLink is set to No.=FIELD(No.)

That’s it guys – I realize this is a little rough start on extensibility – I promise that there will be other and more entry level starter examples on extensibility – I just decided to create an end-to-end sample to show how to leverage the Virtual Earth functionality in a Factbox.

As usual you can download the visual studio project here.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Integration to Virtual Earth – Part 2 (out of 4)

If you haven’t read Part 1, you should do so before continuing here. In Part 1 we saw how to add geographical information to your customer table and how to connect to the Virtual Earth geocode web service. Now we want to put this information to play.

First of all, we all know Virtual Earth (if not – try it at http://maps.live.com). Less commonly known is it, that the map control used in maps.live.com actually is available for public usage in other web pages.

The Virtual Earth Interactive SDK

If you navigate to http://dev.live.com/virtualearth/sdk/ you will get a menu in the left side looking like this:

image

and a map looking like this:

image

(depending on where in the world you are)

Try looking at the different tabs and you can see the source code for getting a map like this – and you can select other menu items.

Try clicking at the menu item for “Show a specific map”. The map now shows the space needle in Seattle and if you click the source code tab you will see the code for creating that map:

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”&gt;
<html>
<head>
<title></title>
<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″>
http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2

var map = null;
function GetMap()
{
map = new VEMap(‘myMap’);
map.LoadMap(new VELatLong(47.6, -122.33), 10 ,’h’ ,false);
}  

</head>
<body onload=”GetMap();”>

</body>
</html>

So – HTML and Javascript again. A few things which are important to know is that this line:

http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2

Imports the mapcontrol library from the Virtual Earth Web Site and allows you to use all the controls and functions exposed by this library (like a C# reference and using statement).


var map = null;
function GetMap()
{
map = new VEMap(‘myMap’);
map.LoadMap(new VELatLong(47.6, -122.33), 10 ,’h’ ,false);
}  

Is the actual code – for manipulating the map. 47.6, –122.33 should be latitude and longitude for the Space Needle, 10 is the Zoom level, ‘h’ is the style (h==hybrid) if you click the reference tab on the dev sdk window you can find the documentation to LoadMap and other stuff.

Turns out that the Space Needle is not exactly in that position – the right position would be 47.620564, -122.349577 – anyway you get the picture,

The instantiation of the Map also tells you where on the web site, the map should be placed (‘myMap’) which references a

area in the body:

 

 

This

area specifies the location, flow definition and the size of the control.

The last thing we need to know is how our function (GetMap) in Javascript is executed – and that happens as an event handler to the onload event on the body tag.

 

Note that Javascript is executed on the Client and there is no serverside code in this example.

Knowing that we can communicate with NAV Web Services from Javascript (as explained in this post) we should be able to create a site, which loads a map and displays pushpins for all our customers – more about this in a second…

Deployment

Knowing that there are a million different ways to deploy web sites I will not go into detail about how this should be done – but only explain how it can be done (aka what I did on my laptop).

What I did was to install IIS (Internet Information Server) on my laptop. After this I fired up Visual Studio, selected File –> New –> Web Site and specified that I wanted to create an Empty Web Site called http://localhost/mysite

In the empty web site I right clicked on the project and selected add new item, selected HTML page and called it default.htm:

image

C# / Visual Basic choice really didn’t matter as I wasn’t going to make any server side code.

In this default.htm I replaced the content with the following HTML

http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&#8221;>

/fonta%20href=

var map = null;
function GetMap()
{
map = new VEMap(‘myMap’);
map.LoadMap(new VELatLong(47.620564, -122.349577), 16 ,’h’ ,false);
}

</body>
</html>

and when I open the site in my browser i get:

image

Voila, the Space Needle.

So far so good and if you succeeded in showing this map in a browser from your own Web Site – we know that all the basics are in place.

What’s needed in NAV

Nothing comes for free – and just because we added the latitude and longitude to the customer table doesn’t mean that we can query on them.

We need a function in NAV, exposed as a Web Service, which we can call and ask for all customers in a rectangle of the Earth. Now sceptics might say that there are no such thing as a rectangle of the earth – but programming towards the Virtual Earth API – there is. For returning the customers through Web Services I have created a XML port:

image

and the two lines of Code behind this

Customer – Export::OnAfterGetRecord()
ref.GETTABLE(“<Customer>”);
_Bookmark := FORMAT(ref.RECORDID,0,10);

for making sure that the bookmark is correctly formatted.

The function that we want to expose as a webservice looks like this:

GetCustomersWithin(latitude1 : Decimal;latitude2 : Decimal;longitude1 : Decimal;longitude2 : Decimal;VAR result : XMLport CustomerLocation)
customers.SETRANGE(Latitude, latitude1, latitude2);
customers.SETRANGE(Longitude, longitude1, longitude2);
result.SETTABLEVIEW(customers);

So this is the reason why it is good to remember to have a key on the latitude and longitude fields in the customer table.

I just added this function to the NavMaps codeunit and made sure that the other function (from post 1) is private – meaning that this function is the only function exposed and you need to expose the NavMaps codeunit as a web service in the Web Service table

image

I called it Maps – and this is used in the code below.

Combining things

In the menu you can find code for how to add shapes (pushpins and other things). You can also find code for how to subscribe to events and we are going to do this for two of the events from the mapcontrol (onendzoom and onendpan) as they change the current viewing rectangle of the control.

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html>
<head>
<title></title>
<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″ />
/fonta%20href=

var map = null;
var defaultURL = “http://localhost:7047/DynamicsNAV/WS/CRONUS_International_Ltd/&#8221;;
    var XMLPortResultNS = ‘urn:microsoft-dynamics-nav/xmlports/customerlocation’;
var XMLPortResultNode = ‘Customer’;
var resultSet;

    // Event handler for endzoom event
function EndZoomHandler(e) {
UpdatePushPins();
}

    // Event handler for endpan event
function EndPanHandler(e) {
UpdatePushPins();
}

    // Initialize the map
function GetMap() {
map = new VEMap(‘myMap’);

        // center and zoom are passable as parameters
var latitude = parseFloat(queryString(“latitude”, “0”));
var longitude = parseFloat(queryString(“longitude”, “0”));
var zoom = parseInt(queryString(“zoom”, “2”));

        // use normal dashboard
map.SetDashboardSize(VEDashboardSize.Normal);
// load the map
var position = new VELatLong(latitude, longitude);
map.LoadMap(position, zoom, ‘r’, false);
// hook events
map.AttachEvent(“onendzoom”, EndZoomHandler);
map.AttachEvent(“onendpan”, EndPanHandler);
// Place pushpins
UpdatePushPins();
}

    // Update all pushpins on the map
function UpdatePushPins() {
map.DeleteAllShapes();
// Get the view rectangle
var botlft = map.PixelToLatLong(new VEPixel(1, myMap.offsetHeight-1));
var toprgt = map.PixelToLatLong(new VEPixel(myMap.offsetWidth-1, 1));

        // Get customers within rectangle
GetCustomersWithin(botlft.Latitude, toprgt.Latitude, botlft.Longitude, toprgt.Longitude);
if (resultSet != null) {
i = 0;
while (i                 var shape = new VEShape(VEShapeType.Pushpin, new VELatLong(resultSet[i].childNodes[2].text, resultSet[i].childNodes[3].text));
shape.SetTitle(resultSet[i].childNodes[0].text + ” ” + resultSet[i].childNodes[1].text);
shape.SetDescription(”

” +

” +

” +

” +

Contact ” + resultSet[i].childNodes[5].text + “
Phone ” + resultSet[i].childNodes[6].text + “ Sales ” + resultSet[i].childNodes[7].text + “ Profit ” + resultSet[i].childNodes[8].text + “ Open Customer Page

“);
map.AddShape(shape);
i++;
}
}
}
// Helper function for querying parameters to the site
function queryString(parameter, defaultvalue) {
var loc = location.search.substring(1, location.search.length);
var param_value = false;
var params = loc.split(“&”);
for (i = 0; i             param_name = params[i].substring(0, params[i].indexOf(‘=’));
if (param_name == parameter) {
param_value = params[i].substring(params[i].indexOf(‘=’) + 1)
}
}
if (param_value) {
return param_value;
}
else {
return defaultvalue;
}
}

    // Get Base URL
function GetBaseURL() {
return defaultURL;
}

    // Get Customers within specified rectangle by connecting to NAV WebService
function GetCustomersWithin(latitude1, latitude2, longitude1, longitude2) {
resultSet = null;
try {
// Instantiate XMLHTTP object
xmlhttp = new ActiveXObject(“Msxml2.XMLHTTP.6.0”);
xmlhttp.open(“POST”, GetBaseURL() + “Codeunit/Maps”, false, null, null);
xmlhttp.setRequestHeader(“Content-Type”, “text/xml; charset=utf-8”);
xmlhttp.setRequestHeader(“SOAPAction”, “GetCustomersWithin”);

            // Setup event handler when readystate changes
xmlhttp.onreadystatechange = function() {
// Inline function for handling response
if ((xmlhttp.readyState == 4) && (xmlhttp.Status == 200)) {
var xmldoc = xmlhttp.ResponseXML;
xmldoc.setProperty(‘SelectionLanguage’, ‘XPath’);
xmldoc.setProperty(‘SelectionNamespaces’, ‘xmlns:tns=”‘ + XMLPortResultNS + ‘”‘);
resultSet = xmldoc.selectNodes(‘//tns:’ + XMLPortResultNode);
}
}
// Send request
xmlhttp.Send(‘http://schemas.xmlsoap.org/soap/envelope/&#8221;
>’ + latitude1 + ” + latitude2 + ” + longitude1 + ” + longitude2 + ”);
}
catch (e) {
alert(e.message);
}
}


</head>
<body onload=”GetMap();” style=”margin:0; width:100%; height:100%; overflow: hidden”>

</body>
</html>

I will let the code speak for itself and direct attention to some of my other Javascript posts and/or information generally available on Javascript on the Internet.

image

The defaultURL variable in the start should be set to the URL of your web services listener and if you want to have more fields when you hover over a customer on the map, you will need to add these fields to the customerLocation XML port and add them to the SetDescription call in the code.

I have only tried this with the demo data – and of course you could imagine that if you have a very high number of customers the pushpin placement routine would be too slow. If this is the case, we can either limit the rectangle (if it spans too much – then ignore) or we can check the size of the returned and only place pushings for the firs xx number.

Epilog

When I said nothing comes for free, it wasn’t entirely true – this blog post is free, the usage of the information in this post is free include code snippets etc. – so some things are free:-)

I realize that this was a LOT of non-NAV code – but I do think it opens up some very compelling scenarios for integration when we combine the strength of different products using Web Services.

As always the NAV objects and the default.htm from above is available for download here.

In step 3 I will show how we can add an action to the Customer Card for showing the area map for a given customer – and you probably already know how to do this, if you have followed this post carefully.

 

Enjoy

 

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Microsoft Windows Vista Gadget – My “Stuff”

This is my second gadget. My first gadget was the Search gadget, which you can find here.

I won’t repeat any explanation as to what a gadget is, nor will I talk about Javascript – instead I will focus upon the things, which are new in this post:

  • Returning an XMLPort from Web Services to Javascript
  • Running a Web Service method asynchronous
  • Use the same gadget for invoking three different methods
  • Poor mans error handling in Javascript

What I want to do is to create a gadget that can show My Customers, My Vendors or My Items.

image

image

image

The flyout from the gadget should be additional information about the record in the gadget, like

image

Clicking on the customer number should open the Customer Task Page and it should be easy to add additional information to this window (the things that are interesting to quickly get access to for our users).

In the Settings dialog you should be able to control things like

image

I will not discuss My Activities in this post, although it would be cool to have a Vista Gadget with My Activities (must do that some other day).

Ready? – well here we go…

The GetMyStuff Codeunit

In NAV we need to expose something, through which we can get access to My Customers, My Vendors and My Items – and the easiest thing would of course be to just expose the 3 pages: 9150 (My Customers), 9151 (My Vendors) and 9152 (My Items).

The problem with this approach is, that we need to get a bookmark back (for opening Task Pages) from the Web Service – meaning that we either create another field in these pages – or we create another Web Service Method for returning the bookmark. I really don’t want to modify base objects if I can avoid it and adding another function just multiplies the number of times we invoke Web Service method by a factor, which I am not interested in doing either.

So I am going to go with a Codeunit, 3 XML ports – and the Codeunit basically just have these three functions.

GetMyCustomers(VAR result : XMLport “My Customers”)
mycustomer.SETRANGE(“User ID”,USERID);
result.SETTABLEVIEW(mycustomer);

GetMyItems(VAR result : XMLport “My Items”)
myitem.SETRANGE(“User ID”,USERID);
result.SETTABLEVIEW(myitem);

GetMyVendors(VAR result : XMLport “My Vendors”)
myvendor.SETRANGE(“User ID”,USERID);
result.SETTABLEVIEW(myvendor);

Now returning a XMLPort (which is a NAV construct) from a Web Service sounds weird – and of course we do NOT return the XML Port object to the Web Service Consumer. What we get is the output of the XMLPort – and if you send data into the XMLPort, it is going to run the XMLPort for input.

The XMLPort for My Customers looks like:

image

and if we take a look at the WSDL for a Codeunit which returns this XMLPort, the schema for the return type very much looks like the above definition

image

and the schema for the method references this as both input and output

image

In the XMLPort properties, I have set the following properties

image

and in the Table element properties MinOccurs is set to Zero. If MinOccurs is 1 (the default), the XMLPort will return an empty My Customer if the list is empty – we do not want that.

Codebehind on the XMLPort sets the three variables _Name, _Bookmark and _Phone.

MyCustomer – Export::OnAfterGetRecord()
rec.GET(“My Customer”.”Customer No.”);
ref.GETTABLE(rec);
_Name := rec.Name;
_Phone := rec.”Phone No.”;
_Bookmark := FORMAT(ref.RECORDID,0,10);

Where rec is type Record of Customer and ref is a RecordRef.

The My Vendors and My Items XMLPorts are created following the same pattern and my gadget code will be made in a way, so that additional fields in the XMLPort will be displayed in the flyout – so you can add whatever you want to this XMLPort.

The Gadget

Again – using Javascript and instead of listing the entire Gadget Sourcecode I will describe the flow.

The main HTML body contains two areas, in which I insert the content via DHTML. These areas are called CAPTION and CONTENT.

CAPTION is for holding the top part of the gadget, where is says My Customer, My Vendor etc. and CONTENT is for holding the list of records. The reason for the caption to be dynamic is that it changes based on the selected type of course.

The first code executed in the script (except for the variable declarations) is the onreadystatechanged event handler – and note that the construct

// Microsoft suggests using onreadystatechange instead of onLoad
document.onreadystatechange = function()
{
if(document.readyState==”complete”)
{
// Initialize Settings
System.Gadget.settingsUI = “settings.html”;
        System.Gadget.onSettingsClosed = settingsClosed;

        // Initialize flyout
System.Gadget.Flyout.file = “flyout.html”;
System.Gadget.Flyout.onShow = flyoutShowing;  

        // Set the Caption of the Gadget
setCaption();

        // Initialize timer – to refresh every x seconds
setTimeout( “refresh()”, 1000 );
}
}

creates the delegate and assigns it to the event handler in one go.

In the event handler we setup the Settings dialog to use settings.html and the flyout to use flyout.html. setCaption creates a HTML string for the Caption and sets that into the CAPTION DHTML area and lastly, we setup the refresh method to be called in 1 second. This is done in order for the Gadget to be added smoothly and without any delays from connecting to Web Services etc.

The refresh function starts by adding another call to refresh delayed (depending on the RefreshInterval) and call setContent. setConent is the function, which does the actual Web Service method invoke.

// Refresh Gadget
function refresh()
{
// Initialize timer – to refresh every
setTimeout( “refresh()”, GetRefreshInterval()*1000 );

    // Set the Content of the Gadget
setContent();
}

The three functions: GetRefreshInterval(), GetType() and GetBaseURL() are only for getting variables from the settings dialog. All functions will default the settings to the default value set in the top of the Javascript section, if they are not already defined. The reason for writing the values to the settings file here is that the settings.html becomes much simpler this way.

The settingsClosed event handler is setup in the onreadystatechanged above and is called when the Settings dialog is opened (clicking on the wrench next to the gadget). This event handler will update the Caption and call refresh (in order to force a refresh now – and setup a new timeout).

// Refresh Gadget on Settings Closed
function settingsClosed(event)
{
// User hits OK on the settings page.
if (event.closeAction == event.Action.commit)
{
// Caption might have changed based on settings
setCaption();

        // Refresh content, update refresh Interval
setTimeout(“refresh()”, 1000);
}
}

The setContent function starts out by setting up some global variables based on the type shown in the gadget

// Setup variables and invoke Web Service method for getting My records
function setContent()
{
RTCpage = ”;
Type = GetType();
if (Type == ‘My Vendors’)
{
// Settings for My Vendors

        RTCpage = ’26’;
WSfunction = ‘GetMyVendors’;
XMLPortResultNS = ‘urn:microsoft-dynamics-nav/xmlports/myvendors’;
XMLPortResultNode = ‘MyVendor’;
}
else if (Type == ‘My Items’)
{
// Settings for My Items

        RTCpage = ’30’;
WSfunction = ‘GetMyItems’;
XMLPortResultNS = ‘urn:microsoft-dynamics-nav/xmlports/myitems’;
XMLPortResultNode = ‘MyItem’;
}
else if (Type == ‘My Customers’)
{
// Settings for My Customers
RTCpage = ’21’;
WSfunction = ‘GetMyCustomers’;
XMLPortResultNS = ‘urn:microsoft-dynamics-nav/xmlports/mycustomers’;
XMLPortResultNode = ‘MyCustomer’;
}
else
{
RTCpage = ”;
}
// Invoke GetMyStuff Web Service
try
{
xmlhttp = new ActiveXObject(“Msxml2.XMLHTTP.4.0”);
xmlhttp.open(“POST”, GetBaseURL()+”Codeunit/GetMyStuff”, false, null, null);
xmlhttp.setRequestHeader(“Content-Type”, “text/xml; charset=utf-8”);
xmlhttp.setRequestHeader(“SOAPAction”, “GetMyStuff”);

        // Setup event handler when readystate changes
xmlhttp.onreadystatechange = function()
{
if ((xmlhttp.readyState == 4) && (xmlhttp.Status == 200))
{
xmldoc = xmlhttp.ResponseXML;
xmldoc.setProperty(‘SelectionLanguage’, ‘XPath’);
xmldoc.setProperty(‘SelectionNamespaces’, ‘xmlns:tns=”‘+XMLPortResultNS+'”‘);
myXML = xmldoc.selectNodes(‘//tns:’+XMLPortResultNode);
updateGadget(true);
}
else
{
updateGadget(false);
}
}
xmlhttp.Send(‘<?xml version=”1.0″ encoding=”utf-8″?><soap:Envelope xmlns:soap=”
http://schemas.xmlsoap.org/soap/envelope/&#8221;><soap:Body><‘+WSfunction+’ xmlns=”urn:microsoft-dynamics-schemas/codeunit/GetMyStuff”><result></result></’+WSfunction+’></soap:Body></soap:Envelope>’);
}
catch(e)
{
// Something went wrong – display: “Service not available”, indicating that there of course are no bugs in the above code:-)
updateGadget(false);
}
}

After setting up the variables, we initialize the xmlhttp and setup a delegate function for onreadystatechange on the xmlhttp (this gets invoked when the Web Service method is done). After this we invoke Send with a SOAP document conforming to the WSDL for the Web Service.

When the onreadystatechange event handler is executed we read the XML and update the content of the Gadget. If anything goes wrong we call the updateGadget function with false – indicating that it should change the content to an error message.

The UpdateGadget builds a HTML table and inserts this table into the CONTENT area of the gadget. For every row we add a call to showflyout if the user clicks the row in order to get additional information.

// Add a row to newHTML
newHTML += ‘<tr><td height=”18″ valign=”middle” background=”Images/gadgetmiddle.png”>’;
newHTML += ‘

‘;
newHTML += ‘ ‘+myXML[o].childNodes[1].text+’
‘;
newHTML += ‘

</td></tr>’;
o++;

The showFlyout function is pretty simple

// Show flyout with additional information
function showFlyout(no)
{
System.Gadget.Flyout.show = false;
flyoutNo = no;
System.Gadget.Flyout.show = true;
}

and after this method has been called, the next thing happening is that the flyoutShowing event handler is invoked – and in this event handler we can calculate the content of the flyout and set it in the flyout.

// Flyout Showing event handler
// Calculate content of flyout
function flyoutShowing()
{
flyoutHTML = ‘<table width=”100%” border=”0″ hspace=”0″ vspace=”0″ cellpadding=”0″ cellspacing=”0″>’;
flyoutHTML += ‘<tr><td height=”31″ align=”left” valign=”middle” background=”Images/topband.png” nowrap><p><strong><font color=”#FFFFFF” size=”3″ face=”Segoe UI”>&nbsp;’+myXML[flyoutNo].childNodes[1].text+'</font></strong></p></td></tr>’;
flyoutHTML += ‘<tr><td valign=”top”><table cellspacing=”5″>’;
for(i=3; i<myXML[flyoutNo].childNodes.length; i++)
{
flyoutHTML += ‘<tr><td>’+myXML[flyoutNo].childNodes[i].nodeName+'</td><td>’;
if (i==3)
{
flyoutHTML += ‘<a href=”dynamicsnav:////runpage?page=’+RTCpage+’&bookmark=’+myXML[flyoutNo].childNodes[2].text+’&mode=view”>’+myXML[flyoutNo].childNodes[i].text+'</a>’;
}
else
{
flyoutHTML += myXML[flyoutNo].childNodes[i].text;
}
flyoutHTML += ‘</td></tr>’;
}
flyoutHTML += ‘</table></td></tr></table>’;

    obj = System.Gadget.Flyout.document.getElementById(“CONTENT”);
obj.innerHTML = flyoutHTML;
}

Really mostly string manipulation in order to create a string that looks correct. I could probably have done the same with XSLT – but this seems pretty easy. As you can see the function enumerates the content of the childNodes to myXML – and adds everything to the flyout. This means that if you add some fields to the XMLPort, then these will be included in the flyout as well.

The flyout.html only contains an empty HTML document with a CONTENT area, which is set by the code above.

Settings.html

The settings.html is really simple, with a method for reading the settings and setting them onto the form

// Initialize settings Form
document.onreadystatechange = function()
{
if(document.readyState==”complete”)
{
// Read settings and set in form
URL.value = System.Gadget.Settings.read(“URL”);
RefreshInterval.value = System.Gadget.Settings.read(“RefreshInterval”);
Type.value = System.Gadget.Settings.read(“Type”);
}
}

A method which for setting the settings back into the settings file.

// Event handler for onSettingsClosing
System.Gadget.onSettingsClosing = function(event)
{
if (event.closeAction == event.Action.commit)
{
// Write new URL into settings
System.Gadget.Settings.writeString(“URL”, URL.value);
System.Gadget.Settings.writeString(“RefreshInterval”, RefreshInterval.value);
System.Gadget.Settings.writeString(“Type”, Type.value);

        // State that it is OK to close the settings form
event.cancel = false;
}
}

and the form itself in HTML

<table width=”100%” height=”100%”>
<tr><td>
Web Service Base URL:<br>
<input type=”textbox” id=”URL” maxlength=”250″>
</td></tr>
<tr><td>
My type:<br>
<select name=”Type” size=”1″>
<option value=”My Customers”>My Customers</option>
<option value=”My Vendors”>My Vendors</option>
<option value=”My Items”>My Items</option>
</select>
</td></tr>
<tr><td>
Refresh Interval:<br>
<select name=”RefreshInterval” size=”1″>
<option value=”10″>5 seconds</option>
<option value=”30″>30 seconds</option>
<option value=”60″>1 minute</option>
<option value=”300″>5 minutes</option>
</select>
</td></tr>
</table>

I will let the code speak for itself.

If you want to see the entire thing, feel free to download it from http://www.freddy.dk/GetMyStuff.zip. the Zip file both contains the Gadget (open that and install – or rename to .zip) and a .fob file with the NAV 2009 objects. You will need to expose the GetMyStuff Codeunit as a webservice with the same name.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Search in NAV 2009 – Part 3 (out of 3)

If you haven’t read part 2 and part 1 of the Search in NAV 2009 posts, you should do so before continuing.

This is the 3rd and final part of the Search in NAV 2009 post. In this section I will show how to create a Windows Vista Gadget and have this gadget connect to NAV through Web Services and search in NAV (like the System Tray version in part 2).

We will create an installable Gadget like:

image

and when installed the user should be able to perform searches like:

image

Giving the user the opportunity to click on items and link into NAV 2009.

As you will notice the results window is different from the results window in part 2 and the main reason for this is, that I couldn’t get intra document links (that be <A HREF=”#Customers”> and <A NAME=”Customers”>) to work in a flyout. Every time you would click a link which should reposition yourself in the document – it would reload the page and leave me with a blank page.

After having struggled with this for some hours I decided that that piece was primarily done in order to help keyboard users – and since Gadgets kind of require the mouse I decided to remove the intra document links.

If anybody finds a way to do this, feel free to add a comment and make me smarter! 🙂

I actually think the sample shows that the strategy of having the Web Service just return a result set and have the consumer format this in the way that fits the consumer is the right decision.

What is a .Gadget file?

If you ever downloaded a file called Something.Gadget and opened it, you might get a warning like this

image

and if you say Install, then the Gadget gets installed – very easy indeed, but what is this?

As a hint – try to rename the file to Something.Gadget.zip – and you will see.

The file extension Gadget is known by Windows Sidebar, which will look for a Gadget.xml in the .zip file and if a correct Gadget.xml is present, it will display this installation dialog and if you select to Install, the .zip file is unpacked into a directory under

C:\Users\<username>\AppData\Local\Microsoft\Windows Sidebar\Gadgets

and add the gadget to the sidebar.

Note that the .zip file should NOT contain the outer directory – only the content.

And what is the Gadget then?

The Gadget is actually just a small html document, which can contain Javascript, VB Script code or other client side code, supported in html. The first file read by the Sidebar is the Gadget.xml file, which in my Search example looks like:

<?xml version=”1.0″ encoding=”utf-8″ ?>
<gadget>
<name>NAV Search Gadget</name>
<namespace>NAVsearch</namespace>
<version>1.0</version>
<author name=”Freddy Kristiansen”>
<info url=”
http://blogs.msdn.com/freddyk” />
</author>
<copyright>None, feel free to use!</copyright>
<description>Freddys NAV Search Gadget</description>
<icons>
<icon height=”48″ width=”48″ src=”Images/Navision.ico” />
</icons>
<hosts>
<host name=”sidebar”>
<base type=”HTML” apiVersion=”1.0.0″ src=”gadget.html” mce_src=”gadget.html” />
<permissions>full</permissions>
<platform minPlatformVersion=”0.3″ />
</host>
</hosts>
</gadget>

So, this is where you define Name, Namespace, Version, Author, etc.  But also Icon to display in the Add Gadget and the html document to display in the sidebar (in this case gadget.html).

In my sample I will be using Javascript – not because I by any means is an expert in Javascript – but I did some Javascript coding back around year 2000 – so I guess it is time to refresh my memory. I use notepad as my editor – and the biggest problem I have run into is, that whenever I make a mistake (like misspell something btw. Javascript is case sensitive) execution of Javascript will just stop without giving any form of error. I guess there are better way to write Javascript than this – I just haven’t found it.

Gadget.html

Note, that this is not an HTML tutorial, I expect you to know the basic constructs of HTML and Javascript – you should be able to find a LOT of content around these things on the Internet.

The body section of my gadget looks like this:

<body bgcolor=”0″ leftmargin=”0″ topmargin=”0″ >
<g:background opacity=”100″></g:background>
<table width=”100%” height=”100%” border=”0″ hspace=”0″ vspace=”0″ cellpadding=”0″ cellspacing=”0″>
<tr>
<td height=”36″ align=”left” valign=”top” background=”Images/gadgettop.png” nowrap><p style=”margin-top: 10px”><strong><font color=”#FFFFFF” size=”3″ face=”Segoe UI”>&nbsp;NAV Search</font></strong></p></td>
</tr>
<tr>
<td height=”22″ valign=”middle” background=”Images/gadgetmiddle.png”>
<input type=”textbox” id=”SearchText” onFocus=”hideFlyout();”><input type=”image” src=”Images/search.png” id=”doSearch” onClick=”search();”>
</td>
</tr>
<tr>
<td height=”28″ border=”0″ background=”Images/gadgetbottom.png”>

Microsoft Dynamics NAV

</td>
</tr>
</table>
</body>

As you can see, most of this is HTML in order to make the Gadget look right. The only two Javascript methods that are called is

  • hideFlyout() – when the textbox receives focus.
  • search() – when you click the search icon (or press enter in the text box)

and of course our Gadget has references to some images from an Images folder.

The main search function looks like this

// Main search function
// search after the content in the textbox
function search()
{
// If flyout is shown, hide it
if (System.Gadget.Flyout.show)
{
hideFlyout();
}

    // Get search string
str = document.getElementById(“SearchText”).value;
if (str != “”)
{
// Perform search
result = doSearch(str);
if (result != “”)
{
// Store HTML to use when flyout pops out
newHTML = result;
// Display result in flyout
System.Gadget.Flyout.show = true;
}
}
}

System.Gadget.Flyout is part of the Gadget Framework and gives you access to set a document used for flyouts, show the flyout and hide it again.

The flyout is (as you can imagine) also just a HTML document – even though it doesn’t behave totally like a normal browser showing a HTML document – more about that later.

As you can see, the function, which will be doing the Web Service connection and the “real” search is doSearch:

// the “real” search function
function doSearch(searchstring)
{
// Get the URL for the NAV 2009 Search Codeunit
var URL = GetBaseURL() + “Codeunit/Search”;

// Create XMLHTTP and send SOAP document
xmlhttp = new ActiveXObject(“Msxml2.XMLHTTP.4.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=”
http://schemas.xmlsoap.org/soap/envelope/”><soap:Body><DoSearch xmlns=”urn:microsoft-dynamics-schemas/codeunit/Search”><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=”
http://schemas.xmlsoap.org/soap/envelope/” xmlns:tns=”urn:microsoft-dynamics-schemas/codeunit/Search”‘);
result = xmldoc.selectSingleNode(“/soap:Envelope/soap:Body/tns:DoSearch_Result/tns:result”).text;

// Load result into XML Document
xmldoc = new ActiveXObject(“Msxml2.DOMDocument.4.0”);
xmldoc.loadXML(result);

    // Load XSL document
xsldoc = new ActiveXObject(“Msxml2.DOMDocument.4.0”);
xsldoc.load(“SearchResultToHTML.xslt”); 

    // Transform
return xmldoc.transformNode(xsldoc);
}

Wow – a lot of code.

This is actually the only code in the Gadget connecting to Web Services – all the other code is housekeeping and has as such nothing to do with NAV 2009. Basically we just get the URL for the Web Service and use XMLHTTP to connect to the Web Service and get a SOAP response back. We use XPath to find the XML result from our codeunit. Load this into a XML Document. Load the XSLT into another XML Document and transform the XML using the XSLT – somehow similar to the way we did it in C# in part 2.

I will post other examples of Gadgets communicating with NAV Web Services, stay tuned.

The basic initialization of the gadget is done in

// Microsoft suggests using onreadystatechange instead of onLoad
document.onreadystatechange = function()
{
if(document.readyState==”complete”)
{
// Initialize Settings and Flyout
System.Gadget.settingsUI = “settings.html”;
System.Gadget.Flyout.file = “flyout.html”; 

        // Add eventhandler for Flyout onShow
System.Gadget.Flyout.onShow = flyoutShowing;  

// Write default Base URL in settings if not already done
GetBaseURL();
}
}

When setting the value of settingsUI on System.Gadget the gadget will get a small image icon when you hover over the Gadget and an Options menu item in the Context menu. Both these options will open the HTML defined in settingsUI.

There is no code in the Flyout – the only special thing is with the flyout is that it contains an IFRAME element, which loads the content.html document in order to get a scrollbar if the content of the flyout becomes too big.

The GetBaseURL function is used under startup – and when we need to connect.

// Get the Base Web Services URL
function GetBaseURL()
{
// Read the URL from settings
var URL = System.Gadget.Settings.readString(“URL”);
if (URL == “”)
{
// No settings in the settings.ini – write the default URL
URL = defaultURL;
System.Gadget.Settings.writeString(“URL”, URL);
}
// Always terminate with /
if (URL.substr(URL.length-1,1) != “/”)
{
URL = URL + “/”;
}
return URL;
}

The reason for calling the function at startup is, that we set the settings to the default URL if it isn’t already defined. It is better that the settings dialog comes up with “some” default than just a blank URL – IMO.

The settings.html contains two functions for doing the housekeeping of the settings:

// Initialize settings Form
document.onreadystatechange = function()
{
if(document.readyState==”complete”)
{
// Read settings and set in form
URL.value = System.Gadget.Settings.read(“URL”);
}
}

// Event handler for onSettingsClosing
System.Gadget.onSettingsClosing = function(event)
{
if (event.closeAction == event.Action.commit)
{
// Write new URL into settings
System.Gadget.Settings.writeString(“URL”, URL.value);

        // State that it is OK to close the settings form
event.cancel = false;
}
}

I will let the code speak for itself.

The System.Gadget.Settings read and write functions stores the settings in

C:\Users\<username>\AppData\Local\Microsoft\Windows Sidebar\settings.ini

and the settings will be stored in clear text, you will actually be able to modify this file as well.

That’s it for the NAV Search demo – I hope you like it, you can download the Gadget from http://www.freddy.dk/Search – Part 3.zip. Note that this download cannot stand alone – you need the NAV piece of this, which you can find in Part 1.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Search in NAV 2009 – Part 1 (out of 3)

During the partner keynote and during a couple of the other presentations, we showed a small application, which was able to search in NAV 2009 through multiple tables, display a result set and allow people to drill into task pages in NAV 2009 from the search result window.

During the next 3 posts, I will explain how this demo is done and make it available for download. The sample comes with absolutely no warranty, but you can download it and see how things are done and reuse pieces of the sample or the full sample.

In the first part I will describe how the search functionality is done inside NAV, how to expose this as a Web Service and search from Microsoft Infopath, getting an XML result set back.

In the second part, I will describe how to create a small Windows application, which connects to the Web Service from part 1 and uses XSLT to transform the XML to a nice HTML document with links back into NAV 2009.

In the third part, I will describe how to create this as a Microsoft Windows Vista Gadget with a flyout showing the search results.

Scenario

The demo scenario goes like this:

In the tray of you Windows, there is a small Dynamics Icon

image

If you click this Icon (or use an assigned global hotkey), a small window pops up

image

The user types in what he is looking for and hit ENTER, which closes the Search Window and pops up the Search Result window:

image

Now the user can click on the links in the left hand side in order to link back into NAV, or select them with the keyboard.

The Vista Gadget which we will be completing in Part 3 looks like:

image

The only disadvantage of the Vista Gadget is, that it is doesn’t really support the keyboard very well – I like the System Tray version better:-)

But – much of this is later on – the outcome of the first part is basically the following:

image

Admitted – not very useful, but stay tuned for part 2 and 3.

Table definitions

When doing wildcard search on multiple tables, we of course need some setup tables, which will tell us which tables to search in. We also need to setup which fields in these tables we want to search in – and we need a table definition for a temporary table in which we can store the search result.

The Search Tables table defines which tables to search through.

image

Table No defines a table to search through.
Page No is the card page which should be used for showing a record from the table.
Id Field No is the field number of the ID field in the table.
Name Field No is the field number of the Name field in the table.

I have only one key in the Search Tables table – and that is Table No.

and the Search Fields table defines which fields to search for in these tables:

image

Table No is the table and Field No is a field which should be included in the search.

Also in the Search Fields we only have one key, which includes both Table No and Field No, and last but not least we need a table definition, which we use for a temporary table while doing the search.

image

for every match we find in a table, we create one record in this temporary table, where

Bookmark is the bookmark (used when launching a page in the Role Tailored Client).
Name is the name field of the record.
Table is the name of the Table in which the record was found.
Id is the id field of the record.
Page is the Page number we want to open in the Role Tailored Client for this record.

It should be clear how the outcome of this can become the XML you see in the InfoPath above – and probably also how this then transforms into the HTML in part 2 and 3.

The Search Codeunit

Disclaimer: Note, that I am not a trained C/AL developer – meaning that the following code might not be the most efficient – but it works for the purpose for which I use it. If you find things, that can be done smarter, better, faster or just things that are made stupid, let me know so that I can learn something as well.

First of all we create a Codeunit called Search and add the following code. The first section of the DoSearch method is all about searching.

The function loops through all Search Tables and for each Search Table, it loops through the Search Fields – and perform a search. For every match we create a record in the results temporary table (if it isn’t already inserted).

DoSearch(searchstring : Text[40];VAR result : BigText)
CLEAR(result);
results.DELETEALL;
IF searchtable.FIND(‘-‘) THEN
BEGIN
REPEAT
rec.OPEN(searchtable.”Table No”);
searchfield.SETRANGE(searchfield.”Table No”, searchtable.”Table No”);
IF searchfield.FIND(‘-‘) THEN
BEGIN
REPEAT
rec.RESET();
field := rec.FIELD(searchfield.”Field No”);
field.SETFILTER(‘*’ + searchstring + ‘*’);
IF rec.FIND(‘-‘) THEN
BEGIN
REPEAT
results.SETRANGE(results.Bookmark, FORMAT(rec.RECORDID,0,10));
IF NOT results.FIND(‘-‘) THEN
BEGIN
results.INIT();
results.Bookmark := FORMAT(rec.RECORDID,0,10);
results.Id := rec.FIELD(searchtable.”Id Field No”).VALUE;
results.Name := rec.FIELD(searchtable.”Name Field No”).VALUE;
results.Page := searchtable.”Page No”;
results.Table := rec.NAME;
results.INSERT();
END;
UNTIL rec.NEXT = 0;
END;
field.SETFILTER(”);
UNTIL searchfield.NEXT =0;
END;
rec.CLOSE;
searchfield.SETRANGE(searchfield.”Table No”);
UNTIL searchtable.NEXT = 0;
END;

Note, the FORMAT(recid, 0, 10) – which is the way to get a bookmark, which can be used for linking back into NAV 2009.

You probably noticed that the result is defined as a BigText and not as a XMLPort. If I was doing to use this function from C# only, I might have made it as a XMLPort – and used the strongly typed interface – but I also need to connect to this Web Service from Javascript (in part 3), so I will stick with the BigText.

That does however mean, that we manually have to build the XML document based on the temporary results table. The following code is also in the DoSearch function:

results.RESET;
results.SETCURRENTKEY(results.Table, results.Id);
IF results.FIND(‘-‘) THEN
BEGIN
CREATE(XMLDoc, false, false);
XMLDoc.async(FALSE);
TopNode := XMLDoc.createNode(1,’SEARCHRESULT’,”);
XMLDoc.appendChild(TopNode);
currentTable := ”;
REPEAT
IF results.Table <> currentTable THEN
BEGIN
currentTable := results.Table;
TableNode := XMLDoc.createNode(1,’TABLE’,”);
TableAttribute := XMLDoc.createAttribute(‘NAME’);
TableAttribute.value := currentTable;
TableNode.attributes.setNamedItem(TableAttribute);
TopNode.appendChild(TableNode);
END;
MatchNode := XMLDoc.createNode(1,’MATCH’,”);
MatchAttribute := XMLDoc.createAttribute(‘PAGE’);
MatchAttribute.value := results.Page;
MatchNode.attributes.setNamedItem(MatchAttribute);
ValueNode := XMLDoc.createNode(1,’BOOKMARK’,”);
ValueTextNode := XMLDoc.createTextNode(results.Bookmark);
ValueNode.appendChild(ValueTextNode);
MatchNode.appendChild(ValueNode);
ValueNode := XMLDoc.createNode(1,’ID’,”);
ValueTextNode := XMLDoc.createTextNode(results.Id);
ValueNode.appendChild(ValueTextNode);
MatchNode.appendChild(ValueNode);
ValueNode := XMLDoc.createNode(1,’NAME’,”);
ValueTextNode := XMLDoc.createTextNode(results.Name);
ValueNode.appendChild(ValueTextNode);
MatchNode.appendChild(ValueNode);
TableNode.appendChild(MatchNode);
UNTIL results.NEXT = 0;
result.ADDTEXT(XMLDoc.xml);
END;

Note that we build the XML in a server side COM object (XMLDoc) and after building the XML Document, I insert that in a BigText in one go.

That statement would fail in the Classic Client (because XMLDoc.xml often is larger than the allowed Text size) – but on the Service Tier, this works just fine because the ADDTEXT takes a string – and there is no size limit on that.

Populating Search Tables and Search Fields

Before we can test the Search Web Service, we need to define which fields we want the search mechanism to run on.

This can of course be done manually – or we can create a function to do this. I of course went for the second approach and created a function to populate the tables with the following data:

image image

You can see the code of that function if you download the sample, but basically it scans through metadata for all tables and searches for tables with a card form specified, which has a Search Name/Description in the table.

Now having populated the Search Tables we are ready to expose the code unit as a Web Service and test it. In the Web Service Table we need to expose the Search Codeunit:

image

having done this – you should be able to start an Internet Explorer and type in the following URL

http://localhost:7047/DynamicsNAV/WS/CRONUS_International_Ltd/Codeunit/Search

giving you the WSDL of the Web Service (given of course that your Service Tier is on localhost, DynamicsNAV is your instance name and you are using the default W1 database.

image

Testing the Web Service from Infopath

Infopath is a nice tool to test your Web Service methods and it is really pretty easy. I have added a walkthrough of how to do case you haven’t tried it before.

Start InfoPath and design a Form Template

image

Base the Form Template on a Web Service

image

Only receive data, since we are not going to alter any data in this case

image

Use the URL pointing to the Search Codeunit Web Service

image

Select the DoSearch operation

image

and give the Data Connection a name

image

Set the title and the Subtitle and drag the search string to the parameters section and the result to the data section

image

Make the result field higher, select TextBox Properties and check the Multiline checkbox, and hit Preview

image

Now the Infopath template launches and can type in cycle in the search string and hit the Run Query button. You probably need to allow Infopath to communicate to your Web Service – but after that you should get the following result

image

That’s it – if you get something similar to this, the search method works.

In part 2 we will create a small Winforms application consuming this Web Service.

You can download a zip file containing the NAV objects in a .fob file and the Infopath template here: http://www.freddy.dk/Search – Part 1.zip.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV