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