Directions US 2009

Directions US 2009 is over – running from 11/10 – 11/14 with a serious of deep dive sessions on Saturday. The entire event was professionally executed and the quality of everything was just great. A huge congratulations to the comity on this event, a huge congratulations to the partner community for having these events and a huge thank you from Microsoft for inviting us to be part of this.

I had the pleasure of hosting 3 sessions – one on “What’s new in NAV 2009SP1 for the developer” and 2 deep dive sessions on Web Services as the 2 final sessions on Saturday, and we actually had a pretty good number of people staying for these very last sessions.

So how did it go?

I liked the session survey’s! They had a question called: “Do I want to see this speaker again next year?” – which instead of trying to analyze other responses gives a direct indication of whether I will plan a trip to San Diego next year – I actually don’t know whether or not we will get the survey results…

If I where to grade my own performance, I would probably give me a B- for the What’s new session and an A for my deep dive sessions. When starting the What’s new session, I had a section prepared about what’s new for the user, but the majority of this was already shown in the Keynote. I decided to ask how many people attended the keynote on Tuesday – and then quickly go over the demo for the people that did NOT attend the keynote, so I did…

“How many people did NOT attend the Keynote?”

was the question – and only one single person raised his hand… (which actually was my own Manager in Microsoft – so I knew that shouldn’t do a what’s new for the user for him alone). This totally took me by surprise me and I ended up finalizing my session 10 minutes early.

The deep dive sessions went better – people where active asking questions and understanding things and I had absolutely no problem filling out 2 hours talking about Web Services – without just repeating things that everybody have seen before. The majority of the audience knew about Web Services and had also tried using Web Services before – that was very good.

I wanted to do a recording of the session and make it available here – that did not work for me. Instead I said that I would post info about all the different areas covered in the session on my blog (BTW – I was happy to see, that the majority of the audience knew my blog). The things covered in my session, which you will see as posts on my blog are:

  • Codeunit web services and data types
  • Page web services and filters
  • Extending page web services
  • Using XMLports to read and write data
  • What Company to use
  • Using NAV Web Services from
    • PHP
    • Java
    • C#
    • VB
    • VBScript
    • Javascript
    • Silverlight
    • Mobile
    • C/AL
  • Edit In Excel R2 (I actually promised that long time ago)
  • Synchronize A/D users to NAV

In the session I only had samples in PHP and Java – I will write up a end-to-end scenario and implement this in all these languages/platforms so that people can overcome the initial connection/interaction problems when using NAV Web Services.

A lot of things to do – so I better get going…

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Timer events on a page

Have you ever wanted to have an event raised every 10th second on a page in the RoleTailored Client?

Wait no more – here is how you can do just that in Microsoft Dynamics NAV 2009SP1.

A Timer control is a Non-Visual Add-In

I have seen a number of development platforms treat a Timer as a Non-Visual Add-In (including .net) – so I thought I would try to create a non-visual Add-In for NAV – and what better than create the Timer. A Timer should not be visible to the user, but it should be able to raise events.

There are different ways to create a Non-Visual control, but the most obvious method will not work.

Adding a control and setting Visible to FALSE – will cause the control to be optimized away – it will never be created.

You can however create a Non-Visual control in other ways:

  • Set the visible property to a global variable, which is false.
  • Set the size (and MinSize, MaxSize) of the Control to 0, 0.

The first approach would require you to add a variable called something like falsevar on each page you use the Timer Control – and that isn’t really what we want – so I will use the second approach.

Well – then everything seems pretty simple – right?

Yes and No.

It is very simple to create a non-visual control which instantiates a timer and fires events – Yes, but what if the service tier opens up a modal dialog (like a CONFIRM command) – then I would suggest that we do NOT keep firing events.

For this purpose our control needs to subscribe to two application level events.

Application.EnterThreadModal

Application.LeaveThreadModal

What is my Application? Well, that is of course the RoleTailored Client. Your WinForms Control gets created as a first class citizen in the RoleTailored Client and of course you have access to the Application events as well. In fact there are all kinds of things you can do and all kinds of things you shouldn’t do.

Always bare in mind that if you start to go outside the control itself – think whether this is necessary, think future compatibility if the RoleTailored Client changes various things and remember to clean up.

For the two events above – they are pretty clear – EnterThreadModal is fired when the application enters Modal state and LeaveThreadModal is fired when the application leaves the modal state.

Remember to clean up – your mother isn’t here!

When coding in .net you often don’t need to consider cleaning up – the garbage collector will come and clean everything up. Now that isn’t always true.

In the case of the Application Level events – when you subscribe to an event, you actually give the Application object a pointer to your object – telling it to call you whenever something happens. This in fact means that the garbage collector is not allowed to cleanup anymore – it doesn’t matter that the page is closed, your control is gone – the Application object still maintains a reference to your object and therefore it will stay.

Of course this doesn’t apply when you subscribe to events in your own control, since the object holding the reference to your object goes out of scope at the same time as yourself.

Hmmm – admitted – I am probably getting too nerdy now – but it is rather important to understand this in order to avoid memory leaks and these memory leaks will affect the RoleTailored Client – not only your Add-In.

Instead of going further into detail – the curious read can read much more about garbage collection on msdn: Garbage Collector Basics and Performance Hints.

Let’s look at the code

The way I have implemented the Timer control is like this

[ControlAddInExport(“FreddyK.TimerControl”)]
public class TimerControl : StringControlAddInBase, IStringControlAddInDefinition
{
EventHandler EnterThreadModal;
EventHandler LeaveThreadModal;
Timer timer = null;
int interval = 0;
int count = 0;

    /// <summary>
/// Constructor – Setup timer and Application event subscriptions
/// </summary>
public TimerControl()
{
EnterThreadModal = new EventHandler(Application_EnterThreadModal);
LeaveThreadModal = new EventHandler(Application_LeaveThreadModal);
Application.EnterThreadModal += EnterThreadModal;
Application.LeaveThreadModal += LeaveThreadModal;
timer = new Timer();
timer.Tick += new EventHandler(timer_Tick);
}

    /// <summary>
/// Dispose method – cleanup timer and Application event subscriptions
/// </summary>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
Application.EnterThreadModal -= EnterThreadModal;
Application.LeaveThreadModal -= LeaveThreadModal;
if (timer != null)
{
timer.Stop();
timer.Dispose();
timer = null;
}
}
}

    /// <summary>
/// Event handler for Application.EnterThreadModal
/// </summary>
void Application_EnterThreadModal(object sender, EventArgs e)
{
timer.Stop();
}

    /// <summary>
/// Event handler for Application.LeaveThreadModal
/// </summary>
void Application_LeaveThreadModal(object sender, EventArgs e)
{
if (timer.Interval != 0)
timer.Start();
}

    /// <summary>
/// Create the native Add-In Control
/// </summary>
protected override Control CreateControl()
{
// Create a panel with the size 0,0
Panel panel = new Panel();
panel.BorderStyle = BorderStyle.None;
panel.MinimumSize = new Size(0, 0);
panel.MaximumSize = new Size(0, 0);
panel.Size = new Size(0, 0);
return panel;
}
/// <summary>
/// Timer tick handler – raise the Service Tier Add-In Event
/// </summary>
void timer_Tick(object sender, EventArgs e)
{
// Stop the timer while running the add-in Event
timer.Stop();
// Invoke event
this.RaiseControlAddInEvent(this.count++, “”);
// Restart the timer
timer.Start();
}

    /// <summary>
/// Override to specify that Caption should be omitted
/// </summary>
public override bool AllowCaptionControl
{
get
{
return false;
}
}

    /// <summary>
/// Override to specify that value has not changed
/// </summary>
public override bool HasValueChanged
{
get
{
return false;
}
}

    /// <summary>
/// Value for the Timer Control – the value is the number of 1/10’s of a second between Tick events
/// NOTE: every event is sent from the Client to the Service Tier – meaning that this is not intended
///       for events executing more frequently than 1/10’s of a second
/// </summary>
public override string Value
{
get
{
return base.Value;
}
set
{
base.Value = value;
if (!int.TryParse(value, out interval))
{
interval = 0;
}
interval = interval * 100;
if (timer != null && timer.Interval != interval)
{
timer.Interval = interval;
count = 0;
if (interval == 0)
timer.Stop();
else
timer.Start();
            }
}
}
}

 

A couple of things to note

  • The Value is set on the Control even it doesn’t seem necessary – that is the reason for checking whether the interval has changed before doing anything.
  • We don’t really use the native control, the Panel(0,0), for anything – it is only there for the RoleTailored Client to have something to hold on to – returning null causes the RoleTailored Client to display an Add-In error.
  • I stop the timer while running the server side event. The primary reason for this is to ensure we don’t get multiple events triggered simultaneously and this causes the interval time to be applied after the event returns – not from the time the event started.
  • If you setup the Timer to trigger an event every 10 seconds – it will do so when there has been 10 seconds without any modal dialogs. If this isn’t what you want, you should setup the trigger to fire every second and look when the Add-In event Index parameter is 10.

How to use the Control

For a test, we create a sample page like this:

image

with the following global variables:

image

and the following triggers:

OnOpenPage()
timer := ’10’;

timer – OnControlAddIn(Index : Integer;Data : Text[1024])
count := Index;

 

As you can see, the timer is set to trigger once a second and the Index in the AddIn event actually counts the number of times the trigger has been fired, so the count will be counting.

Now you might wonder – why is the Timer caption Timer – DO NOT REMOVE?

The reason for this is, that the RoleTailored Client doesn’t really know about the concept Non-Visual controls and as you probably know, personalization can remove everything from a page – including your timer:

image

If you remove this control – the Timer will of course stop.

You can find the Visual Studio project and the TimerTest.fob here.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Using touchscreens with the RoleTailored Client

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

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

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

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

image

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

Button Panels

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

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

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

Escaping from Javascript

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

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

Show me the code!!!

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

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

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

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

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

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

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

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

and now the Add-In part of the Control.

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

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

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

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

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

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

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

Things to note:

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

The MyScriptManager could look like this:

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

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

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

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

window.external.clickevent(i, s);

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

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

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

Let’s put the BrowserControl to work for us

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

image

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

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

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

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

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

this should give us the following page when running:

image

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Running Code on the Client without deploying COM objects

Yes, it can be done!

No, it isn’t .net code – nor AL code.

Why?

It started out as me being a little too fast when stating that you could easily download a file to the Client and attach it to Outlook without any user interaction – and as you might know that is true, but you might also know that if you go the recommended route:

FILE.DOWNLOAD(FileName, ”, ‘<TEMP>’,”, ToFile);
Mail.NewMessage(”,”,Name,”,ToFile,TRUE);
FILE.ERASE(FileName);

Then you will get an e-mail that looks like this:

image

People might not assume that this actually is Invoice no. 10103 in PDF format. What you of course want to have is:

image

So, how do we get there.

I actually did respond to a post on mibuso a while back (http://www.mibuso.com/forum/viewtopic.php?f=32&t=29806) about how this could be done, but that would involve a COM object deployed to all clients and not everybody wants that (although I have posted a method on how to do this automatically).

The problem here is, that file.download always places the file in a temporary directory with a temporary filename – and there is (to my knowledge) no other way to copy a file to the Client.

Assuming that this is correct, how do we then rename a file on the client without having to deploy COM objects?

I said without deploying COM objects, not without USING COM objects

As you know, we can run COM objects on the server or on the Client and one of the COM objects, which ships with Windows can come in handy here. The Windows Script Host – if we instantiate this COM object we can actually give the component a VB Script to execute in the context of the COM component (which would be either on the Server or on the Client).

Windows Script Host

Yes, WSH is legacy – but it is widely used and it is included on all Windows versions from Windows XP and up. I am not going to make this a tutorial on VBScript and WSH – for that you can find a number of good posts on the internet – or start by reading msdn

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

Creating a script function / method

The method AddCode on the Windows Script Host COM object is used to add sourcecode to the component.

Note, that you need to add an entire function / method in one call and note, that each line needs to be terminated by a CR.

You also need to specify what language you use, the control supports JScript and VBScript.

A VBScript function which returns Hello <name> could look like this:

function Hello(who)
Hello = “Hello “&who
end function

Creating this function in a Client side COM component could look like:

IF CREATE(objScript,TRUE,TRUE) THEN
BEGIN
CR := ‘ ‘; CR[1] := 13;
objScript.Language := ‘VBScript’;
objScript.AddCode(
‘function Hello(who)’+CR+
‘  Hello = “Hello “&who’+CR+
‘end function’);
END;

The way I write this is, that I try to maintain the structure of the VBScript even though it is inside a string in NAV, maybe I am fooling myself, but I think it is more readable.

Invoking a script function / method

There are two ways of invoking a script method:

Eval – used to invoke a function, and get a return value back.

The above function could be called using

MESSAGE(FORMAT(objScript.Eval(‘Hello(“Freddy”)’)));

Note – when calling functions, VBScript wants your parameters embraced by parentheses.

ExecuteStatement – used to invoke a method which doesn’t return anything

Let’s rewrite the above function to a method and have the method show a MessageBox:

The VBScript could look like:

sub Hello(who)
MsgBox “Hello “&who, 0, “Title”
end sub

and creating this function in a COM object and calling the method could look like:

IF CREATE(objScript,TRUE,TRUE) THEN
BEGIN
CR := ‘ ‘; CR[1] := 13;
objScript.Language := ‘VBScript’;
objScript.AddCode(
‘sub Hello(who)’+CR+
‘  MsgBox “Hello “&who, 0, “Test”‘+CR+
‘end sub’);
objScript.ExecuteStatement(‘Hello “Freddy”‘);
END;

Note – when calling methods (or sub’s) VBScript does NOT want the parameters embraced by parentheses.

Some sample scripts

Rename a temporary file

function RenameTempFile(fromFile, toFile)
set fso = createobject(“Scripting.FileSystemObject”)
set x = createobject(“Scriptlet.TypeLib”)
path = fso.getparentfoldername(fromFile)
toPath = path+””+left(x.GUID,38)
fso.CreateFolder toPath
fso.MoveFile fromFile, toPath+””+toFile
RenameTempFile = toPath
end function

As you can see, I am doing exactly what I responded on the mibuso thread here – just in VBScript instead – which then requires no client side install.

BTW this function is actually used in ClausL’s post about sending e-mail with PDF attachments, which proves that we do talk with our colleagues at Microsoft:-). Note that there is no good way of creating a GUID from VBScript – I (mis)use the fact that every instance of Scriptlet.TypeLib gets assigned a new GUID.

Get Machine name

function GetComputerName()
set net = createobject(“wscript.network”)
GetComputerName = net.ComputerName
end function

I know, that you also can read an environment variable – but this way you can actually get all kind of information on the network though this.

Launch an application

sub Notepad()
set shell = createobject(“WScript.Shell”)
shell.Run “notepad.exe”
end sub

Yes, you can do this by using the Shell object directly in NAV, like:

Shell       Automation       ‘Microsoft Shell Controls And Automation’.Shell

CREATE(objShell,True,true);
objShell.Open(‘c:\windows\system32\notepad.exe’);

I just wanted to show that you that stuff like this can be done in VBScript too, and note, that the Shell object in VBScript and in NAV is not the same.

Asking a simple question

function Input(question, title, default_answer)
Input = InputBox(question, title, default_answer)
end function

A couple of partners have told me, that they are unhappy with the discontinuation of INPUT from NAV and having to create pages for even the simplest questions. Running the following code:

IF CREATE(objScript,TRUE,TRUE) THEN
BEGIN
CR := ‘ ‘; CR[1] := 13;
objScript.Language := ‘VBScript’;

  objScript.AddCode(
‘function Input(question, title, default_answer)’+CR+
‘  Input = InputBox(question, title, default_answer)’+CR+
‘end function’);

  s := objScript.Eval(‘Input(“How old are you?”, “A simple question”, “”)’);
MESSAGE(s);
END;

Brings up this dialog on my machine:

image

Who knows, maybe somebody can use this as an alternative to INPUT.

Read the RoleTailored Client configuration file

function ReadConfigFile()
set shell = CreateObject(“WScript.Shell”)
folder = shell.ExpandEnvironmentStrings(“%LOCALAPPDATA%”)
if folder = “” then folder = shell.ExpandEnvironmentStrings(“%USERPROFILE%”)&”Local SettingsApplication Data”
  filename = folder&”MicrosoftMicrosoft Dynamics NAVClientUserSettings.config”
set fso = createobject(“Scripting.FileSystemObject”)
set file = fso.OpenTextFile(filename, 1)
ReadConfigFile = file.ReadAll()
end function

Note that I have NOT tested this function under Windows XP – I know that LOCALAPPDATA is not defined on Windows XP and I think the line:

  if folder = “” then folder = shell.ExpandEnvironmentStrings(“%USERPROFILE%”)&”Local SettingsApplication Data”

should take care of finding the right folder – if anybody can confirm that, then add that as a comment to this post.

Bringing up a MESSAGE with the outcome of this function on my machine gives me this dialog:

image

I don’t know whether that could come in handy, but maybe it can spawn off some good ideas.

Wrapping up

As you can see, you can do a lot of things in VB Script on the Client (or on the Server). There are a number of scripts you can find on the internet to work with the A/D (create, delete and enumerate users).

Of course there a limitations as to what you can do in VBScript and it isn’t a real alternative to writing a COM component, but for something it is easy and straightforward – and it doesn’t require any client side installation of components and this works in both Classic and RTC.

You can download the rename function from ClausL’s post about sending e-mail with PDF attachments. You will need to do copy, paste and maybe modify the other samples in order to use them.

 

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

Microsoft Dynamics NAV 2009 SP1 launched!

I guess it is a little late to call this news, but nevertheless – on September 1st NAV 2009 SP1 launched.

SP1 is big release for the user – the majority of the feedback we got from users on NAV 2009 was taken into consideration and I truly believe that we will see hard core classic client users shift and prefer the Role Tailored Client with these changes. Yes, they will have to get used to new ways of doing things – but I think we closed the gap on the pieces we missed out on in NAV 2009.

All in all NAV 2009 SP1 just works more intuitively and it adds a feature called Client Extensibility.

Client Extensibility is the ability to add custom controls to NAV and I am sure you will see a lot of blogs on this topic in the future (my first was done back in June based on NAV 2009 SP1 CTP2 – can be found here)

Over the next weeks I will post updates to a number of my previous posts – what it takes to make them run under SP1.

One thing I have heard from a number of ISV’s and partners is, that they are trying to minimize the number of Client side components, that could be COM components or Add-Ins (Client Extensibility Controls), and the very first thing I want to blog about, is a method to overcome this hurdle. A way to auto-deploy Client side components without having to run around and install anything on all clients.

Stay tuned

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Auto deployment of Client Side Components

NOTE: There is an updated post on Auto deployment of Client Side Components here.

When you install the RoleTailored Client on a number of clients, you might need to install a number of Client side components as well. This might not sound as too much of a problem when you need to install the client anyway – but lets say you install an ISV Add-on with a live customer, who already have 100 clients install – and now you need to install the objects to the database – AND you need to run to 100 computers and install Client side components.

Yes, you can do this with system management policies, but not all customers are running SMS and it would just be way easier if everything could be handled from the ISV Add-On and the Client Components could be auto deployed.

When doing this – it is still important, that IF the customer is running SMS and decide to deploy the Client Side components through system policies – then the auto deployment should just pick this up and accept that things are Ok.

Two kinds of Client Side Components

NAV 2009 SP1 supports Add-Ins (Client Extensibility Controls) and Client side COM components (as NAV 2009) and the way these components are installed is very different.

Add-Ins needs to be placed in the Add-Ins folder under the RoleTailored Client folder on the Client and COM components can be installed wherever on the Client, but needs to registered in the registry with regasm.

Both Add-Ins and COM components might rely on other client side components, so it is important that we don’t just create a way of copying files to the Client – but we should instead create a way of launching a setup program on the client, which then installs the components. In my samples, I have one Setup program for every component, but an ISV could easily package all components together in one installation program and install them all in one go.

To install a client side component really isn’t that difficult – use FILE.DOWNLOAD with an MSI and that’s it. But how do we detect whether or not the component is installed already?

We cannot keep a list on the server side, since the computer might get re-installed or restored – we need a way of discovering whether a component is installed.

Detecting whether a Client side COM component is installed

I will start with the COM component (since it will take a COM component to check whether an Add-In is installed). The COM component needs a CREATE statement to be initialized and if you check the return value of the CREATE statement – you know whether or not the COM component is executable. If not we launch a FILE.DOWNLOAD

IF NOT CREATE(mycomponent, TRUE, TRUE) THEN
FILE.DOWNLOAD(mycomponentinstaller);

Almost too simple right?

Now – I know that some people will say – well, what if I have an updated version of the COM component and it needs to be deployed?

My answer to that would be to change the COM signature, in effect making it a different COM component and allow them to be installed side-by-side. This would in effect mean that you might have multiple versions of COM components installed on a client, but they typically don’t take up a lot of space, and they don’t run if nobody uses them.

You could also create a function for checking the version number of the component like:

IF NOT CREATE(mycomponent, TRUE, TRUE) THEN
FILE.DOWNLOAD(mycomponentinstaller)
ELSE IF NOT mycomponent.CheckVersion(100) THEN
FILE.DOWNLOAD(mycomponentinstaller);

problem with this approach is, that NAV keeps a lock on the Client side component (event if you CLEAR(mycomponent)) due to performance reasons and your mycomponentinstaller will have to close the NAV client in order to update the component.

I like the solution better, where you just create a new GUID and thus a new component – so that is what I will describe here.

Detecting whether an Add-In is installed on the Client

If you have installed the Server pieces of the Virtual Earth Integration (look here), but have a Client without the VEControl Add-In, this is how the FactBox will look:

image

Not very informative when you were expecting this:

image

But as you might know, we actually didn’t write any code to plugin the control and the Control Add-In error above is handled by the Client without actually notifying the Service Tier that anything is missing.

What we need to do, is to create one line of code in the INIT trigger of all pages, which uses an Add-In:

ComponentHelper.CheckAddInNameKey(‘FreddyK.LargeVEControl’,’1c9f7ad47dba024b’);

and then of course create a function that actually checks that the Add-In is there and does a FILE.DOWNLOAD(addininstaller) if it isn’t.

Problem here is that we need a COM component in order to check the existence of an Add-In, and this COM component will have to run Client side (how else could it inspect the Add-ins folder – doh).

The INIT trigger is executed before anything is sent off to the Client and thus we can install the component and continue opening the page after we have done that. BTW the FILE.DOWNLOAD is NOT going to wait until the user actually finishes the setup program, so we will have to bring up a modal dialog telling the user to confirm that he has completed the setup.

BTW as you probably have figured out by now, the above line requires a registration of Add-Ins like:

ComponentHelper.RegisterAddIn(‘FreddyK.LargeVEControl’,’1c9f7ad47dba024b’,’NAV Large Virtual Earth Control’, ‘NavVEControl.msi’);

In order to specify what file to download. Now I could have added this to the Check function to avoid a table – but I actually don’t think it belongs there.

The ComponentHelper

So, what I have done is to collect some functionality that I find I use all the time in various samples in a Component called the ComponentHelper.

The functions are:

  1. Installation of Client side COM components (used by the majority of samples)
  2. Installation of Client side Add-Ins (used by all samples with Add-ins)
  3. Ability to Escape and Unescape strings (the method Web Services uses for encoding of company name – used in the Virtual Earth Integration)
  4. Ability to register a codeunit or page as Web Service from code (used by all samples using Web Services)
  5. Global information about the URL to my IIS and Web Service tier (used in Edit In Excel and Virtual Earth Integration)
  6. Modify metadata programmatically (all samples)

In fact I am hoping that these basic pieces of functionality will find their way into the base product in the future, where they IMO belong.

Installation of Client side COM components

Every time you use a self built COM component (in this case the NAVAddInHelper), which you want to auto-deploy, you should create a function like this:

LoadAddInHelper(VAR NAVAddInHelper : Automation “‘NAVAddInHelper’.NAVAddInHelper”) Ok : Boolean
Ok := FALSE;
WHILE NOT CREATE(NAVAddInHelper,TRUE,TRUE) DO
BEGIN
IF NOT AskAndInstallCOMComponent(‘NAV AddIn Helper’, ‘NAVAddInHelper.msi’) THEN
EXIT;
END;
Ok := TRUE;

and always invoke this when you want to create an instance of the Component (instead of having CREATE(NAVAddInHelper,TRUE,TRUE) scattered around the code.

AskAndInstallCOMComponent(Description : Text[80];InstallableName : Text[80]) Retry : Boolean
Retry := FALSE;
IF CONFIRM(STRSUBSTNO(TXT_InstallCOMComponent, Description)) THEN
BEGIN
Retry := InstallComponent(InstallableName);
END;

InstallComponent(InstallableName : Text[80]) Retry : Boolean
Retry := TRUE;
toFile := InstallableName;
fromFile := APPLICATIONPATH + ‘ClientSetup’+InstallableName;
IF NOT FILE.EXISTS(fromFile) THEN
BEGIN
fromFile := APPLICATIONPATH + ‘..ClientSetup’+InstallableName;
END;
IF FILE.DOWNLOAD(fromFile, InstallableName, ”, ”, toFile) THEN
BEGIN
Retry := CONFIRM(TXT_PleaseConfirmComplete);
END;

as you can see from the code, the function will try to create the component until it succeeds or the user says No, I do not want to install the component. At this time I would like to mention a small bug in NAV 2009 SP1 – when you try to CREATE a COM component client side and it isn’t there, the Client will still ask you whether or not you want to run a client side component, but since the Control isn’t installed – it doesn’t know what to call it, meaning that you will get:

image

Now it is OK for the user to cancel this window because he doesn’t know what it is, but if he says Never Allow (silly choice to give the user:-)), he will have to delete personalization settings for automation objects to get this working again.

image

BTW If the user declines running a COM component – our code will see this as the component is not installed and ask him to install it.

Installation of Client side Add-Ins

To check whether an Add-in is installed, we first check whether it is registered in the Client’s add-in table.

CheckAddInNameKey(AddInName : Text[220];PublicKeyToken : Text[20]) Found : Boolean
Found := FALSE;
IF NOT AddIn.GET(AddInName,PublicKeyToken) THEN
BEGIN
MESSAGE(STRSUBSTNO(TXT_AddInNotRegisterd, AddInName, PublicKeyToken));
EXIT;
END;
Found := CheckAddIn(AddIn.”Control Add-in Name”, AddIn.”Public Key Token”, AddIn.Description);

Without anything here – nothing works. After this we check our own table (in which we have information about what executable to download to the client)

CheckAddIn(AddInName : Text[220];PublicKeyToken : Text[20];Description : Text[250]) Found : Boolean
IF Description = ” THEN
BEGIN
Description := AddInName;
END;
Found := FALSE;
IF LoadAddInHelper(NAVAddInHelper) THEN
BEGIN
WHILE NOT NAVAddInHelper.CheckAddIn(AddInName, PublicKeyToken) DO
BEGIN
IF NOT InstallableAddIn.GET(AddInName, PublicKeyToken) THEN
BEGIN
IF NOT CONFIRM(STRSUBSTNO(TXT_AddInNotFound, Description)) THEN
BEGIN
EXIT(FALSE);
END;
END
ELSE
EXIT(AskAndInstallAddIn(Description, InstallableAddIn.InstallableName));
END;
Found := TRUE;
END;

and last but not least – the method that installs the Add-In

AskAndInstallAddIn(Description : Text[80];InstallableName : Text[80]) Retry : Boolean
Retry := FALSE;
IF CONFIRM(STRSUBSTNO(TXT_InstallAddIn, Description)) THEN
BEGIN
Retry := InstallComponent(InstallableName);
END;

BTW, the method to register Add-Ins to this subsystem is

RegisterAddIn(“Control Name” : Text[220];”Public Key Token” : Text[20];Description : Text[128];InstallableName : Text[80])
IF NOT AddIn.GET(“Control Name”, “Public Key Token”) THEN
BEGIN
AddIn.INIT();
AddIn.”Control Add-in Name” := “Control Name”;
AddIn.”Public Key Token” := “Public Key Token”;
AddIn.Description := Description;
AddIn.INSERT(TRUE);
END;
IF NOT InstallableAddIn.GET(“Control Name”, “Public Key Token”) THEN
BEGIN
InstallableAddIn.INIT();
InstallableAddIn.”Control Add-in Name” := “Control Name”;
InstallableAddIn.”Public Key Token” := “Public Key Token”;
InstallableAddIn.InstallableName := InstallableName;
InstallableAddIn.INSERT(TRUE);
END;

As you can see I could have extended the AddIn table – but I decided to go for adding a table instead, it doesn’t really matter.

Ability to Escape and Unescape strings

In the Virtual Earth sample, I need to construct a URL, which contains the company name from NAV. Now with NAV 2009SP1 we use standard Escape and Unescape of strings in the URL, so I have added functions to ComponentHelper to do this. In fact, they just call a function in the C# COM component, which contains these functions.

Ability to register a codeunit or page as Web Service from code

Instead of having to ask partners and/or users to register web services in the Web Service table or form, I have created this small function in the ComponentHelper to do this.

RegisterWebService(isPage : Boolean;”Object ID” : Integer;”Service Name” : Text[80];Published : Boolean)
IF isPage THEN
BEGIN
ObjType := WebService.”Object Type”::Page;
END ELSE
BEGIN
ObjType := WebService.”Object Type”::Codeunit;
END;

IF NOT WebService.GET(ObjType, “Service Name”) THEN
BEGIN
WebService.INIT();
WebService.”Object Type” := ObjType;
WebService.”Object ID” := “Object ID”;
WebService.”Service Name” := “Service Name”;
WebService.Published := Published;
WebService.INSERT();
COMMIT;
END ELSE
BEGIN
IF (WebService.”Object ID” <> “Object ID”) OR (WebService.Published<>Published)  THEN
BEGIN
WebService.”Object ID” := “Object ID”;
WebService.Published := Published;
WebService.MODIFY();
COMMIT;
END;
END;

Global information about the URL to my IIS and Web Service tier

Again – a number of the samples I create will integrate from the RoleTailored Client to an application or a web site, which then again uses Web Services. I found out, that I needed a central way to find the URL of the right Web Service listener and the best way was to create a table in which I store the base URL (which would be ://WS/”>://WS/”>http://<server>:<port>/<instance>/WS/ (default http://localhost:7047/DynamicsNAV/WS/).

Also in the Virtual Earth I spawn up a browser (with HYPERLINK) and I need a location for the intranet server on which an application like the MAP would reside.

Modify Metadata programmatically

I found that all my samples worked fine in the W1 version of NAV 2009 SP1, but as soon as I started to install them on other localized version, the pages on which I added actions etc. had been modified by local functionality and since there is no auto merge of pages, people would have to merge page metadata or find themselves loosing local functionality when they installed my samples.

I have added 4 functions:

GetPageMetadata(Id : Integer;VAR Metadata : BigText)

SetPageMetadata(Id : Integer;Metadata : BigText)

AddToMetadata(Id : Integer;VAR Metadata : BigText;Before : Text[80];Identifier : Text[80];Properties : Text[800]) result : Boolean

AddToPage(Id : Integer;VersionList : Text[30];Before : Text[80];Identifier : Text[80];Properties : Text[800]

where the last function just call the three other (Get, Add, Set metadata).

I am not very proud of the way these functions are made – they just search for a line in the exported text file and inserts some metadata but they meet the needs.

As an example on how these functions are used you will find:

// Read Page Metadata
ComponentHelper.GetPageMetadata(PAGE::”Customer Card”, Metadata);

// Add Map Factbox
ComponentHelper.AddToMetadata(PAGE::”Customer Card”, Metadata, ‘    { 1900383207;1;Part   ;’,
‘    { 66031;1  ;Part      ;’,
‘ SubFormLink=No.=FIELD(No.); PagePartID=Page66030 }’)
OR

// Add View Area Map Action
ComponentHelper.AddToMetadata(PAGE::”Customer Card”, Metadata, ‘      { 82      ;1   ;ActionGroup;’, ‘      { 66030   ;2   ;Action    ;’,
‘ CaptionML=[ENU=View Area Map]; OnAction=VAR MAP : Codeunit 66032; BEGIN MAP.OpenCustomerMAPInBrowser(Rec); END; }’);

// Write Page Metadata back
ComponentHelper.SetPageMetadata(PAGE::”Customer Card”, Metadata);

So basically – it reads the metadata for the page, checks whether the action already has been added (the string ‘      { 66030   ;2   ;Action    ;’ exists already). If not it searches for the string ‘      { 82      ;1   ;ActionGroup;’ and inserts the action below that. Not pretty – but it works.

The Visual Studio piece

As mentioned earlier a couple of functions are needed in a client side COM component.

The Escape and Unescape functions really doesn’t do anything:

public string EscapeDataString(string str)
{
return Uri.EscapeDataString(str);
}

public string UnescapeDataString(string str)
{
return Uri.UnescapeDataString(str);
}

and the essence of the CheckAddIn is the code found in the LoadAddIn function of the AddIn class:

Assembly assembly = Assembly.LoadFrom(dll);

this.publicKey = “”;
foreach (byte b in assembly.GetName().GetPublicKeyToken())
{
this.publicKey += string.Format(“{0:x2}”, b);
}

Type[] types = assembly.GetTypes();
foreach (Type type in types)
{
foreach (System.Attribute att in System.Attribute.GetCustomAttributes(type))
{
ControlAddInExportAttribute expAtt = att as ControlAddInExportAttribute;
if (expAtt != null && !string.IsNullOrEmpty(expAtt.Name))
{
if (!isAddIn)
{
this.controlNames = new List<string>();
isAddIn = true;
}
this.controlNames.Add(expAtt.Name);
}
}
}

Which loads an Add-In, finds the public key token and the registered controls. The rest is really simple – check whether one of the Add-Ins in fact is the one we are looking for – else install it…

The Visual Studio solution also contains a setup project for generating the .msi file which needs to be placed in the ClientSetup folder.

Putting it all together

So, now we have a .fob file and an .msi file which we need to install on the Service Tier – so why don’t we create a Setup project, which contains this .fob (install that in a ServerSetup folder) and the .msi (install that in the ClientSetup folder).

Doing this makes installing the ComponentHelper a 3 step process:

  1. Install ComponentHelper.msi on the Service Tier
  2. Import a .fob from the ServerSetup folder
  3. Run a codeunit which registers the necessary stuff

In fact I am trying to make all the demos and samples installable like the ComponentHelper itself – so that anybody can download cool samples and get a sexy Microsoft Dynamics NAV 2009 SP1 – to work with.

ComponentHelper1.01.zip (which contains ComponentHelper1.01.msi) can be downloaded here.

If you don’t fancy downloading the .msi (for whatever reason) – the source to NAVAddHelper can be downloaded here and the ComponentHelper objects can be downloaded here.

 

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Multiple Service Tiers – SP1

Right around the release of Microsoft Dynamics NAV 2009, I wrote a blog entry with some .bat files on how to create multiple Service Tiers when working with NAV 2009.

The blog post is here: http://blogs.msdn.com/freddyk/archive/2008/10/29/multiple-service-tiers.aspx

Now NAV 2009 SP1 is about to be released, it is time for a small update. One of the files of the package is a CustomSettings.template file, which really is just the CustomSettings.config with a few values replaced with variable names, so that we can replace those automagically.

Now in SP1, the CustomSettings.config has changed – new keys have been added and we also support named instances in the database.

SP1 will actually run with the old config file, so we could just ignore the entire thing and continue as if nothing happened – the .bat files will still work in SP1.

However – if we want to take advantage of the named instances in SQL Server or we want to have the additional keys available for modifying we need to change something.

I have created a new CustomSettings.template based on the SP1 config file – copy the config file and change the following keys:

    <add key=”DatabaseServer” value=”#DBSERVER#”></add>
<add key=”DatabaseInstance” value=”#DBINSTANCE#”></add>
<add key=”DatabaseName” value=”#DATABASE#”></add>
<add key=”ServerInstance” value=”#INSTANCE#”></add>

and extended the createservice.bat file to also allow a database instance to be specified, meaning that the usage is now:

CreateService name [dbserver] [“dbinstance”] [“dbname”] [demand|auto|disabled] [both|servicetier|ws]

The new .zip file is available for download here.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

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

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

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

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

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

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

The Goal

image

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

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

Note that the Weather factbox is not shown here.

What is it?

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

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

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

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

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

How is the control build?

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

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

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

 

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

image

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

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

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

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

6. Add the following class to the file:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

</body>
</html>

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

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

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

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

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

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

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

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

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

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

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

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

But what is the public key token?

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

sn –T VEControl.snk

in a Visual Studio command prompt.

image

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

image

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

image

The code in OnAfterGetRecord is

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

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

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

As usual you can download the visual studio project here.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Microsoft SQL Server Management Studio Express

Most of you probably already know this, and I guess I am the last living nerd to discover that you actually can have SQL Server Management Studio for SQL Express…

Anyway – I will showcase my lack of knowledge and admit that I just downloaded this from MS and installed next to my Demo install of NAV on my Laptop running Windows 7.

image

You can download Microsoft SQL Server Management Studio Express here.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV