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

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