Displaying Company information in Card pages

I received a question from a customer, who is running multiple companies and often have multiple instances of NAV open with different companies. On the main page, they do have information about what company is the active:

image

but when they have opened a number of Task Pages in the various instances of NAV, they cannot distinguish one from the other.

Example:

image

This image tells you the page and the Customer name – and you can easily identify the right page when Alt+TAB’ing between pages, but if you have multiple companies this doesn’t help you a lot.

So what determines the caption?

The fields used in the caption on a page is determined by:

DataCaptionExpr on the page. This is an expression, which can use fields, functions etc. to build up a caption. If that isn’t defined, the client looks for

DataCaptionFields on the page. This is a collection of fields, which are used to build the caption by adding them together with a character 183 (middle dot) between them. If that isn’t defined, the client looks for

DataCaptionFields on the table, which basically is the same as DataCaptionFields on the page.

In a standard NAV, there is no DataCaptionExpr nor DataCaptionFields defined on the Customer Card, but on the Customer table you find:

image

In order to add the Company name behind the caption you will need to change the DataCaptionExpr on the Customer Card to f.ex.

“No.” + ‘ · ‘ + Name + ‘ [‘+COMPANYNAME+’]’

which would cause the Customer Card to look like

image

You can of course select to change the expression to whatever you like – or maybe create some function, which automagically returns a caption, only real flipside is that you need to modify the card pages, on which you need this functionality. In the end this is probably not a very large number.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

What COMPANY to use?

As you know, when creating an application consuming NAV Web Services you need to specify the Company name as part of the URL to the Web Service, but what company should you be using?

Some applications are web front-ends placing data from the web application into NAV. For applications like this you typically would have a config file in which you specify what company things needs to go to. For these applications, this post adds no further value.

Other applications are integration applications, like a lot of the applications you can see on my blog:

  • Search
  • “My” gadgets
  • MAP
  • Edit In Excel

for all of these applications, it really doesn’t make sense to run with a different company than the users default company.

Example – if you search through your NAV data – you really want to search through the data in the active company – not just any company.

Wouldn’t it be nice if you could type in the URL

/Codeunit/Search”>http://localhost:7047/DynamicsNAV/WS/<default>/Codeunit/Search

and then the <default> would be replaced by the authenticated users default company – unfortunately this doesn’t work (I have suggested this feature for v7 though:-)). Instead, we have to do the work in the Web Service consuming application. Easiest solution is of course to create a Codeunit with a function, returning the default company of a user, call that and then build your URL for calling the Page / Codeunit web service.

A function like that could be:

GetDefaultCOMPANY() : Text[30]
Session.SETRANGE(“My Session”,TRUE);
Session.FINDFIRST;
WindowsLogin.SETRANGE(ID,Session.”User ID”);
WindowsLogin.FINDFIRST;
UserPers.SETRANGE(“User SID”,WindowsLogin.SID);
UserPers.FINDFIRST;
EXIT(UserPers.Company);

The problem with this approach is (as you probably already figured out) that every call to a Web Service will require 2 roundtrips instead of one and for Page based Web Service access there really isn’t much you can do better.

For Codeunit based Web Service access you can however avoid a lot of these roundtrips by using a very simple pattern in the way you write your functions. I have rewritten my search method to return a Text[30] and start off with the following lines of code:

company := GetDefaultCOMPANY();
IF company <> COMPANYNAME THEN
EXIT(company);

and the consumer will have to build up the URL for the Web Service in code with whatever company (the first in the list of companies would be just fine), call the web service and if it returns a different company than the one used to invoke the web service, build a new URL and try again.

In the Search gadget this would look like (the lines in Red are the important changes)

// the “real” search function
function doSearch(searchstring) {
    specifiedCompany = GetCompany();
usedCompany = specifiedCompany;
if (specifiedCompany == “default”) {
if (myCompany == “”) {
Companies = GetCompanies();
if (Companies != null)
myCompany = Companies[0].text;
}
usedCompany = myCompany;
}

    // Get the URL for the NAV 2009 Search Codeunit
var URL = GetBaseURL() + encodeURIComponent(usedCompany) + “/Codeunit/Search”;

    // Create XMLHTTP and send SOAP document
xmlhttp = new ActiveXObject(“Msxml2.XMLHTTP.6.0”);
xmlhttp.open(“POST”, URL, false, null, null);
xmlhttp.setRequestHeader(“Content-Type”, “text/xml; charset=utf-8”);
xmlhttp.setRequestHeader(“SOAPAction”, “DoSearch”);
xmlhttp.Send(‘<?xml version=”1.0″ encoding=”utf-8″?><soap:Envelope xmlns:soap=”‘ + SoapEnvelopeNS + ‘”><soap:Body><DoSearch xmlns=”‘ + CodeunitSearchNS + ‘”><searchstring>’ + searchstring + ‘</searchstring><result></result></DoSearch></soap:Body></soap:Envelope>’);

    // Find the result in the soap result and return the rsult
xmldoc = xmlhttp.ResponseXML;
xmldoc.setProperty(‘SelectionLanguage’, ‘XPath’);
xmldoc.setProperty(‘SelectionNamespaces’, ‘xmlns:soap=”‘ + SoapEnvelopeNS + ‘” xmlns:tns=”‘ + CodeunitSearchNS + ‘”‘);

    userCompany = xmldoc.selectSingleNode(‘//tns:return_value’);
myCompany = userCompany.text;

    if ((specifiedCompany == “default”) && (myCompany != usedCompany)) {
// Default company has changed – research
return doSearch(searchstring);
}

   … do the actual searching

}

In this sample I use three variables:

specifiedCompany is the company specified in the config file (default means use users default company)

usedCompany is the company used to invoke the last WS method

myCompany is my current belief of the users current company, which gets replaced if a method returns a new default company.

Using a pattern like this will help lowering the number of round trips and still allow your consuming application to use the users default company.

This “trick” is only possible in NAV 2009 SP1. NAV 2009 RTM will change the users default company to the company you use to invoke the Web Service with – which again will cause the above function to always return the same company name as the one you invoke the Web Service with.

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Extending page Web Services (and creating a Sales Order again)

It has been working in the same way since NAV 2009, but I still get asked often how this works, so why not write up a quick post on this. I also realize that my prior post on how to create Sales Orders through Web Services was made very complex due to compatibility with NAV 2009.

This post only works in NAV 2009 SP1 and will show how to extend the Order page with a Post function and show how to Create a Sales Order from C# and post it.

Extending the page

First of all, we need to create a codeunit with the function, we want to add to the Order page.

image

Then we expose this codeunit with the same name as the page we want to extend, without putting a check in the published column

image

Note: All functions in the codeunit needs to have the first parameter be of the same type as the base record as the page you want to extend, else the page will no longer be available and you will get an error in the event log on the Service Tier.

Now taking a look at the WSDL in a browser will show us the new function as a first class citizen

image

and we can start using this.

Creating a Sales Order through Web Services

This might seem like repeating myself from a prior post, but that post did contain a lot of other information, which really isn’t necessary if you only target SP1.

Creating an order is a 3 step process:

  1. Create the Order Header
  2. Fill out the Order Header and create the Order lines
  3. Fill out the Order lines

Creating the Order header

Is really simple

Order_Service service = new Order_Service();
service.UseDefaultCredentials = true;

Order order = new Order();
service.Create(ref order);

After this we have a Order Number and an empty order – exactly like leaving the order No. field on the Sales Order Page.

Fill out the Order Header and create the Order lines

In this sample I will just fill out the Sell_to_Customer_No – a number of the other Order Header fields will be auto-updated when updating the order

order.Sell_to_Customer_No = “10000”;

Then we need to create the Order lines – in this sample I will create 5. BTW – It is NOT trivial to add an order line after the fact, so I suggest you add the needed number of lines in one go:

order.SalesLines = new Sales_Order_Line[5];
for (int i = 0; i < 5; i++)
order.SalesLines[i] = new Sales_Order_Line();
service.Update(ref order);

Fill out the Order lines

In this sample, I will just create 5 lines with green ROME guest chairs.

for (int i = 0; i < 5; i++)
{
order.SalesLines[i].Type = OrderPageRef.Type.Item;
order.SalesLines[i].No = “1960-S”;
order.SalesLines[i].Quantity = 1;
}
service.Update(ref order);

That’s it – the order is created and you can find it in the Client.

And at last… – Post the order

Having created the order, now it is time to post the order

service.PostOrder(order.Key);

As you can see, the function takes a Record parameter, but we give it a Key.

Note, that calling a function does not make an implicit Update – meaning that if you have done changes to the record in C# and call the function, you will get an error when calling update later. Reason – the PostOrder function has changed the record and will tell you that the record was changed by another user.

After calling a function on a page you will need to Re-read the record if you need to do more work.

That’s it

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Synchronize A/D users to NAV

During my work with demos like Edit In Excel, I wanted to make sure that these things would work in all localized versions of NAV 2009 SP1 – meaning that I needed to install 14 different databases and 14 running Service Tier’s. Having done that, I also wanted to allow my colleagues who needed to check something, access to these service tiers.

For a geek (like me:-)), that problem looks like something you need to write an application for, even though it probably takes more time than it would be to add the users one by one whenever needed, but it certainly is more fun to write the application – and… maybe somebody else can actually use the ideas from this to do something cool.

How to do?

Basically what I want to do is, to enumerate the Remote Desktop Users of the computer and make sure that these users are SUPER in NAV. Now, I can do this in NAV calling out to a COM object enumerating my users – but that really wouldn’t help me, because I would have to start NAV with every service tier or database and launch that action.

So, first I created a codeunit in NAV, exposed this codeunit as Web Services. Then I created a console application running on a schedule on my server, which enumerates the users and invoke the Web Service function to check all users are created in NAV.

Of course the scheduled application has to run with elevated permissions and that user needs to be able to change users in NAV as well.

The NAV code

The codeunit contains two functions:

ImportUserSID(SID : Text[119];Role : Code[20]) : Boolean
IF NOT Winlogin.GET(SID) THEN
EXIT(FALSE);
Winlogin.INIT;
Winlogin.SID := SID;
Winlogin.INSERT();
IF NOT WinAccess.GET(SID) THEN
BEGIN
WinAccess.INIT;
WinAccess.”Login SID” := SID;
WinAccess.”Role ID” := Role;
WinAccess.INSERT;
END;
EXIT(TRUE);

and

SynchronizeUsers()
DATABASE.SYNCHRONIZEALLLOGINS();

BTW. Calling SynchronizeAllLogins running through Web Services didn’t seem to work when called through Web Services and running enhanced database security – whether this is a bug or a problem in my setup – I don’t know, but I am going to file a bug on it.

As you can see, the ImportUserSID does not modify the role if the user is already in the login table – it could of course be modified to do that easily, but it wasn’t necessary for my usage.

The C# code for enumerating an A/D group

Lets just look at the code:

DirectoryEntry dir = new DirectoryEntry(“WinNT://localhost/Administrators”);
Console.WriteLine(“Enumerating users”);
foreach (object obj in (IEnumerable)dir.Invoke(“members”))
{
DirectoryEntry user = new DirectoryEntry(obj);
Console.WriteLine(user.Path);
}
Console.WriteLine(“Done”);
Console.ReadLine();

Running this on my computer outputs the following:

image

The next step is, to get the SID’s for each user – now you would think that there should be an SID function on the user object returning a string, but unfortunately it isn’t that simple. Luckily somebody invented the Internet – and luckily somebody was kind enough to post some information for us to use.

Have a look at http://www.netomatix.com/GetUserSid.aspx where I found the following code, which seems to work for the purpose.

On the user object you have a property collection. One of these properties is called objectSid, which is a byte[] and that can be transformed into a SID string using the following function:

private static string ConvertByteToStringSid(Byte[] sidBytes)
{
StringBuilder strSid = new StringBuilder();
strSid.Append(“S-“);
try
{
// Add SID revision.
strSid.Append(sidBytes[0].ToString());
// Next six bytes are SID authority value.
if (sidBytes[6] != 0 || sidBytes[5] != 0)
{
string strAuth = String.Format
(“0x{0:2x}{1:2x}{2:2x}{3:2x}{4:2x}{5:2x}”,
(Int16)sidBytes[1],
(Int16)sidBytes[2],
(Int16)sidBytes[3],
(Int16)sidBytes[4],
(Int16)sidBytes[5],
(Int16)sidBytes[6]);
strSid.Append(“-“);
strSid.Append(strAuth);
}
else
{
Int64 iVal = (Int32)(sidBytes[1]) +
(Int32)(sidBytes[2] << 8) +
(Int32)(sidBytes[3] << 16) +
(Int32)(sidBytes[4] << 24);
strSid.Append(“-“);
strSid.Append(iVal.ToString());
}

        // Get sub authority count…
int iSubCount = Convert.ToInt32(sidBytes[7]);
int idxAuth = 0;
for (int i = 0; i < iSubCount; i++)
{
idxAuth = 8 + i * 4;
UInt32 iSubAuth = BitConverter.ToUInt32(sidBytes, idxAuth);
strSid.Append(“-“);
strSid.Append(iSubAuth.ToString());
}
}
catch (Exception ex)
{
return “”;
}
return strSid.ToString();
}

Using this function, you can now write out the SID’s for each user:

DirectoryEntry dir = new DirectoryEntry(“WinNT://localhost/Administrators”);
Console.WriteLine(“Enumerating users”);
foreach (object obj in (IEnumerable)dir.Invoke(“members”))
{
DirectoryEntry user = new DirectoryEntry(obj);
Console.WriteLine(user.Path);
System.DirectoryServices.PropertyCollection col = user.Properties;
byte[] sidBytes = col[“objectSid”].Value as byte[];
if (sidBytes != null)
{
string strSid = ConvertByteToStringSid(sidBytes);
if (!string.IsNullOrEmpty(strSid))
{
Console.WriteLine(“SID=” + strSid);
}
}
}
Console.WriteLine(“Done”);
Console.ReadLine();

Of course we are not in the business of writing SID’s for users in a console application, but you should now have the building blocks for creating whatever mechanism to add A/D users to NAV, exposing the codeunit we talked about at first and then calling these web services functions from the console application.

Good luck

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

 

 

 

Directions US 2009

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

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

So how did it go?

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

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

“How many people did NOT attend the Keynote?”

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

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

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

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

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

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

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Timer events on a page

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

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

A Timer control is a Non-Visual Add-In

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

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

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

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

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

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

Well – then everything seems pretty simple – right?

Yes and No.

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

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

Application.EnterThreadModal

Application.LeaveThreadModal

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

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

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

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

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

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

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

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

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

Let’s look at the code

The way I have implemented the Timer control is like this

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

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

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

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

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

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

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

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

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

 

A couple of things to note

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

How to use the Control

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

image

with the following global variables:

image

and the following triggers:

OnOpenPage()
timer := ’10’;

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

 

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

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

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

image

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

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

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV

Using touchscreens with the RoleTailored Client

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

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

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

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

image

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

Button Panels

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

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

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

Escaping from Javascript

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

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

Show me the code!!!

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

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

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

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

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

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

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

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

and now the Add-In part of the Control.

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

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

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

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

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

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

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

Things to note:

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

The MyScriptManager could look like this:

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

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

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

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

window.external.clickevent(i, s);

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

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

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

Let’s put the BrowserControl to work for us

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

image

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

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

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

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

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

this should give us the following page when running:

image

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Enjoy

Freddy Kristiansen
PM Architect
Microsoft Dynamics NAV