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

If you haven’t read Part 1 and Part 2, 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, and in part 2 we created a simple web site showing a map and adding pushpins.

Now we want to add an action to the customer card, showing us the location of the customer and other customers in the surrounding area.

Parameters to the website

The web site we create in part 2 takes 3 optional parameters: Latitude, Longitude and Zoom

If you try to type in

http://localhost/mysite/default.htm?latitude=0&longitude=0&zoom=6

you will get something like this:

image

Which is location 0,0 and a zoom factor of 5.

Now the only thing we need to do in NAV is to create an action, which launches this web site with the location set to the coordinates of the customer and set the zoom factor to e.g. 10. The web site will then connect back to web services and place push pins where the customers are and that part already works.

Adding the action

In the customer card open the site actions and add an action called View Area Map:

image

Switch to code view and add the following line to the action:

HYPERLINK(‘http://localhost/mysite/default.htm?latitude=’+FORMAT(Latitude,0,2)+’&longitude=’+FORMAT(Longitude,0,2)+’&zoom=10’);

FORMAT(xxx,0,2) ensures that the latitude and longitude are in the correct format and when you launch the action on the Cannon Group you will get:

image

A nice map of the surroundings and if we hover over the customers we can see more information about the customer.

I did not upload the binaries for this to any site – since it really is just adding this one line of code to a new action.

Part 4 of the Virtual Earth Integration will have to wait a little. It uses SP1 features and we are not allowed to blog about SP1 features until the Statement of Direction is published and that should be the case end of March 2009 – so stay tuned for the round-up of Virtual Earth Integration beginning of April 2009.

 

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

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

Disclaimer

Please note that this is an example on how to do geocoding and Virtual Earth integration. There are probably a lot of different ways to do the same, and this might not work in all areas of the world. I do however think that there is enough information in the post that people can make it work anywhere and the good thing about the samples and demos I post in my blog is, that they are free to be used as partners and customers see fit. My samples doesn’t come with any warranty and if installed at customer site, the partner and/or customer takes full responsibility.

Following this sample also does not release you from following any license rules of the products used in the blog (note that I don’t know these rules)

Mappoint or Virtual Earth

As you probably know there already is an integration to Mappoint in NAV 2009. This integration makes it possible to open a map for a given customer (or create route description on how to get there). The way it works is, that when you request an online map, a URL is created which will open a map centered on the requested customer. It is a one way integration – meaning that we can see a map and/or calculate routes.

But… – it is a one way integration. Wouldn’t it be cool if we could request NAV for all customers in a range of 10 miles from another customer – display all customers on a map and have information included directly on Virtual Earth with phone numbers, orders and other things.

That is what this is all about…

But in order to do that, we need more information on our customers than just an address, a city and a country, as this information is hard to query. How would NAV know that Coventry is near Birmingham – if we don’t tell it.

The idea is off course to add geocode information to all customers in our customer table.

We can do this the hard way (typing them in), the other “easy” way (create an automation object which does the trick for you).

Latitude and Longitude

I am not (and I wouldn’t be capable of) trying to describe in details what Latitude and Longitude is – if you want this information you should visit

http://en.wikipedia.org/wiki/Latitude

and

http://en.wikipedia.org/wiki/Longitude

Not that it necessarily helps a lot, but there you have it.

A simpler explanation can be found on http://www.worldatlas.com, which is also, where this image is from

image

Click the image to take you directly to the description.

In this map, coordinates are described as degrees, minutes and seconds + a direction in which this is from the center (N, S, E, W).

There are different ways to write a latitude and a longitude – in my samples I will be using decimal values, where Latitude is the distance from equator (positive values are on the northern hemisphere and negative values are on the southern) and Longitude is the distance from the prime meridian (positive values are going east and negative values are going west). This is the way Microsoft Virtual Earth uses latitude and longitude in the API.

Underneath you will find a map with a pushpin in 0,0.

image

Another location (well known to people in the Seattle area) is Latitude = 47.6 and Longitude = -122.33, which on the map would look like:

image

Yes – the Space Needle.

I think this is sufficient understanding to get going.

Preparing your customer table

First of all we need to create two fields in the customer table, which will hold the geocode information of the customer.

image

Set the Decimalplaces for both fields to 6:8 and remember to create a key, including the two fields (else your searches into the customer table will be slow)

image

You also need to add the fields to the Customer Task Page, in order to be able to edit the values manually if necessary.

Virtual Earth Web Services

In order to use the Microsoft Virtual Earth Web Services you need an account. I do not know the details about license terms etc., but you can visit

https://mappoint-css.live.com/MwsSignup

for signing up and/or read about the terms. I do know that an evaluation developer license is free – so you can sign up for getting one of these – knowing of course that you probably cannot use this for your production data – please contact maplic@microsoft.com for more information on this topic.

Having signed up for a developer account you will get an account ID and you will set a password which you will be using in the application working with the Virtual Earth Web Services. This account ID and Password is used in your application when connecting to Web Services and you manage your account and/or password at

https://mappoint-css.live.com/CscV3

You will also find a site in which you can type in your Account ID and password to test whether it is working.

On these sites there are a number of links to Mappoint Web Services – this is where the confusion started for me…

I wrote some code towards Mappoint and I quickly ran into problems having to specify a map source (which identifies the continent in which I needed to do geocoding). I really didn’t want this, as this would require setup tables and stuff like that in my app. After doing some research I found out that Virtual Earth exposes Web Services to the Internet, which are different from Mappoint (I do not have any idea why). A description of the Virtual Earth Web Services API can be found here:

http://msdn.microsoft.com/en-us/library/cc980922.aspx

and a description of the geocode service can be found here:

http://msdn.microsoft.com/en-us/library/cc966817.aspx

and yes, your newly assigned account and password for Mappoint services also works for Virtual Earth Web Services.

The way it works is, that when connecting to Virtual Earth Web Services you need to supply a valid security token. This token is something you request from a different web service and when requesting this token, you specify the number of minutes the token should be valid (Time-To-Live).

Confused?

Creating a COM automation object for geocoding addresses

If you think you have the grasp around the basics of the Virtual Earth Web Services – let’s get going…

First of all – fire up your Visual Studio 2008 SP1 and create a new Class Library (I called mine NavMaps).

Add the CLSCompliant(true) to the AssemblyInfo.cs file (I usually to this after the ComVisible(false) line).

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components.  If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
[assembly: CLSCompliant(true)]

After this you need to sign the assembly. Do this by opening properties of project, go to the Signing TAB,  check the “Sign the assembly” checkbox and select new – type in a filename and password protect the key file if you want to (I usually don’t).

Next thing is to create the COM interface and Class – the interface we want is:

[ComVisible(true)]
[Guid(“B1F26FE7-0EA0-4883-BD6A-0398F8D2B139”), InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface INAVGeoCode
{
string GetLocation(string query, int confidence, ref double latitude, ref double longitude);
}

For the implementation we need a Service Reference to:

http://staging.dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc?wsdl

called

GeocodeService

Note that the configuration of this Service Reference will be written in app.config – I will touch upon this later.

The implementation could be:

[ComVisible(true)]
[Guid(“9090DF4C-FB24-4a4b-9E49-3924353A6040”), ClassInterface(ClassInterfaceType.None)]
public class NAVGeoCode : INAVGeoCode
{
private string token = null;
private DateTime tokenExpires = DateTime.Now;

    /// <summary>
/// Geocode an address and return latitude and longitude
/// Low confidence is used for geocoding demo data – where the addresses really doesn’t exist:-)
/// </summary>
/// <param name=”query”>Address in the format: Address, City, Country</param>
/// <param name=”confidence”>0 is low, 1 is medium and 2 is high confidence</param>
/// <param name=”latitude”>returns the latitude of the address</param>
/// <param name=”longitude”>returns the longitude of the address</param>
/// <returns>Error message if something went wrong</returns>
public string GetLocation(string query, int confidence, ref double latitude, ref double longitude)
{
try
{
// Get a Virtual Earth token before making a request
string err = GetToken(ref this.token, ref this.tokenExpires);
if (!string.IsNullOrEmpty(err))
return err;

            GeocodeService.GeocodeRequest geocodeRequest = new GeocodeService.GeocodeRequest();

            // Set the credentials using a valid Virtual Earth token
geocodeRequest.Credentials = new GeocodeService.Credentials();
geocodeRequest.Credentials.Token = token;

            // Set the full address query
geocodeRequest.Query = query;
// Set the options to only return high confidence results
GeocodeService.ConfidenceFilter[] filters = new GeocodeService.ConfidenceFilter[1];
filters[0] = new GeocodeService.ConfidenceFilter();
switch (confidence)
{
case 0:
filters[0].MinimumConfidence = GeocodeService.Confidence.Low;
break;
case 1:
filters[0].MinimumConfidence = GeocodeService.Confidence.Medium;
break;
case 2:
filters[0].MinimumConfidence = GeocodeService.Confidence.High;
break;
default:
return “Wrong value for confidence parameter”;
}

            GeocodeService.GeocodeOptions geocodeOptions = new GeocodeService.GeocodeOptions();
geocodeOptions.Filters = filters;

            geocodeRequest.Options = geocodeOptions;

            // Make the geocode request
GeocodeService.IGeocodeService geocodeService = new ChannelFactory<GeocodeService.IGeocodeService>(new BasicHttpBinding(), new EndpointAddress(“http://staging.dev.virtualearth.net/webservices/v1/geocodeservice/GeocodeService.svc&#8221;)).CreateChannel();
GeocodeService.GeocodeResponse geocodeResponse = geocodeService.Geocode(geocodeRequest);

            if (geocodeResponse.Results.Length == 0 || geocodeResponse.Results[0].Locations.Length == 0)
{
return “No locations found”;
}
latitude = geocodeResponse.Results[0].Locations[0].Latitude;
longitude = geocodeResponse.Results[0].Locations[0].Longitude;

            return “”;
}
catch (Exception ex)
{
return ex.Message;
}
}
}

Before we add the last function – GetToken – I would like to draw attention to the line:

GeocodeService.IGeocodeService geocodeService = new ChannelFactory<GeocodeService.IGeocodeService>(new BasicHttpBinding(), new EndpointAddress(“http://staging.dev.virtualearth.net/webservices/v1/geocodeservice/GeocodeService.svc&#8221;)).CreateChannel();

This isn’t normally the way you would instantiate the service class. In fact normally you would see:

GeocodeService.GeocodeServiceClient geocodeService = new GeocodeService.GeocodeServiceClient();

which is simpler, looks nicer and does the same thing – so why bother?

Which configuration file to use?

The primary reason is to avoid using the configuration file. The standard way of instantiating the Service Client is looking for a number of settings in the appSettings section in the config file – and you wouldn’t think that should be a problem – but it is. The problem is that it uses the application configuration file – NOT the DLL config file, and I couldn’t find any way to make it read the DLL config file for these settings.

So if my NavMaps.dll should be accessible from the classic client, I would have to create a finsql.exe.config with the right configuration. Microsoft.Dynamics.Nav.Client.exe.config would be the configuration file for the Roletailored Client (if we are running the automation client side) and I would have to find a way to merge the config settings into the service tier configuration if we are running the automation server side.

So, to avoid all that crap (and severe deployment problems), I instantiate my WCF Client manually through code – meaning that no app.config is necessary.

Requesting a Virtual Earth Security Token

First you need to add a Web Reference to

https://staging.common.virtualearth.net/find-30/common.asmx?wsdl

called

TokenWebReference

The code for the GetToken could look like this:

/// <summary>
/// Check validity of existing security token and request a new
/// Security Token for Microsoft Virtual Earth Web Services if necessary
/// </summary>
/// <param name=”token”>Security token</param>
/// <param name=”tokenExpires”>Timestamp for when the token expires</param>
/// <returns>null if we have a valid token or an error string if not</returns>
private string GetToken(ref string token, ref DateTime tokenExpires)
{
if (string.IsNullOrEmpty(token) || DateTime.Now.CompareTo(tokenExpires) >= 0)
{
// Set Virtual Earth Platform Developer Account credentials to access the Token Service
TokenWebReference.CommonService commonService = new TokenWebReference.CommonService();
commonService.Credentials = new System.Net.NetworkCredential(“<your account ID>”, “<your password>”);

        // Set the token specification properties
TokenWebReference.TokenSpecification tokenSpec = new TokenWebReference.TokenSpecification();
IPAddress[] localIPs = Dns.GetHostAddresses(Dns.GetHostName());
foreach (IPAddress IP in localIPs)
{
if (IP.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
{
tokenSpec.ClientIPAddress = IP.ToString();
break;
}
}
// Token is valid an hour and 10 minutes
tokenSpec.TokenValidityDurationMinutes = 70;

        // Get a token
try
{
// Get token
token = commonService.GetClientToken(tokenSpec);
// Renew token in 1 hour
tokenExpires = DateTime.Now.AddHours(1);
}
catch (Exception ex)
{
return ex.Message;
}
}
return null;
}

Note that I am giving the token a TTL for 70 minutes – but renew after 60 – that way I shouldn’t have to deal with tokens being expired.

<your account ID> should be replaced by your account ID and <your password> should be replaced with your password.

Using the geocode automation object

Having build the assembly we need to put it to play.

After building the assembly we need to place it in the “right” folder – but what is the right folder?

It seems like we have 4 options

  1. In the Classic folder
  2. In the RoleTailored Client folder
  3. In the Service Tier folder
  4. Create a Common folder (next to the Classic, RoleTailored and Service Tier folders) and put it there

Let me start by excluding the obvious choice, the Service Tier folder. The reason is, that if you have multiple Service Tiers, they will all share the same COM automation object and you don’t really know which directory the assembly is read from (the one where you did the last regasm). You could accidently delete the Service Tier in which the registered assembly is located and thus end up having denial of service.

RoleTailored Client folder also seems wrong – there is absolutely no reason for running this object Client side – it should be on the Service Tier and there might not be a RoleTailored Client on the Service Tier. The same is case with the Classic folder – so I decided to create a Common folder and put the DLL there. Another reason for selecting the Common folder is, that you can install it on the Client (for being able to run it in the Classic Client) in a similar way as on the server.

Your situation might be different and you might select a different option.

After copying the DLL to the Common folder, we need to register the DLL and make it available as a COM automation object.

C:\Windows\Microsoft.NET\Framework\v2.0.50727\regasm NAVMaps.dll /codebase /tlb

Is the command to launch.

Creating a codeunit for geocoding your customers

What I normally do in situations like this is, to create a codeunit, which initializes itself when you Run it.

In this case Running the codeunit should run through all customers and geocode them.

OnRun()
IF cust.FIND(‘-‘) THEN
BEGIN
REPEAT
err := UpdateLatitudeAndLongitude(cust);
IF (err <> ”) THEN
BEGIN
IF NOT CONFIRM(‘Customer ‘+cust.”No.”+ ‘ – Error: ‘+err + ‘ – Continue?’) THEN EXIT;
END;
UNTIL cust.NEXT = 0;
END;

Whether or not you want an error here or not is kind of your own decision.

The function, that does the job looks like:

UpdateLatitudeAndLongitude(VAR cust : Record Customer) error : Text[1024]
CREATE(NavMaps, TRUE, FALSE);
country.GET(cust.”Country/Region Code”);
query := cust.Address+’, ‘+cust.”Address 2″+’, ‘+cust.City+’, ‘+’, ‘+cust.”Post Code”+’, ‘+country.Name;
error := NavMaps.GetLocation(query, 2, cust.Latitude, cust.Longitude);
IF (error = ”) THEN
BEGIN
cust.MODIFY();
END ELSE
BEGIN
query := cust.City + ‘, ‘ + country.Name;
error := NavMaps.GetLocation(query, 0, cust.Latitude, cust.Longitude);
IF (error = ”) THEN
BEGIN
cust.MODIFY();
END;
END;

Local variables looks like this

image

Now we can discuss whether this is the right way to go around it – problem for me is, that the demodata are not real addresses – meaning that the street names are fine, city names are fine – but the street and number doesn’t exist in the city.

So – what I do here is to start building a query with Address, City, Post code, Country Name – and tell Virtual Earth to make a High confidence search – this will return the location of correct addresses. In the demo database, this returns absolutely nothing. Next thing is to just give me the location of the city in the country – whether you want to do this for your customer table is kind of your own decision, but it works for the demo data.

BTW – i tried this with addresses in Denmark (my former) and the US (my current) – and the format seems to suit Virtual Earth in these countries, but I didn’t try a lot of other addresses. The advantage of using the query is really to give Virtual Earth the freedom to interpret and look at the address – and it does a pretty good job. If anybody experiences problems with this in certain countries – please let me know (with a suggestion as to what should be changed) and I will include this.

Can’t make it work?

If (for some reason) this code doesn’t work for you – but you really want to get on with the next two parts of this walkthrough, I have included a table here with Latitudes and Longitudes for all the customers in the W1 database.

Copy the entire table into a clean excel spreadsheet and use Edit In Excel to copy the data from the two columns to the two columns in your customer table in one go and save it back to NAV. Note that the Latitude and Longitude is only available in Excel if you

  1. Have added the fields to the customer card
  2. Updated the Web Reference in the Edit In Excel project to the customer card after doing step 1 (if you already had Edit In Excel running)

If you don’t have the Edit In Excel – you can find it here http://blogs.msdn.com/freddyk/archive/tags/Excel/default.aspx

 

No Name Latitude Longitude
01121212 Spotsmeyer’s Furnishings 25.72898470 -80.23741968
01445544 Progressive Home Furnishings 41.86673200 -87.70100800
01454545 New Concepts Furniture 33.77397700 -84.38731800
01905893 Candoxy Canada Inc. 48.38170029 -89.24547985
01905899 Elkhorn Airport 49.97715327 -101.23461867
01905902 London Candoxy Storage Campus 42.98689485 -81.24621458
10000 The Cannon Group PLC 52.47865520 -1.90859489
20000 Selangorian Ltd. 52.42190337 -1.53778508
20309920 Metatorad Malaysia Sdn Bhd 3.08329985 101.64999984
20312912 Highlights Electronics Sdn Bhd 3.15021023 101.71284467
20339921 TraxTonic Sdn Bhd 1.54907294 110.34416981
21233572 Somadis 34.01504517 -6.83272026
21245278 Maronegoce 33.60242486 -7.61274353
21252947 ElectroMAROC 33.91666643 -6.91666670
27090917 Zanlan Corp. -26.35520540 27.40158677
27321782 Karoo Supermarkets -29.11835074 26.22492447
27489991 Durbandit Fruit Exporters -29.83637005 30.94218850
30000 John Haddock Insurance Co. 53.47962007 -2.24880964
31505050 Woonboulevard Kuitenbrouwer 52.14019530 6.19148992
31669966 Meersen Meubelen 51.98542312 5.90462968
31987987 Candoxy Nederland BV 52.37311967 4.89319481
32124578 Nieuwe Zandpoort NV 51.17786638 4.83266278
32656565 Antarcticopy 51.22171506 4.39739518
32789456 Lovaina Contractors 50.88170014 4.71750006
33000019 Francematic 48.81669022 1.94925517
33002984 Parmentier Boutique 48.85692470 2.34120972
33022842 Livre Importants 48.90484169 2.81284869
34010100 Libros S.A. 41.38566770 2.16993861
34010199 Corporación Beta 39.43432257 -0.38737597
34010602 Helguera industrial 40.41576270 -3.70385108
35122112 Bilabankinn 64.11111474 -21.90939903
35451236 Gagn &Gaman 64.92900006 -18.96200001
35963852 Heimilisprydi 64.13533777 -21.89521417
38128456 MEMA Ljubljana d.o.o. 46.05124690 14.50306222
38546552 EXPORTLES d.o.o. 46.05124690 14.50306222
38632147 Centromerkur d.o.o. 46.55813813 15.65098330
40000 Deerfield Graphics Company 51.86390832 -2.24978395
41231215 Sonnmatt Design 47.42380030 8.55140001
41497647 Pilatus AG 47.05957649 8.30785449
41597832 Möbel Scherrer AG 47.69385177 8.63503763
42147258 BYT-KOMPLET s.r.o. 49.03722882 17.81008579
42258258 J &V v.o.s. 48.98017220 17.21433230
42369147 PLECHKONSTRUKT a.s. 48.85557219 16.05438888
43687129 Designstudio Gmunden 47.91861087 13.79814081
43852147 Michael Feit – Möbelhaus 47.58900024 14.13999975
43871144 Möbel Siegfried 48.16780201 16.35428691
44171511 Zuni Home Crafts Ltd. 52.49931332 -2.13111123
44180220 Afrifield Corporation 51.27380021 0.52508533
44756404 London Light Company 52.20986970 0.11156514
45282828 Candoxy Kontor A/S 56.15704469 10.20700961
45282829 Carl Anthony 56.15704469 10.20700961
45779977 Ravel Møbler 55.31117991 10.79238497
45979797 Lauritzen Kontormøbler A/S 57.03462996 9.92748722
46251425 Marsholm Karmstol 56.67225949 12.85753034
46525241 Konberg Tapet AB 57.78593438 14.22523925
46897889 Englunds Kontorsmöbler AB 58.59301685 16.17726378
47523687 Slubrevik Senger AS 59.85794865 10.47694914
47563218 Klubben 59.91503946 10.56067966
47586954 Sjøboden 71.07307523 24.70469333
49525252 Beef House 51.21562980 6.77605525
49633663 Autohaus Mielberg KG 53.55334528 9.99244496
49858585 Hotel Pferdesee 50.04764177 8.57851513
50000 Guildford Water Department 51.23708010 -0.57051592
60000 Blanemark Hifi Shop 51.51777074 -0.15552994
61000 Fairway Sound 51.50632493 -0.12714475
62000 The Device Shop 51.50632493 -0.12714475
IC1020 Cronus Cardoxy Sales 56.10199973 9.55599993
IC1030 Cronus Cardoxy Procurement 53.55334528 9.99244496

Note that some of the customers have the same latitude, longitude – this is due to the demo data.

Next steps

Ok admitted – that was a long post – but hopefully it was helpful (and hopefully you can make it work)

As usual – you can download the objects and the Visual Studio solution – and please do remember that this is NOT a solution that is installable and will work all over – you might have issues or problems and I would think the best place to post questions is mibuso (where my answer will get read by more people).

In the next post I will create a website, which connects to NAV Web Services to get customer location information – and in the third post I will show how to create an action in the customer List and Card to open up an area map centered around a specific customer.

So… – after step 2 you will see this

image

Stay tuned!

The forth post in this series is a surprise – and will require NAV 2009 SP1 in order to work – so stay tuned for this one…

 

Enjoy and good luck

 

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

The Customer MAP demo

My newest demo was shown at the NAV Partner Keynote and also on the NAV Customer General session.

I also did a very short walkthrough of the demo at the NAV06 concurrent session at Convergence – and I promised a number of people that I would add a walkthrough of how the demo is done on my blog.

This post is only an appetizer – the real stuff is going to be out here over the next couple of days – and will be a 3 step walkthrough.

  1. How to geocode the customers in your customer database using a Microsoft Virtual Earth Web Service
  2. How to use this geocode information from an intranet application using Microsoft Virtual Earth
  3. Adding an action to the Customer page to view other customers in the area

A screenshot of the demo can be seen here

image

and basically what happens is that I added a Latitude and a Longitude field to the Customer table – and created a small automation object, which can geocode adresses. Having this information in the customer table enables a lot of cool scenarios – the above one is just the first simple one, that springs into mind.

Beside the geocoding the above solution requires a Web Service codeunit to be exposed and a small intranet application.

The Web Service codeunit contains a function that returns all the customers in a rectangle of the world (given by the bottom left and the top right latlong coordinates) – and the small intranet application just calls this webservice every time you zoom or pan the map.

Stay tuned

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

More SSD testing

If you haven’t read my post about Running Microsoft Dynamics NAV on SSD’s – you should do so first.

After having posted the initial results, I was contacted by other vendors of SSD’s wanting to see whether we could do some additional testing on other hardware. In the interest of the end user, I accepted and once more allocated a slot in the performance lab in Copenhagen.

The new drives to test were:

  • STEC 3½” Zeus IOPS SSD 146GB
  • STEC 2½” MACH8 IOPS SSD 100GB
  • Intel 2½” SSDSA2SH032G1GN 32GB (actually 32GB wasn’t enough for the testing so we took two of those and striped them)

All of these drives looks like standard HDD’s with a SATA interface. Installation is plug and play and no driver installation.

Disclaimer

Remember that the tests we have run here are scenario tests, designed to measure performance deltas on Microsoft Dynamics NAV to make sure that a certain build of NAV doesn’t suddenly get way slower than the previous version and gets shipped with poor performance.

Also again – I haven’t optimized the SQL server at all when doing these tests so you might not see the same performance gain if you switch your drives to SSD’s – or you might see more performance gain (if you know how to optimize for these things).

My testing is ONLY replacing a standard HDD (Seagate Barracuda 500GB, 7200 RPM SATA) with a SSD – and test the same scenarios.

The scenarios are being run for 180minutes each and the perf. testing starts after a 10 minutes warm up time. The tests I will be referring to here are all done simulating 50 users towards a service tier.

Final build of NAV 2009

Between the time of the prior tests and the new tests we released the final version of NAV – so the tests in this post will be based on the RTM version of NAV. Also we got some new Lab equipment – so in order to be honest to the FusionIO tests – we retook all of these tests as well.

In the new list of tests you will see 5 results: HDD, FusionIO, STEC5 (3½”), STEC3 (2½”), Intel

I will use the same tests as in the original post.

Here we go

clip_image002

As we can see if we compare this test to the test on the pre-release – this test is faster on the HDD than the prior test was on the FusionIO SSD.

This of course also means that the performance gain by using SSD’s in this test is smaller – but still a 21% performance enhancement by changing the drive to SSD isn’t bad at all.

clip_image002

Again 25% performance enhancement by changing to SSD’s and the difference between the different types of SSD’s is insignificant in comparison.

clip_image002[6]

Again 25-30% performance enhancement by changing to SSD’s and not a huge difference between the technologies.

Note, that as the numbers get lower – the measurement uncertainty can play a role in the results.

clip_image002[8]

This test is around 10 times faster than when we did the test on the pre-release and now the results are so fast that the uncertainty causes some results as being skyhigh. Analyzing the results actually reveals that there isn’t that much of a difference.

clip_image002[10]

Same picture again – significant performance enhancement changing to SSD’s – not a huge difference between the technologies.

Wrap-up

Test results where not as clear as the last time – primarily because the RTM version solved some of the perf. problems and due to new hardware in the Perf. lab – but still tests show 20-30% performance increase.

I still think SSD’s are here to stay and I do think that people can take advantage of the increased performance they will get simply by changing the drives in their SQL Server. I haven’t tested what performance enhancements you would get from running the Service Tier on a box with SSD’s – but I wouldn’t expect a huge advantage if the service has sufficient RAM.

I will not be conducting any more tests – the primary reasons for this is, that I do not have the hardware anymore – meaning that I couldn’t do a re-run on the same hardware and compare all the different technologies – so any comparison would be unfair to one or the other.

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

PageUp and PageDown in the Role Tailored Client

If you have been working with the Role Tailored Client you are probably aware, that it doesn’t support PageUp and PageDown in Task Pages. The reason for removing this support was, that the new User Experience is centered around starting from the List, and this way you have a better overview and can find the records you are searching for right away easier and then open the Task Page.

This is all good, but users have told us, that they would like to use PageUp and PageDown in Task Pages anyway and the reason is that they want to do work on all the records they have selected in a List Place with e.g. Delayed Sales Orders. Why does the user have to close the Task Page, select the next record and reopen the Task Page.

Another scenario is users looking through recent sales orders, to find an order where the customer bought 20 bicycles (because the user remembers this).

So, no doubt that this is something we will be looking at supporting in the future – but NAV 2009 is released, and it does NOT support PageUp and PageDown.

In this post I will explain how to make a poor-man’s PageUp and PageDown in a Page – the goal is to show how this is done, and if anybody run into a customer, with the need of having PageUp and PageDown in e.g. the Sales Order Page – then they can use this as inspiration.

It isn’t a perfect solution, but it is better than nothing. To apply it on ALL pages would be a lot of work – but to apply it on a couple of pages (where the users request it the most) might be the difference between an unsatisfied user and a happy user.

In this sample, I will add the functionality to the Sales Order List Place, which is page 9305. In the List Place we will add an action called Edit All and in the Task Page for the Sales Order (which is 42) we will add two actions: Next Record and Previous Record.

The Edit All action

First, open the Page Designer for Page 9305. Move the selected line to the line after the last object – and select View -> Actions.

In the list of actions – create a new action called Edit All.

image

In the properties of action we change two values:

image

We set the Image, so that the action has the same image as Edit, and we set the ShortCutKey to Return.

Setting the ShortCutKey to Return means that whenever you press ENTER in the list – this action is called instead of the built-in Edit. DoubleClick does the same as ENTER, so my action also gets called when double clicking a row. Ctrl+Shift+E still calls the Built-in Edit action, like when you select that action in the Menu – the only trick here is ENTER and double click – no magic.

Close the properties and hit F9 to modify the code for the actions. Unfortunately F9 doesn’t position you in the action you are modifying, so you will have to find the right action, add the following variables

image

and the following code

<Action5> – OnAction()
newrec.SETPOSITION(GETPOSITION());
newpage.SETRECORD(newrec);
//newpage.SetViewFromList(GETVIEW(true));
newpage.RUN();

Note, that the SetViewFromList method call is out commented – we haven’t created this function yet on the Sales Order page, so this function more or less does exactly the same as the normal Edit function – open the Sales Order page with the currently selected record.

But we want more…

The Card needs to know the View

In order to support Page Up and Page Down, our Task Page needs to know the Filter and sorting from the List. Both these things are in the value we get from GETVIEW, so we will create a function on the Sales Order page to receive this from our list.

Design the Sales Order page (42) and hit F9 – create a new Global Variable called View (Text[250]) and set the Include In Dataset in the properties

image

Reason for the include in dataset is that it now can be used in the property expressions on actions (we don’t want to have the Next and Previous actions enabled if you cannot do next or previous).

Also create a function called SetViewFromList like this

SetViewFromList(ViewFromList : Text[250])
View := ViewFromList;

We could also insert the following lines to our OnOpenPage (as the first lines)

IF View <> ” THEN BEGIN
CurrPage.CAPTION := View;
END;

Having done this, and saved (with compile) the Sales Order page, we can go back to the Edit All action and remove the comment from the line

newpage.SetViewFromList(GETVIEW(true));

Save and run.

Open your sales order list place, create a filter on Location Code = YELLOW and double click on an order and you will get an order with a different caption

image

A little nerdy –  but I didn’t want to create a function for making the filter human readable in this post.

So far so good…

The Next and Previous Actions

So now, our Task Page knows about the filter, which was applied on the list. We of course haven’t done anything with it yet – but the remaining should be pretty straightforward for people who knows C/AL

Create two actions (at the very end of the list – make sure to get the indent right)

Set the shortcut key of Next Record to Ctrl+PgDn and the shortcut key of Previous Record to Ctrl+PgUp. For some reason I cannot use PgUp and PgDn, the Client crashes when I try to do this with an invalid Shortcut key value – I guess we could have handled this situation a little nicer:-).

Both actions should also have an enabled expression called View <> ”

image

Before adding in the code for these actions we create another global function called NextPrev. In this function we add two local variables

image

and the following code

NextPrev(Ascending : Boolean)
newrec.SETVIEW(View);
newrec.SETPOSITION(Rec.GETPOSITION());
newrec.ASCENDING(Ascending);
newrec.NEXT(1);
IF Rec.GETPOSITION() = newrec.GETPOSITION() THEN EXIT;
CurrPage.CLOSE();
newpage.SetViewFromList(View);
newpage.SETRECORD(newrec);
newpage.RUN();

Basically what we do is to locate the next record (ascending or descending) that matches the filter – and if this record is different from the current record – it closes the current page and opens a new page with the next or previous record (it is not possible to change the current record on a page).

It also transfers the View to the next page, so that Next and Previous still works.

The code for the actions should be straight forward

<Action3> – OnAction()
NextPrev(TRUE);

<Action5> – OnAction()
NextPrev(FALSE);

Save, Compile and run the Role Tailored Client.

Yes – it does some flickering when the page closes and reopens – but it works.

Note that if you move the window and use next and previous, then the next window will open on the currently saved position (the CLOSE doesn’t complete and save the new location as the preferred location until after the new page opens) so the window will jump back and forth – I haven’t found any way to avoid that.

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 2 (out of 3)

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

In this section we will create a small Winforms application, which uses the Web Service we just created in part 1.

Our application will be visible as a System Tray Icon, it will have a global Windows Hotkey with which we can activate search and when you activate the Search application it will popup and look like this

image

In part 3 we will create a Windows Vista Gadget version of the same app.

Visual Studio

I assume that you have worked with Visual Studio and C# before (this is not a C# tutorial – though you can of course play around with the code if you want to learn) and I will be using Visual Studio 2008 (incl. SP1) for my samples, and I won’t go into details about every function in the solution – I will however try to explain how things works and show a couple of the functions (you can download the full sample and play around with it).

In Program.cs (main program), we create an instance of the SearchForm. We do not give the form to Application.Run() – this would show the Search Form immediately and we don’t want that.

The Form has a NotifyIcon (an Icon in the System Tray) and a context menu for that Icon – all of that is setup in the Visual Studio Forms Designer and you will find event handlers for the menu items and for when the user is clicking the NotifyIcon in the code.

The application has a reference to the Search Web Service from part 1 – and currently this is pointing to

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

If you need to change that, you do not need to recompile and change the application – you can do this by modifying the .config file, which gets deployed next to the .exe file (named the same as the .exe file with .config behind – standard .net thingy).

The NAVSearch.exe.config contains a setting for the reference, that looks like this:

<setting name=”NAVsearch_SearchReference_Search” serializeAs=”String”>
<value>
http://localhost:7047/DynamicsNAV/WS/CRONUS_International_Ltd/Codeunit/Search</value>
</setting>

The “main” code is the Event Handler for the Search Button Click event. I will let the code and the comments speak for itself.

/// <summary>
/// Event Handler for Click on the Search Button
/// </summary>
private void bSearch_Click(object sender, EventArgs e)
{
// Create the Service proxy class
SearchReference.Search searchService = new NAVsearch.SearchReference.Search();
searchService.UseDefaultCredentials = true;

    // Invoke the DoSearch method
string result = “”;
searchService.DoSearch(this.eSearch.Text.ToUpper(), ref result);

    // Did we get a result back?
if (!string.IsNullOrEmpty(result))
{
// Load the result into an XML Document
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(result);

        // Load the XSLT Transformation document
XslCompiledTransform xslTrans = new XslCompiledTransform() ;
xslTrans.Load(Path.GetDirectoryName(Application.ExecutablePath) + @”SearchResultToHTML.xslt”);

        // Perform the transformation in memory
StringBuilder sb = new StringBuilder();
StringWriter sw = new StringWriter(sb);
xslTrans.Transform(xmlDoc, null, sw);

        // Hide the Search form
HideSearchForm();

        // Set local image path
string html = sb.ToString().Replace(“#path#”, “file://” + Path.GetDirectoryName(Application.ExecutablePath) + @””);

        // Show the Search Results form
resultsForm = SearchResultsForm.ShowHTML(html);
}
}

The result returned from WebServices was a BigText, which is a string in C# – we simply take that string and load it in an XML Document (if it isn’t empty of course).

The stylesheet needed for transforming the XML into HTML (SearchResultToHTML xslt) is also included in the project, and it also includes the images used by the HTML. In the XSLT all the images are preceded with a path identifier #path#, which we replace with the application directory in order to show the HTML proper.

I won’t go into detail about how XSLT works, there are a ton of resources on the Internet explaining this, I use it for getting from the XML I get from the Search Codeunit to the HTML – and it works for the purpose.

ShowHTML is a static function on the Search Result Form, which opens the Search Result form and displays the HTML in a webbrowser control inside the form.

/// <summary>
/// Open the search result form and show an HTML document
/// </summary>
/// <param name=”html”>HTML document to show</param>
/// <returns>The Search Result Form</returns>
public static SearchResultsForm ShowHTML(string html)
{
SearchResultsForm form = new SearchResultsForm();
form.webBrowser1.DocumentText = html;
form.Show();
form.Activate();
return form;
}

There are a number of small functions in the application to control the behavior of the application.

Windows Key + Z opens up the Search Form – is controlled by the statements

User32.RegisterHotKey(this.Handle, this.GetType().GetHashCode(), (int)Modifiers.MOD_WIN, (int)Keys.Z);

in the constructor and the following method

/// <summary>
/// Event Handler for Windows Messages
/// </summary>
protected override void WndProc(ref Message m)
{
// Only react on WM_HOTKEY
if (m.Msg == (int)Msgs.WM_HOTKEY)
{
// Show the Search Form
this.ShowSearchForm();
}
// Invoke default Message Handler
base.WndProc(ref m);
}

Of course the global hotkey is destroyed in OnClosed – when exiting the application.

When the form is deactivated – we want to hide the form, that is achieved by

/// <summary>
/// Event handler for Deactivate form
/// Hide the SearchForm when it gets deactivated
/// </summary>
private void SearchForm_Deactivate(object sender, EventArgs e)
{
HideSearchForm();
}

and whenever the form is shown, there is some housekeeping to make sure that the Search Result form is closed, it opens up in the right location and it is activated and ready to type in.

/// <summary>
/// Show the Search Form
/// </summary>
private void ShowSearchForm()
{
// If the result Form is open – close it
if (resultsForm != null)
{
resultsForm.Close();
resultsForm = null;
}
// Set the location of the searchform to the lower right corner
this.Location = new Point(System.Windows.Forms.Screen.GetWorkingArea(this).Width – this.Size.Width,
System.Windows.Forms.Screen.GetWorkingArea(this).Height – this.Size.Height);
// SearchForm is topmost
this.TopMost = true;
// Show the Search Form
this.Show();
// Activate it
this.Activate();
// Select the text in the Search TextBox and put focus in the control
this.eSearch.SelectAll();
this.eSearch.Focus();
}

The original demo scenario was a user sitting in Word, wanting to find information about an item in his NAV. The user hits Windows+Z, types in what he is looking for and hits ENTER. The Search Result Form opens with focus and the user can use TAB to select the area in which we wants to look at results or the user can move directly to the search result he is looking for and press ENTER (or use the mouse of course).

When the user presses ESC in the Task Page opened from the Search Result Form the user will return to the Search Result Form and when the user presses ESC again the user is back in the Search Form – and one more ESC will bring him back into Word and the user can continue his work.

You can download the solution for NAV Search here http://www.freddy.dk/Search – Part 2.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