Using touchscreens with the RoleTailored Client

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

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

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

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

image

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

Button Panels

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

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

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

Escaping from Javascript

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

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

Show me the code!!!

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

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

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

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

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

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

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

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

and now the Add-In part of the Control.

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

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

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

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

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

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

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

Things to note:

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

The MyScriptManager could look like this:

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

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

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

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

window.external.clickevent(i, s);

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

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

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

Let’s put the BrowserControl to work for us

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

image

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

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

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

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

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

this should give us the following page when running:

image

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Word Management

As with the release of Microsoft Dynamics NAV 2009, I was also deeply involved in the TAP for Microsoft Dynamics NAV 2009 SP1. My primary role in the TAP is to assist ISVs and partners in getting a customer live on the new version before we ship the product.

During this project we file a lot of bugs and the development team in Copenhagen are very responsive and we actually get a lot of bugs fixed – but… not all – it happens that a bug is closed with “By Design”, “Not Repro” or “Priority too low”.

As annoying as this might seem, I would be even more annoyed if the development team would take every single bug, fix it, run new test passes and punt the releases into the unknown. Some of these bugs then become challenges for me and the ISV / Partner to solve, and during this – it happens that I write some code and hand off to my contact.

Whenever I do that, two things are very clear

  1. The code is given as is, no warranty, no guarantee
  2. The code will be available on my blog as well, for other ISV’s and partners to see

and of course I send the code to the development team in Copenhagen, so that they can consider the fix for the next release.

Max. 64 fields when merging with Word

One of the bugs we ran into this time around was the fact that when doing merge with Microsoft Word in a 3T environment, word would only accept 64 merge fields. Now in the base application WordManagement (codeunit 5054) only uses 48 fields, but the ISV i was working with actually extended that to 100+ fields.

The bug is in Microsoft Word, when merging with file source named .HTM – it only accepts 64 fields, very annoying.

We also found that by changing the filename to .HTML, then Word actually could see all the fields and merge seemed to work great (with one little very annoying aberdabei) – the following dialog would pop up every time you open Word:

clip_image002

Trying to figure out how to get rid of the dialog, I found the right parameters to send to Word.OpenDataSource, so that the dialog would disappear – but… – then we are right back with the 64 fields limitation.

The reason for the 64 field limitation is, that Word loads the HTML as a Word Document and use that word document to merge with and in a document, you cannot have more than 64 columns in a table (that’s at least what they told me).

I even talked to PM’s in Word and got confirmed that this behavior was in O11, O12 and would not be fixed in O14 – so no rescue in the near future.

Looking at WordManagement

Knowing that the behavior was connected to the merge format, I decided to try and change that – why not go with a good old fashion .csv file instead and in my quest to learn AL code and application development, this seemed like a good little exercise.

So I started to look at WordManagement and immediately found a couple of things I didn’t like

MergeFileName := RBAutoMgt.ClientTempFileName(Text029,’.HTM’);
IF ISCLEAR(wrdMergefile) THEN
CREATE(wrdMergefile,FALSE,TRUE);
// Create the header of the merge file
CreateHeader(wrdMergefile,FALSE,MergeFileName);
<find the first record>
REPEAT
// Add Values to mergefile – one AddField for each field for each record
  wrdMergefile.AddField(<field value>);
  // Terminate the line
wrdMergefile.WriteLine;
UNTIL <No more records>
// Close the file
wrdMergefile.CloseFile;

now wrdMergefile is a COM component of type ‘Navision Attain ApplicationHandler’.MergeHandler and as you can see, it is created Client side, meaning that for every field in every record we make a roundtrip to the Client (and one extra roundtrip for every record to terminate the line) – now we might not have a lot of records nor a lot of fields, but I think we can do better (said from a guy who used to think about clock cycles when doing assembly instructions on z80 processors back in the start 80’s – WOW I am getting old:-))

One fix for the performance would be to create the file serverside and send it to the Client in one go – but that wouldn’t solve our original 64 field limitation issue. I could also create a new COM component, which was compatible with MergeHandler and would write a .csv instead – but that wouldn’t solve my second issue about wanting to learn some AL code.

Creating a .csv in AL code

I decided to go with a model, where I create a server side temporary file for each record, create a line in a BigText and write it to the file. After completing the MergeFile, it needs to be downloaded to the Client and deleted from the service tier.

The above code would change into something like

MergeFileName := CreateMergeFile(wrdMergefile);
wrdMergefile.CREATEOUTSTREAM(OutStream);
CreateHeader(OutStream,FALSE); // Header without data
<find the first record>
REPEAT
CLEAR(mrgLine);
// Add Values to mergefile – one AddField for each field for each record
AddField(mrgCount, mrgLine, <field value>);
  // Terminate the line
mrgLine.ADDTEXT(CRLF);
  mrgLine.WRITE(OutStream);
CLEAR(mrgLine);
UNTIL <No more records>
// Close the file
wrdMergeFile.Close();
MergeFileName := WordManagement.DownloadAndDeleteTempFile(MergeFileName);

As you can see – no COM components, all server side. A couple of helper functions are used here, but no rocket science and not too different from the code that was.

CreateMergeFile creates a server side temporary file.

CreateMergeFile(VAR wrdMergefile : File) MergeFileName : Text[260]
wrdMergefile.CREATETEMPFILE;
MergeFileName := wrdMergefile.NAME + ‘.csv’;
wrdMergefile.CLOSE;
wrdMergefile.TEXTMODE := TRUE;
wrdMergefile.WRITEMODE := TRUE;
wrdMergefile.CREATE(MergeFileName);

AddField adds a field to the BigText. Using AddString, which again uses DupQuotes to ensure that “ inside of the merge field are doubled.

AddField(VAR count : Integer;VAR mrgLine : BigText;value : Text[1024])
IF mrgLine.LENGTH = 0 THEN
BEGIN
count := 1;
END ELSE
BEGIN
count := count + 1;
mrgLine.ADDTEXT(‘,’);
END;
mrgLine.ADDTEXT(‘”‘);
AddString(mrgLine, value);
mrgLine.ADDTEXT(‘”‘);

AddString(VAR mrgLine : BigText;str : Text[1024])
IF STRLEN(str) > 512 THEN
BEGIN
mrgLine.ADDTEXT(DupQuotes(COPYSTR(str,1,512)));
str := DELSTR(str,1,512);
END;
mrgLine.ADDTEXT(DupQuotes(str));

DupQuotes(str : Text[512]) result : Text[1024]
result := ”;
REPEAT
i := STRPOS(str, ‘”‘);
IF i <> 0 THEN
BEGIN
result := result + COPYSTR(str,1,i) + ‘”‘;
str := DELSTR(str,1,i);
END;
UNTIL i = 0;
result := result + str;

and a small function to return CRLF (line termination for a merge line)

CRLF() result : Text[2]
result[1] := 13;
result[2] := 10;

When doing this I did run into some strange errors when writing both BigTexts and normal Text variables to a stream – that is the reason for building everything into a BigText and writing once pr. line.

and last, but not least – a function to Download a file to the Client Tier and delete it from the Service Tier:

DownloadAndDeleteTempFile(ServerFileName : Text[1024]) : Text[1024]
IF NOT ISSERVICETIER THEN
EXIT(ServerFileName);

FileName := RBAutoMgt.DownloadTempFile(ServerFileName);
FILE.ERASE(ServerFileName);
EXIT(FileName);

It doesn’t take much more than that… (beside of course integrating this new method in the various functions in WordManagement). The fix doesn’t require anything else than just replacing codeunit 5054 and the new WordManagement can be downloaded here.

Question is now, whether there are localization issues with this. I tried changing all kinds of things on my machine and didn’t run into any problems – but if anybody out there does run into problems with this method – please let me know so.

What about backwards compatibility

So what if you install this codeunit into a system, where some of these merge files already have been created – and are indeed stored as HTML in blob fields?

Well – for that case, I created a function that was able to convert them – called

ConvertContentFromHTML(VAR MergeContent : BigText) : Boolean

It isn’t pretty – but it seems to work.

Feedback is welcome

I realize that by posting this, I am entering a domain where I am the newbie and a lot of other people are experts. I do welcome feedback on ways to do coding, things I can do better or things I could have done differently.

 

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

NAV 2009 and Unicode!

The title might be a bit misleading, but I am writing this post as a response to a problem, which a partner ran into with NAV 2009 – and the problem is caused by Unicode. I am not a Unicode expert, so bare with me if I am naming some things wrong.

As you know, NAV 2009 is a 3T architecture and the Service Tier is 95% managed code (only the lower pieces of the data stack is still unmanaged code). You probably also know, that managed code supports Unicode natively – in fact a string in C# or VB.Net is by default a Unicode string.

In C# you use byte[] if you need to work with binary data. My earlier post about transferring binary data between the Service Tier and Client Side COM or Web Service consumers (you can find it here) I read the file content into a byte[] and use a base64 encoding to encode this content into a string, which is transferable between platforms, code pages etc.

Is NAV 2009 then Unicode compliant?

No, we cannot claim that. There are a lot of things, that stops us from having a fully Unicode compliant product – but we are heading in that direction. Some of the things are:

  • The development environment and the classic client is not Unicode. When you write strings in AL code they are in OEM.
  • The import/export formats are OEM format.
  • I actually don’t know what the format inside SQL is, but I assume it isn’t Unicode (again because we have mixed platforms).

Lets take an example:

In a codeunit I write the following line:

txt := ‘ÆØÅ are three Danish letters, ß is used in German and Greek.’;

Now, I export this as a .txt file, and view this in my favorite DOS text viewer:

image

As you can see – the Ø has changed and a quick look at my codepage reveals that I am running 437 – the codepage that doesn’t have an Ø.

Opening the exported file in Notepad looks different:

Txt := ‘’î are three Danish letters, á is used in German and Greek.’;

Primary reason for this is, that Notepad assumes Unicode and the .txt file is OEM. When I launch the RoleTailored Client, and take a look at that line in the generated C# source file in Notepad:

txt = new NavText(1024, @”ÆØÅ are three Danish letters, ß is used in German and Greek.”);

Nice – and we know that Notepad is Unicode compliant and C# uses Unicode strings, so the AL -> C# compiler converts the string to Unicode – how does this look in my favorite DOS text viewer:

image

Clearly my string has been encoded.

But wait…

If NAV 2009 converts my text constants to Unicode – what if I write this string to a file using my well known file commands in AL? – well, let’s try.

I added the following code to a Codeunit and ran it from both the Role Tailored Client and from the Classic Client.

file.CREATETEMPFILE();
filename := file.NAME;
file.CLOSE();
file.CREATE(filename);
file.CREATEOUTSTREAM(OutStr);
OutStr.WRITETEXT(Txt);
file.CLOSE();

and don’t worry, the results are identical – both platforms actually writes the file in OEM format (we might have preferred Unicode, but for compatibility reasons this is probably a good thing).

Another thing we should try, is to call a managed COM object – and take a look at the string we get there – and again we get the same string from the Classic and from the Role Tailored Client – but in this case, we do NOT get the string in OEM format – we get a Unicode string.

MSXML

Now if we call an unmanaged COM object (like MSXML2 – XMLHTTP) we actually get the OEM string when invoked from the Classic Client and a Unicode string when invoked from the Role Tailored Client. Typically XMLHTTP is used with ASCII only – but in some cases, they do have binary data – which might be in the 128-255 character range.

Our problem now is, that our binary data (which didn’t have any relation to any special characters) gets converted to Unicode – and the Web Service provider doesn’t stand a chance to guess what we mean with the data we send.

The next problem is that the Role Tailored Client doesn’t support a byte[] type (binary) – in which we could have build a command and send it over. I tried a number of things, but didn’t find a way to send any binary data (above 128) to the Send command of XMLHTTP.

The third problem with XMLHTTP is that the only way we can get a result back is by reading the ResponseText – and that is treated as a Unicode string and gets crumbled before we get it into NAV.

Remember that these problems will not occur if the web service provider uses XML to transfer data back and forth – since XML is ASCII compliant.

My first proposal if you are having problems with a Web Service provider, using a binary communication is to query the provider and ask for an XML version of the gateway. If this isn’t possible – you have a couple of options (which both include writing a new COM object).

Create a proxy

You could create a Web Service proxy as a COM component (probably server side) and have a higher level function you call. This would remove the XMLHTTP glue code from NAV and put that into the COM object.

Example – you have a Web Service provider who can verify credit card numbers – and normally you would build up a string in AL and send this to the Send command – and then parse the ResponseStream you get back to figure out whether everything was A OK for not.

Create a function in a new COM object which might be named:

int CheckCreditCard(string CreditCardNo, string NameOnCard, int ExpireMonth, int ExpireYear, int SecurityCode)

Then your business logic in AL would just call and check – without a lot of XML parsing and stuff.

This would be my preferred choice – but it does involve some refactoring and a new COM object that needs to be installed on the server.

Use temporary files to transfer data

As mentioned earlier – writing a file in NAV 2009 with binary data, creates a file in OEM format – which would be the same binary content as we are typing in the AL editor.

So, you could create the string you want to send to XMLHTTP in a temporary file, create a new COM object which contains a function which sends the content of a file to a XMLHTTP object and writes the response back into the same file for NAV to read.

The idea here is that files (and byte[]) are binary data – strings are not.

The function in the COM object could look like:

public int SendFileContent(object NAVxmlhttp, string filename)
{
MSXML2.ServerXMLHTTP xmlHttp = NAVxmlhttp as MSXML2.ServerXMLHTTP;
FileStream fs = File.OpenRead(filename);
BinaryReader br = new BinaryReader(fs);
int len = (int)fs.Length;
byte[] buffer = new byte[len];
br.Read(buffer, 0, len);
br.Close();
fs.Close();

    xmlHttp.send(buffer);

    buffer = xmlHttp.responseBody as byte[];
fs = File.Create(filename);
BinaryWriter bw = new BinaryWriter(fs);
bw.Write(buffer);
bw.Close();
fs.Close();

    return xmlHttp.status;
}

If you went for this approach your AL code would change from:

XmlHttp.Send(Txt);

(followed by opening the ResponseStream) to

file.CREATETEMPFILE();
filename := file.NAME;
file.CLOSE();
file.CREATE(filename);
file.CREATEOUTSTREAM(OutStr);
OutStr.WRITETEXT(Txt);
file.CLOSE();

myCOMobject.SendFileContent(XmlHttp, filename);

(followed by opening the file <filename> again – reading the result).

The second approach would also work from the Classic client – so you don’t have to use IF ISSERVICETIER THEN to do the one of the other.

In the Edit In Excel – Part 4, you can see how to create a COM object – should be pretty straightforward.

Build a binary string that doesn’t get encoded

You could create a function in a COM object, which returns a character based on a numeric value:

public string GetChar(int ch)
{
return “”+(char)ch;
}

Problem with this direction is, that this function should ONLY be used when running in the Role Tailored Client.

Calling this function from the Classic Client will case this string to be seen as Unicode and converted back into OEM – and that really wouldn’t make sense at all.

Convert to/from Unicode using strings instead of files

So what if the data you need to send is confidential and you cannot write that to a file?

Well – you can create a function, which converts the string back to OEM (making it the same binary image) – send it over the wire – and then convert the response to UniCode (so that when the string comes into NAV – it will be converted back to OEM again again).

Seems like a lot of conversion back and forth – but it would actually work from both the Classic and the Role Tailored Client, the code for that goes here:

public class MyCOMobject : IMyCOMobject
{
private static Byte[] oem2AnsiTable;
private static Byte[] ansi2OemTable;

    /// <summary>
/// Initialize COM object
/// </summary>
public MyCOMobject()
{
oem2AnsiTable = new Byte[256];
ansi2OemTable = new Byte[256];
for (Int32 i = 0; i < 256; i++)
{
oem2AnsiTable[i] = (Byte)i;
ansi2OemTable[i] = (Byte)i;
}
NativeMethods.OemToCharBuff(oem2AnsiTable, oem2AnsiTable, oem2AnsiTable.Length);
NativeMethods.CharToOemBuff(ansi2OemTable, ansi2OemTable, ansi2OemTable.Length);
// Remove “holes” in the convertion structure
Int32 ch1 = 255;
Int32 ch2 = 255;
for (;; ch1–, ch2–)
{
while (ansi2OemTable[oem2AnsiTable[ch1]] == ch1)
{
if (ch1 == 0)
break;
else
ch1–;
}
while (oem2AnsiTable[ansi2OemTable[ch2]] == ch2)
{
if (ch2 == 0)
break;
else
ch2–;
}
if (ch1 == 0)
break;
oem2AnsiTable[ch1] = (Byte)ch2;
ansi2OemTable[ch2] = (Byte)ch1;
}
}

    /// <summary>
/// Convert Unicode string to OEM string
/// </summary>
/// <param name=”str”>Unicode string</param>
/// <returns>OEM string</returns>
private byte[] UnicodeToOem(string str)
{
Byte[] buffer = Encoding.Default.GetBytes(str);
for (Int32 i = 0; i < buffer.Length; i++)
{
buffer[i] = ansi2OemTable[buffer[i]];
}
return buffer;
}

    /// <summary>
/// Convert OEM string to Unicode string
/// </summary>
/// <param name=”oem”>OEM string</param>
/// <returns>Unicode string</returns>
private string OemToUnicode(byte[] oem)
{
for (Int32 i = 0; i < oem.Length; i++)
{
oem[i] = oem2AnsiTable[oem[i]];
}
return Encoding.Default.GetString(oem);
}

    /// <summary>
/// Send data through XMLHTTP
/// </summary>
/// <param name=”NAVxmlhttp”>XmlHttp object</param>
/// <param name=”data”>string containing data (in Unicode)</param>
/// <returns>The response from the XMLHTTP Send</returns>
public string Send(object NAVxmlhttp, string data)
{
MSXML2.ServerXMLHTTP xmlHttp = NAVxmlhttp as MSXML2.ServerXMLHTTP;
byte[] oem = UnicodeToOem(data);
xmlHttp.send(oem);
        return OemToUnicode((byte[])xmlHttp.responseBody);
}
}

internal static partial class NativeMethods
{
#region Windows OemToChar/CharToOem imports

    [DllImport(“user32”, EntryPoint = “OemToCharBuffA”)]
internal static extern Int32 OemToCharBuff(Byte[] source, Byte[] dest, Int32 bytesize);

    [DllImport(“user32”, EntryPoint = “CharToOemBuffA”)]
internal static extern Int32 CharToOemBuff(Byte[] source, Byte[] dest, Int32 bytesize);

    #endregion
}

Unfortunately I have not found a way to do this without having a COM object in play.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Transferring binary data to/from WebServices (and to/from COM (Automation) objects)

A number of people have asked for guidance on how to transfer data to/from COM and WebServices in NAV 2009.

In the following I will go through how to get and set a picture on an item in NAV through a Web Service Connection.

During this scenario we will run into a number of obstacles – and I will describe how to get around these.

First of all – we want to create a Codeunit, which needs to be exposed to WebServices. Our Codeunit will contain 2 functions: GetItemPicture and SetItemPicture – but what is the data type of the Picture and how do we return that value from a WebService function?

The only data type (supported by Web Services) that can hold a picture is the BigText data type.

We need to create the following two functions:

GetItemPicture(No : Code[20];VAR Picture : BigText)
SetItemPicture(No : Code[20]; Picture : BigText);

BigText is capable if holding binary data (including null terminals) up to any size. On the WSDL side these functions will have the following signature:

image

As you can see BigText becomes string – and strings in .net are capable of any size and any content.

The next problem we will face is, that pictures typically contains all kinds of characters, and unfortunately when transferring strings to/from WebServices there are a number of special characters that are not handled in the WebServices protocol.

(Now you wonder whether you can have <> in your text – but that isn’t the problem:-)

The problem is LF, CR, NULL and other characters like that.

So we need to base64 (or something like that) encode our picture when returning it from Web Services. Unfortunately I couldn’t find any out-of-the-box COM objects that was able to do base64 encoding and decoding – but we can of course make one ourselves.

Lets assume for a second that we have a base64 COM object – then this would be our functions in AL:

GetItemPicture(No : Code[20];VAR Picture : BigText)
CLEAR(Picture);
Item.SETRANGE(Item.”No.”, No, No);
IF (Item.FINDFIRST()) THEN
BEGIN
  Item.CALCFIELDS(Item.Picture);
// Get Temp FileName
TempFile.CREATETEMPFILE;
FileName := TempFile.NAME;
TempFile.CLOSE;

  // Export picture to Temp File
Item.Picture.EXPORT(FileName);

  // Get a base64 encoded picture into a string
CREATE(base64);
Picture.ADDTEXT(base64.encodeFromFile(FileName));

  // Erase Temp File
FILE.ERASE(FileName);
END;

SetItemPicture(No : Code[20];Picture : BigText)
Item.SETRANGE(Item.”No.”, No, No);
IF (Item.FINDFIRST()) THEN
BEGIN
// Get Temp FileName
TempFile.CREATETEMPFILE;
FileName := TempFile.NAME;
TempFile.CLOSE;

  // Decode the bas64 encoded image into the Temp File
CREATE(base64);
base64.decodeToFile(Picture, FileName);

  // Import picture from Temp File
Item.Picture.IMPORT(FileName);
Item.Modify();
// Erase Temp File
FILE.ERASE(FileName);
END;

A couple of comments to the source:

  • The way we get a temporary filename in NAV2009 is by creating a temporary file, reading its name and closing it. CREATETEMPFILE will always create new GUID based temporary file names – and the Service Tier will not have access to write files in e.g. the C:\ root folder and a lot of other places.
  • The base64 automation object is loaded on the Service Tier (else it should be CREATE(base64, TRUE, TRUE);) and this is the right location, since the exported file we just stored is located on the Service Tier.
  • The base64.encodeFromFile reads the file and returns a very large string which is the picture base64 encoded.
  • The ADDTEXT method is capable of adding these very large strings and add them to a BigText (BTW – that will NOT work in the classic client).
  • We do the cleanup afterwards – environmental protection:-)

So, why does the ADDTEXT support large strings?

As you probably know, the ADDTEXT takes a TEXT and a position as parameter – and a TEXT doesn’t allow large strings, but what happens here is, that TEXT in C# becomes string – and the length-checking of TEXT variables are done when assigning variables or transferring parameters to functions and the ADDTEXT doesn’t check for any specific length (which comes in handy in our case).

The two lines in question in C# looks like:

base64.Create(DataError.ThrowError);
picture.Value = NavBigText.ALAddText(picture.Value, base64.InvokeMethod(@”encodeFromFile”, fileName));

Note also that the base64.decodeToFile function gets a BigText directly as parameter. As you will see, that function just takes an object as a parameter – and you can transfer whatever to that function (BigText, Text, Code etc.). You actually also could give the function a decimal variable in which case the function would throw an exception (str as string would return NULL).

So now you also know how to transfer large strings to and from COM objects:

  1. To the COM object, you just transfer a BigText variable directly to an object parameter and cast it to a string.
  2. From the COM object to add the string return value to a BigText using ADDTEXT.
  3. You cannot use BigText as parameter to a by-ref (VAR) parameter in COM.

In my WebService consumer project I use the following code to test my WebService:

// Initialize Service
CodeUnitPicture service = new CodeUnitPicture();
service.UseDefaultCredentials = true;

// Set the Image for Item 1100
service.SetItemPicture(“1100″, encodeFromFile(@”c:\MandalayBay.jpg”));

// Get and show the Image for Item 1001
string p = “”;
service.GetItemPicture(“1001″, ref p);
decodeToFile(p, @”c:\pic.jpg”);
System.Diagnostics.Process.Start(@”c:\pic.jpg”);

and BTW – the source code for the two functions in the base64 COM object are here:

public string encodeFromFile(string filename)
{
FileStream fs = File.OpenRead(filename);
BinaryReader br = new BinaryReader(fs);
int len = (int)fs.Length;
byte[] buffer = new byte[len];
br.Read(buffer, 0, len);
br.Close();
fs.Close();
return System.Convert.ToBase64String(buffer);
}

public void decodeToFile(object str, string filename)
{
FileStream fs = File.Create(filename);
BinaryWriter bw = new BinaryWriter(fs);
bw.Write(Convert.FromBase64String(str as string));
bw.Close();
fs.Close();
}

If you whish to download and try it out for yourself – you can download the sources here:

The two Visual Studio solutions can be downloaded from http://www.freddy.dk/VSDemo.zip (the base64 COM object and the VSDemo test project)

The NAV codeunit with the two functions above can be downloaded from http://www.freddy.dk/VSDemoObjects.fob.

Remember that after importing the CodeUnit you would have to expose it as a WebService in the WebService table:

image

And…. – remember to start the Web Service listener (if you are running with an unchanged Demo installation).

The code shown in this post comes with no warranty – and is only intended for showing how to do things. The code can be reused, changed and incorporated in any project without any further notice.

Comments or questions are welcome.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV