Welcome to MSDN Blogs Sign in | Join | Help

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('<script type="text/javascript">');
TEMP.ADDTEXT('  function click(i, s) {');
TEMP.ADDTEXT('    window.external.clickevent(i, s);');
TEMP.ADDTEXT('  }');
TEMP.ADDTEXT('</script>');

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 Settings\Application Data"
  filename = folder&"\Microsoft\Microsoft Dynamics NAV\ClientUserSettings.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 Settings\Application 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

Auto deployment of Client Side Components

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/">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

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

Posted by freddyk | 0 Comments
Filed under: ,

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" />
<script type="text/javascript" src="
http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
<script type="text/javascript">
    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);
    }   
</script>
</head> 
<body onload="GetMap();" style="margin:0; position:absolute; width:100%; height:100%; overflow: hidden">
<div id='myMap' style="position: absolute; width:100%; height:100%"></div>  
</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

Posted by freddyk | 0 Comments

Handling Sales Orders from Page based Web Services – in NAV 2009SP1 (and RTM)

First of all, there isn’t going to be a new post on every single record type on how to handle them from Web Services – but Sales Orders are special and the reason for the “(and SP1)” in the titel refers to the fact, that there are changes between RTM and SP1 or maybe a better way to state it is, that the way you could do it in RTM (that might lead to errors) is no longer possible – so you have to do it right.

Secondly, please read the post about the Web Service Changes in SP1 before reading this post – you can find that post here.

Working with the Sales Orders Page from Web Services in NAV 2009SP1

Just to recap a couple of facts about Page based Web Services – we are using the pages and the code on the pages to work with sales orders, this means that we need to mimic the way the RoleTailored Client works, and the RoleTailored Client doesn’t create the order header and all the lines in one go before writing anything to the database. Instead what really happens is that once you leave the primary key field (the Order No) it creates the Order Header in the table. The same with the lines, they are created and then you type data into them, after which they are updated.

So what we need to do is to create the sales order in 4 steps:

1. Create the Order Header
2. Update the Order Header
3. Create an Order Line
4. Update an Order Line
(repeat steps 3-4)

Now this doesn’t mean that you have to do 2 + (no of Orderlines)*2 roundtrips to the server (fortunately) – but you always need 3 roundtrips.

1. Create the Order Header
2. Update the Order Header and Create all Order Lines
4. Update all Order Lines

meaning that you can create all order lines in one go (together with updating header info) and you can update them all in one go.

a code sample for doing this:

// Create Service Reference
var service = new SalesOrder_Service();
service.UseDefaultCredentials = true;

// Create the Order header
var newOrder = new SalesOrder();
service.Create(ref newOrder);

// Update Order header
newOrder.Sell_to_Customer_No = "10000";
// Create Order lines
newOrder.SalesLines = new Sales_Order_Line[2];
for (int idx = 0; idx < 2; idx++)
    newOrder.SalesLines[idx] = new Sales_Order_Line();
service.Update(ref newOrder);

// Update Order lines
var line1 = newOrder.SalesLines[0];
line1.Type = SalesOrderRef.Type.Item;
line1.No = "LS-75";
line1.Quantity = 3;
var line2 = newOrder.SalesLines[1];
line2.Type = NewSalesOrderRef.Type.Item;
line2.No = "LS-100";
line2.Quantity = 3;
service.Update(ref newOrder);

After invoking Create(ref newOrder) or Update(ref newOrder) we get the updated sales order back from NAV, and we know that all the <field>Specified properties are set to true and all strings which has a value are not null – so we can just update the fields we want to update and call update(ref newOrder) again and utilize that SP1 only updates the fields that actually have changed.

This behavior is pretty different from NAV 2009 RTM web services, where it will write all fields to the database if you don’t set strings fields to NULL or set the <field>Specified to false (as described in my previous post).

Making the above code run in NAV 2009RTM

What we really want to do here, is to mimic the behavior of NAV 2009 SP1 in RTM – without having to change the logic.

So I went ahead and wrote two functions. One for making a copy of a record (to be invoked right after your Create(ref xx) or Update(ref xx)) – so we now have a copy of the object coming from NAV. Another function for preparing our object for Update (to be invoked right before calling Update(ref xx)) – to compare our object with the old copy and set all the unchanged fields <field>Specified to false and all unchanged string fields to null.

The two functions are listed towards the end of this post.

Our code from above would then look like:

// Create Service Reference
var service = new SalesOrder_Service();
service.UseDefaultCredentials = true;

// Create the Order header
var newOrder = new SalesOrder();
service.Create(ref newOrder);
SalesOrder copy = (SalesOrder)GetCopy(newOrder);

// Update Order header
newOrder.Sell_to_Customer_No = "10000";
// Create Order lines
newOrder.SalesLines = new Sales_Order_Line[2];
for (int idx = 0; idx < 2; idx++)
    newOrder.SalesLines[idx] = new Sales_Order_Line();
PrepareForUpdate(newOrder, copy);
service.Update(ref newOrder);
copy = (SalesOrder)GetCopy(newOrder);

// Update Order lines
var line1 = newOrder.SalesLines[0];
line1.Type = SalesOrderRef.Type.Item;
line1.No = "LS-75";
line1.Quantity = 3;
var line2 = newOrder.SalesLines[1];
line2.Type = SalesOrderRef.Type.Item;
line2.No = "LS-100";
line2.Quantity = 3;
PrepareForUpdate(newOrder, copy);
service.Update(ref newOrder);

and this code would actually run on SP1 as well – and cause smaller packages to be sent over the wire (not that I think that is an issue).

Deleting a line from an existing Sales Order

Now we have seen how to create a Sales Order with a number of lines – but what if you want to delete a line after having saved the Sales Order. On the Service object you will find a method called Delete_SalesLines, which takes a key and delete that Sales Line.

service.Delete_SalesLines(line.Key);

The only caveat to this is, that if you want to do any more work on the Sales Order, you will have to re-read the Sales Order, else you will get an information that somebody changed the record (and that would be you).

So deleting all lines from a Sales Order could be done by:

foreach (Sales_Order_Line line in so.SalesLines)
    service.Delete_SalesLines(line.Key);

and then you would typically re-read the Sales Order with the following line:

so = service.Read(so.No);

That wasn’t so bad.

My personal opinion is that we should change the Delete_SalesLines to be:

service.Delete_SalesLines(ref so, line);

Which is why I created a function that does exactly that:

void Delete_SalesLines(SalesOrder_Service service, ref SalesOrder so, Sales_Order_Line line)
{
    Debug.Assert(so.SalesLines.Contains<Sales_Order_Line>(line));
    service.Delete_SalesLines(line.Key);
    so = service.Read(so.No);
}

Note, that Í just re-read the order, loosing any changes you have made to the order or order lines. Another approach here could be to remove the line deleted from the lines collection, but things becomes utterly complicated when we try to mimic a behavior in the consumer that IMO should be on the Server side.

A different approach would be to create a codeunit for deleting lines and expose this as an extension to the page (functions added to the page), but we would gain anything, since we still would have to re-read the order afterwards.

Adding a line to an existing Sales Order

More complicated is it, when we want to add a line to an existing Sales Order through the Sales Order Page.

Actually it isn’t complicated to add the line – but it is complicated to locate the newly added line after the fact to do modifications, because it is still true that you need to add the line first and then modify the line afterwards (and update the order).

Adding the line is:

// Create a new Lines array with only the new line and update (meaning create the line)
so.SalesLines = so.SalesLines.Concat(new [] { new Sales_Order_Line() }).ToArray();
service.Update(ref so);

This add’s a new line to the array of lines and update the order.

After invoking update the newly added line is the last in the array (unless somebody messed around in the app-code and made this assumption false).

My personal opinion is that we should add another method to the service called

service.Add_SalesLines(ref so, ref line);

so that we would have the newly added line available to modify and the so available for service.Update(ref so), which is why I created a function that does exactly that:

Sales_Order_Line AddLine(SalesOrder_Service service, ref SalesOrder so)
{
    // Create a new Lines array with only the new line and update (meaning create the line)
    so.SalesLines = so.SalesLines.Concat(new [] { new Sales_Order_Line() }).ToArray();
    service.Update(ref so);
    return so.SalesLines[so.SalesLines.Length-1];
}

Again – If this method existed server side, automatically added by NAV WS, it would be able to do the right thing even though people had mangled in the application logic and change the line numbering sequence or whatever.

A different approach would be to create a codeunit for adding lines and expose this as an extension to the page (functions added to the page). It wouldn’t make the consumers job much easier since we would still have to have any updates to the SalesOrder written before calling the function AND we would have to re-read the sales order after calling the function.

Remember, that if you are using this function from NAV 2009 RTM you might want to consider using PrepareForUpdate before AddLine and GetCopy after AddLine, just as you would do with Update. You could even add another parameter and have that done by the function itself.

Working with other Header/Line objects

Although the samples in this post are using the Sales Orders, the same pattern can be reused for other occurrences of the Header/Line pattern. Just remember that the Header needs to be created first, then you can update the header and create the lines – and last (but not least) you can update the lines.

GetCopy and PrepareForUpdate

Here is a code-listing of GetCopy and PrepareForUpdate – I have tested these functions on a number of different record types and they should work generically

/// <summary>
/// Get a copy of a record for comparison use afterwards
/// </summary>
/// <param name="obj">the record to copy</param>
/// <returns>a copy of the record</returns>
object GetCopy(object obj)
{
    Type type = obj.GetType();
    object copy = Activator.CreateInstance(type);
    foreach (PropertyInfo pi in type.GetProperties())
    {
        if (pi.PropertyType.IsArray)
        {
            // Copy each object in an array of objects
            Array arr = (Array)pi.GetValue(obj, null);
            Array arrCopy = Array.CreateInstance(arr.GetType().GetElementType(), arr.Length);
            for (int arrIdx = 0; arrIdx < arr.Length; arrIdx++)
                arrCopy.SetValue(GetCopy(arr.GetValue(arrIdx)), arrIdx);
            pi.SetValue(copy, arrCopy, null);
        }
        else
        {
            // Copy each field
            pi.SetValue(copy, pi.GetValue(obj, null), null);
        }
    }
    return copy;
}

/// <summary>
/// Prepare record for update
/// Set <field> to null if a string field hasn't been updated
/// Set <field>Specified to false if a non-string field hasn't been updated
/// </summary>
/// <param name="obj">record to prepare for update</param>
/// <param name="copy">copy of the record (a result of GetCopy)</param>
void PrepareForUpdate(object obj, object copy)
{
    Debug.Assert(obj.GetType() == copy.GetType());
    Type type = obj.GetType();
    PropertyInfo[] properties = type.GetProperties();
    for(int idx=0; idx<properties.Length; idx++)
    {
        PropertyInfo pi = properties[idx];
        if (pi.Name != "Key" &&
          
pi.GetCustomAttributes(typeof(System.Xml.Serialization.XmlIgnoreAttribute), false).Length == 0)
        {
            if (pi.PropertyType.IsArray)
            { 
                // Compare an array of objects - recursively
                Array objArr = (Array)pi.GetValue(obj, null);
                Array copyArr = (Array)pi.GetValue(copy, null);
                for (int objArrIdx = 0; objArrIdx < objArr.Length; objArrIdx++)
                { 
                    object arrObj = objArr.GetValue(objArrIdx);
                    PropertyInfo keyPi = arrObj.GetType().GetProperty("Key");
                    string objKey = (string)keyPi.GetValue(arrObj, null);
                    for (int copyArrIdx = 0; copyArrIdx < copyArr.Length; copyArrIdx++)
                    {
                        object arrCopy = copyArr.GetValue(copyArrIdx);
                        if (objKey == (string)keyPi.GetValue(arrCopy, null))
                            PrepareForUpdate(arrObj, arrCopy);
                    }
                }
            }
            else
            {
                object objValue = pi.GetValue(obj, null);
                if (objValue != null && objValue.Equals(pi.GetValue(copy, null)))
                { 
                    // Values are the same – signal no change
                    if (pi.PropertyType == typeof(string))
                    {
                        // Strings doesn’t have a <field>Specified property – set the field to null
                        pi.SetValue(obj, null, null);
                    }
                    else
                    { 
                        // The <field>Specified is autogenerated by Visual Studio as the next property

                        idx++;
                        PropertyInfo specifiedPi = properties[idx]; 
                        // Exception if this assumption for some reason isn’t true

                        Debug.Assert(specifiedPi.Name == pi.Name + "Specified");
                        specifiedPi.SetValue(obj, false, null);
                    }
                }
            }
        }
    }
}

That’s it, not as straightforward as you could have wished for, but SP1 definitely makes things easier once it comes out.

And I will be pushing for a better programming model for this in v7.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Web Services changes in NAV 2009 SP1

NAV 2009 SP1 is being released later this year, so why write about it now?

The main reason is, that NAV 2009 SP1 comes out with a couple of changes, you might want to take into consideration when writing towards NAV 2009 Web Services.

Except for some performance enhancements, the major changes are:

New standard encoding of the Company name

I think one of the questions that has been asked the most is, how does my company name look in the URL: http://server:7047/DynamicsNAV/WS/<company>/

we all know that “CRONUS International Ltd.” becomes “CRONUS_International_Ltd”. Lesser known is it that “CRONUS USA, Inc.” becomes “CROUNS_USA_x002C__Inc” and there are a lot of special cases in this encoding algorithm.

In NAV 2009 SP1 we change to a standard Uri EscapeDataString function, meaning that

CRONUS International Ltd. becomes CRONUS%20International%20Ltd.

CRONUS USA, Inc. becomes CRONUS%20USA%2C%20Inc.

and

CRONUS+,æøå becomes CRONUS%2B%2C%C3%A6%C3%B8%C3%A5

fact is that you actually just can type in the Company name in the URL with the special characters in the browser and it will (in most cases) figure out to select the right company, even in Visual Studio when making your Web Reference (this won’t work if you have / or ? in the company name).

btw you can escape all the characters if you wish to

http://localhost:7047/DynamicsNAV/WS/%43RONUS%20International%20Ltd./Services

is a perfectly good company name – if you prefer Hex over characters.

This change has also affected the return values of the Companies function in SystemService – it now returns the un-encoded company names (= clear text). You can not any longer use the output from the companies function to build your URL – you need to escape the name.

Note: There is no backwards compatibility, trying to access webservices with a URL from NAV 2009 will fail, you need to change the company name encoding.

Schema changes in ReadMultiple and UpdateMultiple

Microsoft Infopath couldn’t modify multiple records using the NAV2009 page based Web Services due to a schema incompatibility. In NAV 2009 SP1 the schema changes for these methods. If you are using proxy based Web Service access (the add service or web reference in Visual Studio) you should just update the reference. If you are using XML Web Services you might have to modify the code used to parse the XML.

I will of course modify the samples on my blog where I use XPath to query the XML.

Updating records in Page based web services only updates the fields that you actually changed

The basics of XML Web Services is, that you send an XML document to a WebServices telling what you want to change. Visual Studio makes it easy to create a reference to a Web Service and get Strongly typed access to f.ex. Customers and Sales Orders through pages.

But how do we tell Web Services which fields actually changed?

For this, Visual Studio autogenerates a <field>Specified boolean property for all non-string fields from NAV and we will change ALL the fields, where <field>Specified is true or where a string is not NULL – NULL in a string value doesn’t mean clear the field, it means don’t update the field.

If you want to clear a field, set the value to String.Empty (“”).

In some cases this have caused problems. Primarily because when you read a customer record in order to change his name, it comes back from the Get function with all <field>Specified set to TRUE and all string fields have content. Changing the Name of a customer – writes the NAME and since the SEARCHNAME is included in the data sent to Web Services that gets updated as well (meaning that NAME and SEARCHNAME could be out of sync).

In NAV 2009 SP1 that has changed. Visual Studio of course still uses <field>Specified and string field <> NULL to determine what comes over the wire, but on the NAV side we only persist what you actually changed, so in NAV 2009 SP1 you can do:

Customer customer = custService.Read("10000");
customer.Name = "The Cannon Group, Inc.";
custService.Update(ref customer);

and it will only update the name of the Customer. In NAV 2009 you would have to either set all the other fields in the customer to NULL or <field>Specified to false in order to get the same behavior – OR you could do like this:

Customer readCustomer = custService.Read("10000");
Customer updateCustomer = new Customer();
updateCustomer.Key = readCustomer.Key;
updateCustomer.Name = "The Cannon Group, Inc.";
custService.Update(ref updateCustomer);

Which also will update only the name (just a small trick, instantiating a new Customer() will have all string fields set to NULL and <field>Specified for other fields set to false – and now we can just set the fields we want to change. Remember setting <field>Specified to true for all non-string fields.).

Note that this will of course work in SP1 as well and the advantage here is that you actually only send the new customer name over the wire to the Web Service.

Changes to how you Create and Updating Sales Orders through a page based Web Service

Actually the way you need to work with Sales Orders in NAV 2009 SP1 through a page based Web Service will also work in NAV 2009 – but the other way around is a problem. In NAV 2009 you could create a sales order with lines with just one call to Web Services, but in reality this didn’t work, you need to do this with a couple of roundtrips.

This is because application code (CurrPage.UPDATE) relies on one kind of transactional behavior (the new order is inserted and committed before any validation trigger starts), but Web Services enforce a different kind (every call to server is wrapped into one atomic transaction that is either committed or rolled back entirely – meaning that insert is not committed until all validation triggers passed).

I will create a special post on how to work with Sales Orders from Web Services – and try to show a way, which works for NAV 2009 SP1 (the same method will work for NAV 2009 as well – so you should consider this early).

Web Services doesn’t change the users default company

A Web Service consumer application would change the users default company in NAV 2009, but since Web Services doesn’t really use the notion of a default company for anything this seemed confusing – and made it impossible for a web service consumer application to call a web service method to request the users default company. In NAV 2009 SP1 – invoking a Web Service method does not change the default company for the user.

Blob fields in Page based Web Services are ignored

In NAV 2009 you couldn’t have a BLOB field on a page (image or whatever), which you exposed as a Web Service.

In NAV 2009 SP1, this has changed. This doesn’t mean that NAV transfers the content of the BLOB to the web service consumer – the field is just ignored.

If you want access to the value of the Blob you will need to write some code, which you can read something about here :

http://blogs.msdn.com/freddyk/archive/2008/11/04/transferring-data-to-from-com-automation-objects-and-webservices.aspx

Codeunits ignores methods with unsupported parameters

Like Pages with unsupported fields (BLOB), also codeunits can be exposed as Web Services even though they contain methods that use parameter types, that are not supported by Web Services. This could be streams, automation, blobs and more.

In SP1 you can connect to NAV Web Services from both PHP and Java

I won’t cover the details in this post, but it should be clear that NAV WebServices are accessible from PHP and Java come SP1. As soon as I have a build, which supports this – I will write a couple of posts on how to do this.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Posted by freddyk | 3 Comments

Installing NAV 2009 without A/D

The majority of installations of NAV 2009 will be in a network environment, you will have a domain server and an Active Directory with your users and your biggest worries will be how to setup a 3T environment on 3 different boxes, getting the delegation setup correctly all of that stuff.

But in a few cases we might run into situations, where there is only a LAN between a number of machines and no server.

In this post I will show what it takes to make one XP computer run the RoleTailored Client (or the Classic) on one XP computer and have another XP computer be the Database Tier, Service Tier - and of course run a client too.

WorkXP1 and WorkXP2

First of all - I have installed two XP computers with XP SP3 and nothing else - no Office, no Visual Studio, no nothing - just plain good old XP.

In both XP's I have used the Set up a home or small office network wizard to make the computers join a workgroup and be able to see each other

image

The computers are named WorkXP1 and WorkXP2 - and there is one user on each computer XP1 on WorkXP1 and XP2 on WorkXP2.

So what I want to do, is to install the full NAV 2009 on WorkXP1 and only the Clients on WorkXP2 - and make WorkXP2 run with WorkXP1 as the Service Tier and Database tier for the RTC or the Classic. I will also see that I can in fact run Web Services from WorkXP2 as well.

I did not try to install the database tier separate from the Service Tier, as I do not think it is relevant in a scenario like this.

Installing the full NAV2009 on WorkXP1

Note, that I cannot just insert the DVD and press Install Demo, because I don't have Microsoft Outlook installed, so I select Choose an installation option and then Customize under Server (or Client or Database Components) and select the following options

image

and then just accept the default setup values and install.

Installing the Clients on WorkXP2

on the WorkXP2 we install both clients - Again Choose an Installation option and select customize under Client and Select to run Classic from My Computer.

image

After selecting Next you will need to Click the RoleTailored Client link and specify the Service Tier computer (which is WorkXP1).

image

Installing...

If you wonder how - I am running this experiment as two virtual machines on a Windows 2008 Server running Hyper-V.

image

image

Workgroup networking

As you might have guessed - starting the clients on WorkXP1 just works, and trying the same on WorkXP2 doesn't.

We need to do 3 things in order to make this work...

1. Create the XP2 user on WorkXP1

The way workgroups works is to authenticate the XP2 user on WorkXP1 using his own Username and Password, so you need to create all the users on the "server", and note, then need to have the same password.

image

After this you of course need to create the user in NAV (using the Classic Client on WorkXP1) and give him the right permissions (In my world everybody is SUPER).

Having done this, we try to start the Classic Client on the WorkXP2 machine and get the following result:

image

This means that the firewall is blocking access to the SQL Server.

2. Open the firewall for SQLSERVR (if you want to run Classic)

On WorkXP1 open the Control Panel and open Windows Firewall. Click Add Program, browse and locate SQLSERVR.EXE under C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Binn add this to the list of exceptions:

image

At the top of the list of exceptions you will BTW. find DynamicsNAVServer - which means that the RoleTailored actually doesn't need this setting.

Trying to connect from the Classic Client now will give a different error:

image

The reason for this is that XP by default is running Simple Sharing, meaning that everybody will login as Guest on a different computer and not trying to login with their own Username and Password.

3. Disable Simple Filesharing on WorkXP1

On WorkXP1 in the Control Panel or any explorer window select Tools -> Folder Options, goto the View Tab and remove the checkmark from Use simple file sharing.

image

After having done this - you should be able to connect using the Classic Client and also the RoleTailored Client should now work.

image

 

It is probably not any different than what you would do with NAV 5.0 - but a number of people has asked me whether or not it is possible - and it is.

BTW – Web Services works in exactly the same way. If you start the Web Service listener on WorkXP1 – you can navigate to

http://WorkXP1:7047/DynamicsNAV/WS/CRONUS%20International%20Ltd/Services (this is the SP1 URL)

You might have to open the firewall for port 7047 if it doesn’t pick up that DynamicsNAVServer is listening on both ports.

 

Enjoy

 

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Dynamic references to NAV Page Web Services in C#

When creating the very first (never published) version of Edit In Excel, it was loosely coupled, meaning that I did not have any Web references in the project to the Customer Page, Vendor Page or other pages. I read the WSDL and used XPath to traverse the XML and build up structures and I was able to attach to any Page Web Service.

The code wasn’t nice, and I was afraid that I would confuse more people than necessary if I posted that source. So I decided to go with a version, where I had a Web References in VS for every page you can connect to Excel.

The caveat of this approach is that everytime you customize the Customer Page, you need to recompile your Edit In Excel solution – AND you need to do some work if you want to add additional pages. Wouldn’t it be nice if we could avoid this?

If you study the code in the Edit In Excel, you will find that the type-strong web references really aren’t used that much, the majority of the code uses the type of the service, the type of the field enumeration etc.

Last week I stumbled over a very interesting blog called crowsprogramming. A special thanks to the author for this post:

C# - Dynamically Invoke Web Service At Runtime

which in details shows how to ask the read the WSDL, build a service description and compile it into an assembly, and it basically consists of 3 methods:

/// <summary>
/// Builds an assembly from a web service description.
/// The assembly can be used to execute the web service methods.
/// </summary>
/// <param name="webServiceUri">Location of WSDL.</param>
/// <returns>A web service assembly.</returns>
public static Assembly BuildAssemblyFromWSDL(Uri webServiceUri)

/// <summary>
/// Builds the web service description importer, which allows us to generate a proxy class based on the
/// content of the WSDL described by the XmlTextReader.
/// </summary>
/// <param name="xmlreader">The WSDL content, described by XML.</param>
/// <returns>A ServiceDescriptionImporter that can be used to create a proxy class.</returns>
private static ServiceDescriptionImporter BuildServiceDescriptionImporter(XmlTextReader xmlreader)

/// <summary>
/// Compiles an assembly from the proxy class provided by the ServiceDescriptionImporter.
/// </summary>
/// <param name="descriptionImporter"></param>
/// <returns>An assembly that can be used to execute the web service methods.</returns>
private static Assembly CompileAssembly(ServiceDescriptionImporter descriptionImporter)

When you call the first method with our NAV WebServices URL, it reads the WSDL, creates a CodeDom of the proxy classes, compiles the proxy and returns an assembly, which you can reflect over – all it takes is the following line of code:

// create an assembly from the web service description
Assembly webServiceAssembly = BuildAssemblyFromWSDL(
           
new Uri("http://localhost:7047/DynamicsNAV/WS/CRONUS_International_Ltd/Page/Customer"));

Type weak

So, what can you really do with an Assembly in your hand…

You cannot use statements like:

if (customer.No == “10000”)

if the webservice isn’t added to the project. How should Visual Studio know that there is a No field in customer. So any Web References you add dynamically will only be there to reflect over and use via reflection, but you can do everything using reflection – it is just harder.

First of all – we can enumerate the public types in the assembly:

// Create Service Reference
Type[] types = webServiceAssembly.GetExportedTypes();
foreach (Type type in types)
    Console.WriteLine(type.ToString());

running this, will output the following:

Customer_Service
Customer
Blocked
Copy_Sell_to_Addr_to_Qte_From
Application_Method
Reserve
Shipping_Advice
Customer_Filter
Customer_Fields

Which are the public types from an assembly, which is the proxy to a NAV Customer Page Webservice. Knowing that all pages follows the same pattern and that everything in Edit In Excel uses reflection over these classes anyway, it really became too compelling to rip out the Web Service References and make everything dynamic (that post will follow this one).

Working with reflection

I am not going to go into detail about how reflection works and what you can do with reflection, but I will show some examples of how to work with the dynamic assembly. First of all we want to create our service class:

Type serviceType = webServiceAssembly.GetType("Customer_Service");
object service = Activator.CreateInstance(serviceType);

if using static web references, this would be Customer_Service service = new Customer_Service();

Now we need to set the UseDefaultCredentials property to true:

PropertyInfo useDefaultCredentials = service.GetType().GetProperty("UseDefaultCredentials");
useDefaultCredentials.SetValue(service, (object)true, new object[] { });

in other words, get the info-class about the property based on the type, and call the setValue on the propertyinfo, specifying the object instance you want to set the value in, the value and an empty array, specifying that there are no parameters for this call.

Using static web references, this would be service.UseDefaultCredentials = true;

Next thing we want to do, is to call ReadMultiple and get all customers:

MethodInfo readMultiple = service.GetType().GetMethod("ReadMultiple");
object[] customers = (object[])readMultiple.Invoke(service, new object[] { null, null, 0 });

You see the picture – get the method info-class based on the type, and invoke the instance based method, specifying the instance and an object[] which contains the parameters you want to use.

In the static world this would be Customer[] customers = service.ReadMultiple(null, null, 0);

Now, we have an array of objects and the objects are of type Customer – but we don’t know about the customer type – only from reflection, so if we want to write the names of all customers we have to do something like:

Type customerType = webServiceAssembly.GetType("Customer");
PropertyInfo no = customerType.GetProperty("No");
PropertyInfo name = customerType.GetProperty("Name");
foreach (object customer in customers)
    Console.WriteLine(no.GetValue(customer, new object[] { }) + " " + name.GetValue(customer, new object[] { }));

Which in a static implementation would be foreach(Customer customer in customers) Console.Writeline(customer.No + “ “ + customer.Name);

When to use dynamic Web References?

So, by now you got it – and yes, it is WAY easier to work with static Web References, inserted in the solution and using the strongly typed classes and methods, so when would you use dynamic Web References?

My answer to this is: Whenever you want to make something generic, where you can connect to different pages and/or where you don’t mind that the pages gets customized. In the scenario, where you have a fixed contract for requesting order information from a web service in NAV, there is absolutely no reason to use dynamic web references. In cases where you are connecting to a page based web reference where you have control over the page, it is easier (and probably safer due to type checking) to use the type strong web service access and maybe using LINQ with NAV Web Services from my last post.

But… – for something like Edit In Excel, dynamic Web References is a gift – and is really useful. I also think that it could be very useful with Bugsy’s Sharepoint sample – I need to investigate that…

Privileges

I was concerned whether stuff like this would require elevated privileges, but it turns out, that as long as the DLL you are creating / calling is going to run in the same context as your application, this doesn’t require anything. I tested this out running as non-administrator with UAC (Windows Vista User Access Control) turned on.

As usual, you can download the DynamicWebReference solution here.

The next thing I will do, is to extract some of the code from the Edit In Excel and create a set of classes, which makes working with dynamic web references easier. This will of course then be rolled back into the Edit In Excel R2.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Posted by freddyk | 0 Comments
Filed under: , , ,

Using LINQ with NAV Web Services

In .NET 3.5 Microsoft released LINQ (Language INtegrated Query) – which is a way of writing select statements strongly typed inside your C# code.

Wouldn’t it be nice if we could leverage this technology when connecting to NAV Web Services?

I thought YES – so I set out to find out what it took…

The goal was to be able to replace code like this:

CustomerRef.Customer_Service cservice = new CustomerRef.Customer_Service();
cservice.UseDefaultCredentials = true;

CustomerRef.Customer_Filter filter = new CustomerRef.Customer_Filter();
filter.Field = CustomerRef.Customer_Fields.Location_Code;
filter.Criteria = "=YELLOW|=BLUE";
CustomerRef.Customer_Filter[] filters = new CustomerRef.Customer_Filter[] { filter };

CustomerRef.Customer[] customers = cservice.ReadMultiple(filters, null, 0);

foreach (CustomerRef.Customer customer in customers)
{
    // do stuff
}

with code like this:

CustomerRef.Customer_Service cservice = new CustomerRef.Customer_Service();
cservice.UseDefaultCredentials = true;

var cquery = from c in new FreddyK.NAVPageQuery<CustomerRef.Customer>(cservice)
             where c.Location_Code == "YELLOW" || c.Location_Code == "BLUE"
             select c;

foreach (CustomerRef.Customer customer in cquery)
{
    // do stuff
}

Which I personally find more readable and has a lot of other advantages:

  1. Strongly typed criterias – if fields change type you get a compile error after updating the Web Reference
  2. Don’t have to think about CultureInfo (NumberFormat and DateFormat) when doing your criteria
  3. Don’t have to duplicate quotes for your search string
  4. Don’t have to convert booleans into Yes/No
  5. Separation of Query and Values in the Query
  6. and probably a lot more…

I always heard that you only needed 3 good reasons for doing something – so this should be more than enough.

Understanding LINQ

Lets look at the above code. The following

CustomerRef.Customer_Service cservice = new CustomerRef.Customer_Service();
cservice.UseDefaultCredentials = true;

initializes the Service (as usually) and

var cquery = from c in new FreddyK.NAVPageQuery<CustomerRef.Customer>(cservice)
             where c.Location_Code == "YELLOW" || c.Location_Code == "BLUE"
             select c;

creates a query (which contains an expression tree of the where clause) and

foreach (CustomerRef.Customer customer in cquery)
{
    // do stuff
}

invokes the query (calls Web Services ReadMultiple) and returns the customers.

FreddyK.NAVPageQuery is a class, which is capable of acting as a LINQ provider for all pages exposed as Web Services.

The NAVPageQuery class is a generic class where you specify the type returned by the service – and you specify the service object as a parameter to the constructor. No need to say, that these two must match, else you will get an exception.

For a class to be usable in a LINQ query it needs to implement IQueryable<type> (which is a combination of IQueryable, IEnumerable<type> and IEnumerable) – and typically you will then have another class, which implements IQueryProvider. In my sample, I have implemented both interfaces in the NAVPageQuery.

Instead of listing the interfaces, I will list what happens when the query is build (when the line with var cquery = is executed):

  1. The NAVPageQuery gets instantiated
  2. LINQ calls the method IQueryProvider IQueryable.Provider to get the IQueryProvider class
  3. LINQ calls the method System.Linq.Expressions.Expression IQueryable.Expression to get the initial expression
  4. Now LINQ builds the expression tree and calls IQueryable<S> IQueryProvider.CreateQuery<S>(Expression expression) from which it expects a IQueryable<type> object.

Now we are done building the query and our variable cquery is set to be of type IQueryable<Customer>

When we then do a foreach on the cquery a little later, the following this happens:

  1. foreach calls into the IEnumerable interface IEnumerator<T> IEnumerable<T>.GetEnumerator() and ask for an enumerator
  2. In this method, I call the Execute method (TResult IQueryProvider.Execute<TResult>(Expression expression)) on the Provider (which is ‘this’)
  3. In the Execute method I run through the Expression tree and builds a number of Filter objects, collect them into an array of filters and call ReadMultiple.

You could argue that it would be a good idea to run through the expression tree while building the query – but the primary reason for NOT doing this is that you could imagine queries like:

decimal amount = 10000;

var cquery = from c in new FreddyK.NAVPageQuery<CustomerRef.Customer>(cservice)
             where c.Balance_LCY > amount
             select c;

In this case you want to separate building the query and creating the filters, since you could:

foreach (CustomerRef.Customer customer in cquery)
{
     // do stuff
}

amount = 100000;
foreach (CustomerRef.Customer customer in cquery)
{
    // do other stuff
}

this demonstrates the need for having the ability to decouple the query and the values used by the query and this is the reason for not creating the filter specs while building the query – but keeping the expression tree.

Another sample:

cquery = from c in new FreddyK.NAVPageQuery<CustomerRef.Customer>(cservice)
         where c.Last_Date_Modified >= DateTime.Today.AddDays(-5)
         select c;

At the time of execution, this query will give you customers modified the last 5 days (imagine building that one with filters easy)

Important things to notice

It is important to realize that NAVPageQuery doesn’t give you any functionality you didn’t have before. With the original Filter class you cannot OR things together (unless it is the same field) – you also cannot do that using LINQ (towards NAV Web Services)

If you try to type in a where clause like

where c.Location_Code == "YELLOW" || c.Country_Region_Code == "US"

It will fail – intentionally!

Of course I could have done client side filtering – but this would only fool the developer, since you would take the performance hit when calling ReadMultiple and you wouldn’t really notice – I wanted the developer to be aware when he makes clever or not so clever choices.

So – the where clause needs to be simple enough so that I can convert it into a filter specification.

The error will be thrown when you try to use the query and it will typically be an exception stating “Query too complex” (even though it might not be complex at all).

Another thing to notice is that AND’s and OR’s cannot be combined (as this isn’t possible in the filter spec):

where c.Location_Code == "YELLOW" && (c.Balance_LCY > amount || c.Location_Code == "BLUE")

is not allowed, but

where c.Balance_LCY > amount && (c.Location_Code == "YELLOW" || c.Location_Code == "BLUE") && c.No != ""

works just fine.

What about updating, deleting and creating?

Nothing new there – you still have your service object and the Customer objects that are returned from the LINQ query is the same object you would get from reading through Readmultiple directly.

This class is only designed to make queries towards web services easier and more error safe.

That’s all good – but how is it made?

For once, I am not going to go through how NAVPageQuery is created in detail – I have commented the source code pretty detailed and if you need to understand how it is build, you can look at the sourcecode combined with the information on how things are executed (from above) and it should be possible to decrypt the source:-)

If you download the solution .zip file here, you will find a class library with one class (NAVPageQuery) and a Console application called TestQuery. The console app. can be deleted if you don’t want it and it has a number of samples – some of which are listed above.

When downloading the test app. – you need to update the web references (so that they match your exposed Pages) – I know that I added the Last_Date_Modified to the Customer Page in order to have access to that field through web services – there might be more.

Questions and comments are welcome, I prefer to answer questions on mibuso instead of here – since it typically makes the answer available to more people.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Posted by freddyk | 0 Comments
Filed under: , , ,

Bugsy's blog is alive

I just received the information, that Bugsy's blog (http://blogs.msdn.com/pchriste) is alive and the first post in that one looks really cool.

Showing NAV Data in Sharepoint using Business Data Catalogue.

using Web Service in NAV 2009.

I know what I am going to play with tonight:-)

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Posted by freddyk | 0 Comments
Filed under: ,
More Posts Next page »
 
Page view tracker