Atlantic Oak

Extensible Controls in Dynamics 365 for Finance and Operations (Tutorial)

In Dynamics 365 for Finance and Operations (D365) Extensible Controls take on the role of the Managed Host Control of AX 2012. In AX 2012 the Managed Host Control could either be a Windows Forms Control or a WPF control. In its simplest form in D365 an Extensible Control is a piece of HTML with JavaScript functionality.

Although at first it might seem that Extensible Controls cannot carry out many of the tasks of Managed Host Controls and that Extensible Controls are limited, let me assure you that it isn't the case. From a UX perspective, HTML5 coupled with JavaScript can do anything that a WF or WPF control can do. It can even outperform them. Given the fact that HTML5 and JavaScript are present in so many devices, and in so many different platforms, it is beyond doubt the user interface language of choice by today's standards.

Now having said that, developing Extensible Controls is much more complicated than writing WF or WPF controls. In this tutorial, we will try to demystify that complexity. To understand this tutorial you must have a basic knowledge of HTML, CSS, JavaScript, JQuery and X++.


We are going to build an interactive tile navigation menu that you can include in any form. It will have some tiles that when clicked will take the user to another form. The tiles will have two sizes and the user will be able to change the background color of the control. This tutorial does not include data binding and will not involve the build class.

At the end of this tutorial you will know:

  • How to trigger an action in JavaScript from X++. A user presses a button on the form and the control displays a message box.
  • How to use properties and the observable pattern. A user selects a color from a drop-down and changes the background color for the control.
  • How to use a method to load data into the control using JSON. Tile information such as position, size, caption and display menu item are loaded from X++ during run time.
  • How to use a method to trigger an action in X++ that originates in JavaScript. A user clicks on a tile in the control and gets redirected to the corresponding D365 for Finance and Operations page via the associated Display Menu Item for the tile.

Control Physical Structure

An Extensible Control is made up of five files:

  • An X++ build class. Which houses the control's design time properties menu. We won't go into this class in this tutorial.
  • An X++ runtime class. Which contains properties and methods which we can call from JavaScript or from X++.
  • An HTML resource file.
  • A JavaScript resource file.
  • A CSS resource file. Optional.

Create the Basic Extensible Control Project Structure

1. Create a new model called NavMenu in a new package and make it reference Application Foundation and Application Platform.








2. Create a new operations project. Call this new project "NavigationMenu".


3. Change the project's model to NavMenu. Right click on the project in solution explorer and click properties.


4. Right click on your computer's desktop then "New -> Text Document" three times.


5. Rename the three files to:

NAMNavigationMenuCtrl.htm

NAMNavigationMenuCtrl.js

NAMNavigationMenuCtrl.css


6. Create a resource called NAMNavigationMenuCtrlHTM and select the NAMNavigationMenuCtrl.htm file from the desktop into it.


7. Create a resource called NAMNavigationMenuCtrlJS and select the NAMNavigationMenuCtrl.js file from the desktop into it.


8. Create a resource called NAMNavigationMenuCtrlCSS and select the NAMNavigationMenuCtrl.css file from the desktop into it.


9. Create a class called NAMNavigationMenuCtrl.


10. Create a class called NAMNavigationMenuCtrlBuild.


11. You should now have the following project structure:


12. In the NAMNavigationMenuCtrl class include the following code:

[FormControlAttribute("NAMNavigationMenuCtrl","",classstr(NAMNavigationMenuCtrlBuild))]
class NAMNavigationMenuCtrl extends FormTemplateContainerControl
{
    protected void new(FormBuildControl _build, FormRun _formRun)
    {
        super(_build, _formRun);
        this.setResourceBundleName("/resources/html/NAMNavigationMenuCtrl");
    }
}

13. In the NAMNavigationMenuCtrlBuild class include the following code:

[FormDesignControlAttribute("NAMNavigationMenuCtrl")]
class NAMNavigationMenuCtrlBuild extends FormBuildContainerControl
{
}

14. In the NAMNavigationMenuCtrl.htm file include the following markup:

<script src="/resources/scripts/NAMNavigationMenuCtrl.js"></script>
<link href="/resources/styles/NAMNavigationMenuCtrl.css" rel="stylesheet" type="text/css" />
<div id="NAMNavigationMenuCtrl" data-dyn-bind="sizing: { height: $data.Height, width: $data.Width }, visible: $data.Visible">
    <p>This is the NAMNavigationMenuCtrl</p>
</div>

15. In the NAMNavigationMenuCtrl.js file include the following code:

(function () {
    'use strict';
    $dyn.ui.defaults.NAMNavigationMenuCtrl = {};
    $dyn.controls.NAMNavigationMenuCtrl = function (data, element) {
        var self = this;
        $dyn.ui.Control.apply(self, arguments);
        $dyn.ui.applyDefaults(self, data, $dyn.ui.defaults.NAMNavigationMenuCtrl);
    }
    $dyn.controls.NAMNavigationMenuCtrl.prototype = $dyn.extendPrototype($dyn.ui.Control.prototype, {});
})();

16. We will add styles to the NAMNavigationMenuCtrl.css file later.

17. Build the solution.

18. Create a model called NavMenuTest in a new package and make it reference the Application Platform, Application Suite and NavMenu models.

Create new model Dynamics 365 for Finance and Operations Step 1


Create new model Dynamics 365 for Finance and Operations Step 2



Create new model Dynamics 365 for Finance and Operations Step 3



Create new model Dynamics 365 for Finance and Operations Step 4


Create new model Dynamics 365 for Finance and Operations Step 5

19. Create a new project in the same solution called NavigationMenuTest, right click on this project select properties and change the model to NavMenuTest.


20. Add a new form to the NavigationMenuTest project and call it NATForm1.

21. Make NavigationMenuTest the startup project and NATForm1 the startup object.




22. Open the NATForm1 form, right click on design and select the NAMNavigationMenuCtrl.


23. Start debugging. You should see the caption of the Form "NATForm1" with the control underneath "This is the NAMNavigationMenuCtrl". The caption for the control comes from the htm file.


And this is all it takes to have a simple Dynamics 365 for Finance and Operations Extensible Control "hello world". Now we're going to look at the element object in JavaScript.

The element object gets passed as a parameter in the main function in JavaScript:

    $dyn.controls.NAMNavigationMenuCtrl = function (data, element) {
        …
    }

The element object represents the main div of the control in HTML:

<div id="NAMNavigationMenuCtrl" data-dyn-bind="sizing: { height: $data.Height, width: $data.Width }, visible: $data.Visible">
    …
</div>

So we can use JQuery to modify the element object and in consequence the main div. We will include the following line of code into our JavaScript:

$(element).css("background-color", "yellow");

Complete .js code:

(function () {
    'use strict';
    $dyn.ui.defaults.NAMNavigationMenuCtrl = {};
    $dyn.controls.NAMNavigationMenuCtrl = function (data, element) {
        var self = this;
        $dyn.ui.Control.apply(self, arguments);
        $dyn.ui.applyDefaults(self, data, $dyn.ui.defaults.NAMNavigationMenuCtrl);
        $(element).css("background-color", "yellow");
    }
    $dyn.controls.NAMNavigationMenuCtrl.prototype = $dyn.extendPrototype($dyn.ui.Control.prototype, {});
})();

Press F5 or Debug -> Start Debugging and you will notice that the div's background will be changed to yellow:


In the div we have the data-dyn-bind attribute:

<div … data-dyn-bind="sizing: { height: $data.Height, width: $data.Width }, visible: $data.Visible">

This binding handler attribute maps to the property sheet of the extensible control and the height, width and visible properties:


To test this, we're going to set the height and width properties manually:


We're setting it to 1500x500 pixels, when we run the form again, this is the result:


X++ Interaction

The JavaScript inside the control can receive instructions from X++. The JavaScript can also send instructions to X++ and in this operation, receive JSON data from X++. It's a two-way street.

To demonstrate sending commands from X++ to JavaScript we're going to add code to the X++ runtime class (NAMNavigationMenuCtrl) and to JavaScript. The objective is that when a user clicks a button in the NATForm1 form a message box is displayed by the control.

1. Add this class level variable to the NAMNavigationMenuCtrl class:

private FormProperty m_dtSendCommandToJavaScript;

2. In the NAMNavigationMenuCtrl class constructor initialize the variable:

m_dtSendCommandToJavaScript = this.addProperty(methodstr(NAMNavigationMenuCtrl, SendCommandToJavaScript), Types::UtcDateTime);

3. Add this getter/setter to the NAMNavigationMenuCtrl:

    [FormPropertyAttribute(FormPropertyKind::Value, "SendCommandToJavaScript")]
    public utcdatetime SendCommandToJavaScript(utcdatetime _dtValue = m_dtSendCommandToJavaScript.parmValue())
    {
        _dtValue = DateTimeUtil::getSystemDateTime();
        m_dtSendCommandToJavaScript.setValueOrBinding(_dtValue);
        return m_dtSendCommandToJavaScript.parmValue();
    }

4. The NAMNavigationMenuCtrl class should now look like this:

[FormControlAttribute("NAMNavigationMenuCtrl","",classstr(NAMNavigationMenuCtrlBuild))]
class NAMNavigationMenuCtrl extends FormTemplateContainerControl
{
    private FormProperty m_dtSendCommandToJavaScript;
    protected void new(FormBuildControl _build, FormRun _formRun)
    {
        super(_build, _formRun);
        this.setResourceBundleName("/resources/html/NAMNavigationMenuCtrl");
        m_dtSendCommandToJavaScript = this.addProperty(methodstr(NAMNavigationMenuCtrl, SendCommandToJavaScript), Types::UtcDateTime);
    }
    [FormPropertyAttribute(FormPropertyKind::Value, "SendCommandToJavaScript")]
    public utcdatetime SendCommandToJavaScript(utcdatetime _dtValue = m_dtSendCommandToJavaScript.parmValue())
    {
        _dtValue = DateTimeUtil::getSystemDateTime();
        m_dtSendCommandToJavaScript.setValueOrBinding(_dtValue);
        return m_dtSendCommandToJavaScript.parmValue();
    }
}

5. In the JavaScript resource file add this code:

        var m_bSendCommandToJavaScript = false;
        $dyn.observe(this.SendCommandToJavaScript, function (_dtParam) {
            if (m_bSendCommandToJavaScript == true)
            {
                alert("SendCommandToJavaScript Invoked");
            }
            m_bSendCommandToJavaScript = true;         
        });

6. So that the JavaScript resource file will end up looking like this:

(function () {
    'use strict';
    $dyn.ui.defaults.NAMNavigationMenuCtrl = {};
    $dyn.controls.NAMNavigationMenuCtrl = function (data, element) {
        var self = this;
        $dyn.ui.Control.apply(self, arguments);
        $dyn.ui.applyDefaults(self, data, $dyn.ui.defaults.NAMNavigationMenuCtrl);
        $(element).css("background-color", "yellow");
        var m_bSendCommandToJavaScript = false;
        $dyn.observe(this.SendCommandToJavaScript, function (_dtParam) {
            if (m_bSendCommandToJavaScript == true)
            {
                alert("SendCommandToJavaScript Invoked");
            }
            m_bSendCommandToJavaScript = true;         
        });
    }
    $dyn.controls.NAMNavigationMenuCtrl.prototype = $dyn.extendPrototype($dyn.ui.Control.prototype, {});
})();

7. In the NATForm1 form set the NAMNavigationMenuCtrl1 to Auto Declaration = yes.


8. Create a button control in NATForm1 and set the text to "Click Me".


9. Override the clicked event and add this line of code:

NAMNavigationMenuCtrl1.SendCommandToJavaScript();

10. The NATForm1 class should now look like this:

[Form]
public class NATForm1 extends FormRun
{
    [Control("Button")]
    class FormButtonControl1
    {
        /// <summary>
        ///
        /// </summary>
        public void clicked()
        {
            NAMNavigationMenuCtrl1.SendCommandToJavaScript();
            super();
        }
    }
}

11. Press F5 to start debugging (or Debug -> Start Debugging). Every time you click on the "Click Me" button you will get a "SendCommandToJavaScript Invoked" message box.


In JavaScript the this.SendCommandToJavaScript variable is created because the "SendCommandToJavaScript" getter/setter in the X++ runtime class is exposed (public). The naming of the variable in JavaScript depends on the FormPropertyAttribute's second parameter. If you were to change the getter/setter attribute to this:

[FormPropertyAttribute(FormPropertyKind::Value, "SendCommandToJavaScriptAAAA")]

You would have to change the variable name in JavaScript to this.SendCommandToJavaScriptAAAA.

The framework's Interaction Service automatically synchronizes the JavaScript variables with the X++ runtime class properties with no intervention whatsoever.

The Extensible Control framework also follows what is called an observable pattern and that's what this JavaScript code is all about:

        $dyn.observe(this.SendCommandToJavaScript, function (_dtParam) {
        …
        });

JavaScript reacts to the changes of the value of the this.SendCommandToJavaScript variable and executes this code:

            if (m_bSendCommandToJavaScript == true)
            {
                alert("SendCommandToJavaScript Invoked");
            }
            m_bSendCommandToJavaScript = true;

Only when the this.SendCommandToJavaScript value changes. So now you're probably wondering what's the purpose of the m_bSendCommandToJavaScript variable. Its purpose is to prevent the code from firing when the control is being initialized. The value of this.SendCommandToJavaScript variable will inevitably change from nothing to something when being initialized. We only need to display the message box when the user clicks the button and not when the form is loading. You can try removing the variable and observing the effect.

And that finally takes us to the getter/setter and why we're using utcdatetime.

    public utcdatetime SendCommandToJavaScript(utcdatetime _dtValue = m_dtSendCommandToJavaScript.parmValue())
    {
        _dtValue = DateTimeUtil::getSystemDateTime();
        m_dtSendCommandToJavaScript.setValueOrBinding(_dtValue);
        return m_dtSendCommandToJavaScript.parmValue();
    }

Every time this getter/setter is called the system date/time will inevitably change and the observable in JavaScript will be triggered.

This is not the intended use of observable properties. But they are useful when we want to trigger an event from X++. We will now go into the original intended usage of observable properties.

In NATForm1 we want to have a FormMenuButtonControl to allow the user to change the background color of the navigation menu control.


1. If you want to avoid having unnecessary code erase the FormProperty, the FormProperty initialization and the getter/setter from the X++ runtime class from the previous demonstration. Also erase the observable function from JavaScript and the "Click Me" button in the form.

2. Erase the following line from the htm resource file:

<p>This is the NAMNavigationMenuCtrl</p>

3. Erase this line in the JavaScript resource:

$(element).css("background-color", "yellow");

4. And replace it with this:

$(element).css("border", "1px solid black");
$(element).css("position", "relative");

5. Create this class level variable in the X++ runtime class:

private FormProperty m_iBackgroundColor;

6. Initialize the FormProperty variable in the X++ runtime class constructor:

m_iBackgroundColor = this.addProperty(methodstr(NAMNavigationMenuCtrl, BackgroundColor), Types::Integer);

7. Add this getter/setter method to the X++ runtime class:

    [FormPropertyAttribute(FormPropertyKind::Value, "BackgroundColor")]
    public int BackgroundColor(int _iValue = m_iBackgroundColor.parmValue())
    {
        m_iBackgroundColor.setValueOrBinding(_iValue);
        return m_iBackgroundColor.parmValue();
    }

8. Add this function to the JavaScript resource:

        $dyn.observe(this.BackgroundColor, function (_iParam) {
            switch (_iParam)
            {
                case 0: //Gray
                    $(element).css("background-color", "#EAEAEA");
                    break;
                case 1: //Light blue
                    $(element).css("background-color", "#C7E0F4");
                    break;
                case 2: //Blue
                    $(element).css("background-color", "#002050");
                    break;
                case 3: //Black
                    $(element).css("background-color", "#000000");
                    break;
            }
        });

9. Open NATForm1, create an Action Pane Control, a Button Group, a Menu Button and four Button controls:


10. For the Menu Button control set the text property to "Background color".


11. For the Button controls set the Text property to "Gray", "Light blue", "Blue" and "Black" beginning from the top and going downwards.


12. Override the clicked event for the four button controls and include the following code, beginning at 0 for grey and ending at 3 for black (1 for light blue and 2 for blue):

        public void clicked()
        {
            NAMNavigationMenuCtrl1.BackgroundColor(0);
            super();
        }

13. Press F5 or Debug -> Start Debugging and using the "Background color" menu change the background color of the control.

Loading Data into The Control Using JSON

Properties can be used to pass scalar variables of data between X++ and the control. But what if you need an array or a matrix (table)? In the Navigation Menu Control the requirement is that for each tile the X & Y position, the size, the caption, the display menu item be stored in a table and then passed at runtime. For the purposes of this tutorial we won't actually use a table, but we will pretend that the data came from a table.

To pass information from JavaScript to X++ and back Extensible Controls use JSON. JSON is short for JavaScript Object Notation and it can be thought of as a lightweight version of XML. Dynamics 365 for Finance and Operations has its own JSON serializer inside the class FormJsonSerializer.

So that we can easily serialize to JSON we have to create two new classes and both are extensions of FormDataContract. The first one is NAMTiles which has a method of type List and can hold several objects of type NAMTile which is our second class. We create several NAMTile objects, group them together in NAMTiles and with a single line of X++ code we serialize and get our JSON string which we then send back over the wire. When it reaches the JavaScript we deserialize with a single line of JS code and start building the tiles inside the control.

1. Create a class called NAMTile. This class represents one tile. Include the following code inside this class:

[DataContractAttribute]
class NAMTile extends FormDataContract
{
    FormProperty m_iTop;
    FormProperty m_iLeft;
    FormProperty m_iTileType;
    FormProperty m_sCaption;
    FormProperty m_sMenu;
    public void new()
    {
        super();
        m_iTop = this.properties().addProperty(methodStr(NAMTile, Top), Types::Integer);
        m_iLeft = this.properties().addProperty(methodStr(NAMTile, Left), Types::Integer);
        m_iTileType = this.properties().addProperty(methodStr(NAMTile, TileType), Types::Integer);
        m_sCaption = this.properties().addProperty(methodStr(NAMTile, Caption), Types::String);
        m_sMenu = this.properties().addProperty(methodStr(NAMTile, Menu), Types::String);
    }
    [DataMemberAttribute("Top")]
    public int Top(int _value = m_iTop.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_iTop.parmValue(_value);
        }
        return _value;
    }
    [DataMemberAttribute("Left")]
    public int Left(int _value = m_iLeft.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_iLeft.parmValue(_value);
        }
        return _value;
    }
    [DataMemberAttribute("TileType")]
    public int TileType(int _value = m_iTileType.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_iTileType.parmValue(_value);
        }
        return _value;
    }
    [DataMemberAttribute("Caption")]
    public str Caption(str _value = m_sCaption.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_sCaption.parmValue(_value);
        }
        return _value;
    }
    [DataMemberAttribute("Menu")]
    public str Menu(str _value = m_sMenu.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_sMenu.parmValue(_value);
        }
        return _value;
    }
}

2. Create a class called NAMTiles. This class represents a collection of tiles. Include the following code inside this class:

[DataContractAttribute]
class NAMTiles extends FormDataContract
{
    FormProperty m_oTileList;
    public void new()
    {
        super();
        m_oTileList = this.properties().addProperty(methodStr(NAMTiles, TileList), Types::Class);
        m_oTileList.parmValue(new List(Types::Class));
    }
    [DataMemberAttribute('TileList'), DataCollectionAttribute(Types::Class, classStr(NAMTile))]
    public List TileList(List _value = m_oTileList.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_oTileList.parmValue(_value);
        }
        return _value;
    }
}

3. Create a method in the X++ runtime class. This method will be called by JavaScript and will return the list of tiles back to JavaScript in JSON format. It does not have any parameters but methods can have parameters of type str, int, etc.

    [FormCommandAttribute("GetTiles")]
    public str GetTiles()
    {
        str sReturn;
        sReturn = FormJsonSerializer::serializeClass(m_oTiles);
        print "sReturn: ", sReturn;
        return sReturn;
    }

4. Modify the init method of the NATForm1 form. Data is hard coded but it could be modified to load from a table.

    public void init()
    {
        super();
        NAMTile oTile;
        int iLeft = 50;
        int iSpacing = 7;
        int iBigWidth = 182;
        int iSmallWidth = 80;
        oTile = new NAMTile();
        oTile.Top(50);
        oTile.Left(iLeft);
        oTile.TileType(2);
        oTile.Caption(SysLabel::labelId2String(literalstr('@SYS336236')));
        oTile.Menu("VendTableListPage");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iBigWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(50);
        oTile.Left(iLeft);
        oTile.TileType(3);
        oTile.Caption(SysLabel::labelId2String(literalstr('@SYS117453')));
        oTile.Menu("VendTableHoldListPage");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iSmallWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(50);
        oTile.Left(iLeft);
        oTile.TileType(4);
        oTile.Caption(SysLabel::labelId2String(literalstr('@SYS117454')));
        oTile.Menu("VendTablePastDueListPage");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iSmallWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(50);
        oTile.Left(iLeft);
        oTile.TileType(3);
        oTile.Caption(SysLabel::labelId2String(literalstr('@SYS191281')));
        oTile.Menu("VendTableDiverseListPage");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iSmallWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(50);
        oTile.Left(iLeft);
        oTile.TileType(4);
        oTile.Caption(SysLabel::labelId2String(literalstr('@SYS132272')));
        oTile.Menu("VendExceptionGroup");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iSmallWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(50);
        oTile.Left(iLeft);
        oTile.TileType(3);
        oTile.Caption(SysLabel::labelId2String(literalstr('@SYS10022')));
        oTile.Menu("VendGroup");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iSmallWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(50);
        oTile.Left(iLeft);
        oTile.TileType(4);
        oTile.Caption(SysLabel::labelId2String(literalstr('@SYS114469')));
        oTile.Menu("VendPriceToleranceGroup");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        //Purchase orders
        iLeft = 50;
        oTile = new NAMTile();
        oTile.Top(137);
        oTile.Left(iLeft);
        oTile.TileType(1);
        oTile.Caption(SysLabel::labelId2String(literalstr('@SYS336224')));
        oTile.Menu("PurchTableListPage");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iBigWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(137);
        oTile.Left(iLeft);
        oTile.TileType(4);
        oTile.Caption(SysLabel::labelId2String(literalstr('@SYS302184')));
        oTile.Menu("PurchTableListPageAssignedToMe");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iSmallWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(137);
        oTile.Left(iLeft);
        oTile.TileType(3);
        oTile.Caption(SysLabel::labelId2String(literalstr('@SYS120064')));
        oTile.Menu("PurchTableReceivedNotInvoicedListPage");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iSmallWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(137);
        oTile.Left(iLeft);
        oTile.TileType(4);
        oTile.Caption(SysLabel::labelId2String(literalstr('@AccountsPayable:PurchaseAgreements')));
        oTile.Menu("PurchAgreementListPage");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iSmallWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(137);
        oTile.Left(iLeft);
        oTile.TileType(3);
        oTile.Caption(SysLabel::labelId2String(literalstr('@GLS109233')));
        oTile.Menu("PlSADTable");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
        iLeft = iLeft + iSmallWidth + iSpacing;
        oTile = new NAMTile();
        oTile.Top(137);
        oTile.Left(iLeft);
        oTile.TileType(4);
        oTile.Caption(SysLabel::labelId2String(literalstr('@AccountsPayable:OpenPrepayments')));
        oTile.Menu("PurchPrepayOpen");
        NAMNavigationMenuCtrl1.Tiles().TileList().AddEnd(oTile);
    }

5. Add the following code to JavaScript after the $dyn.observe function. The m_AddTilesToControl function is responsible for adding the Tiles to the control as soon as GetTiles returns. This function gets a JSON string from GetTiles in X++. The $dyn.callFunction line is responsible for calling the X++ GetTiles method in the X++ runtime class, passing the parameters (empty in this case) and setting the callback function (m_AddTilesToControl).

        self.m_AddTilesToControl = function (_sJSON)
        {
            var oTiles = $.parseJSON(_sJSON, true);
            var i = 0;
            for (i = 0; i <= oTiles.TileList.length - 1; i++)
            {
                var oTile = oTiles.TileList[i];
                var sClass = "tile-big-dark";
                var sSpanClass = "tile-text-big";
                switch(oTile.TileType)
                {
                    case 1:
                        sClass = "tile-big-dark";
                        sSpanClass = "tile-text-big";
                        break;
                    case 2:
                        sClass = "tile-big-light";
                        sSpanClass = "tile-text-big";
                        break;
                    case 3:
                        sClass = "tile-small-dark";
                        sSpanClass = "tile-text-small";
                        break;
                    case 4:
                        sClass = "tile-small-light";
                        sSpanClass = "tile-text-small";
                        break;
                }
                $(element).append("<div class='" + sClass + "' style='left:" + oTile.Left.toString() + "px;top:" + oTile.Top.toString() + "px;'><span class='" + sSpanClass + "'>" + oTile.Caption + "</span></div>");
                console.log("passing");
            }
        }
        var oParams = {};
        $dyn.callFunction(self.GetTiles, self, oParams, self.m_AddTilesToControl);

6. Include these style definitions in the NAMNavigationMenuCtrl.css file:

.tile-big-dark {
    width: 182px;
    height: 80.5px;
    position: absolute;
    background-color: #002050;
}
.tile-big-light {
    width: 182px;
    height: 80.5px;
    position: absolute;
    background-color: #0d62aa;
}
.tile-small-dark {
    width: 80.5px;
    height: 80.5px;
    position: absolute;
    background-color: #002050;
}
.tile-small-light {
    width: 80.5px;
    height: 80.5px;
    position: absolute;
    background-color: #0d62aa;
}
.tile-text-big {
    color: #fff;
    max-width: 180.5px;
    position: absolute;
    bottom: 0;
    left: 0;
    font: normal normal 300 12px/14px 'Segoe UI',tahoma,sans-serif;
    max-height: 35px;
    line-height: 14.1px;
    overflow: hidden;
    margin: 7px 0 2px 0;
    padding: 7px;
    -webkit-line-clamp: 3;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
}
.tile-text-small {
    color: #fff;
    max-width: 78.5px;
    position: absolute;
    bottom: 0;
    left: 0;
    font: normal normal 300 12px/14px 'Segoe UI',tahoma,sans-serif;
    max-height: 35px;
    line-height: 14.1px;
    overflow: hidden;
    margin: 7px 0 2px 0;
    padding: 7px;
    -webkit-line-clamp: 3;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
}

7. Press F5 to debug or Debug -> Start Debugging. This is now the new appearance of the control:


Sending Commands From JavaScript to X++

The final part of this tutorial will show you how to invoke actions in X++ code triggered by user actions in the control. In the Navigation Menu control whenever a user clicks on a tile the form that the tile represents will open.

1. Create a global object outside the main JavaScript function. The reason we're creating it outside is because it will contain the click event handler that will be called by the tiles and has to be outside of the scope of the main function.

GlobalObj = {};
(function () {
…
})();

2. Create this JavaScript function immediately after the $dyn.observe function. This function will handle the onclick event from any tile and call the OpenForm method on the X++ runtime class. It can be triggered by the tile itself or by the child span object, so it first checks if the object has an id, if it does not then it is a span object and we need to reference the parent object instead.

        GlobalObj.m_OnClick = function ()
        {
            var oSource = event.srcElement;
            if (oSource.id == "") {
                oSource = oSource.parentElement;
            }
            var oParams = { _sMenu: oSource.id };
            $dyn.callFunction(self.OpenForm, self, oParams);
        }

3. Modify the append method in JavaScript to make the name of the display menu the id for the div and to create the onclick method handler.

$(element).append("<div id='" + oTile.Menu + "' onclick='GlobalObj.m_OnClick();' class='" + sClass + "' style='left:" + oTile.Left.toString() + "px;top:" + oTile.Top.toString() + "px;'><span class='" + sSpanClass + "'>" + oTile.Caption + "</span></div>");

4. Create this method in the X++ runtime class:

    [FormCommandAttribute("OpenForm")]
    public void OpenForm(str _sMenu)
    {
        Args args;
        args = new Args();
        args.caller(this);
        new MenuFunction(_sMenu, MenuItemType::Display).run(args);
    }

5. Press F5 or Debug -> Start Debugging, click on any tile and the associated form will be opened.

Additional Notes

1. Although it isn't demonstrated in this tutorial, it is possible to initiate a call from X++ like in the SendCommandToJavaScript example, and then have the JavaScript execute a $dyn.callFunction in the $dyn.observe function and have the control receive JSON data from the X++ runtime class.

2. Sometimes when calling the $dyn.callFunction method the call will not execute:

Call will not execute during an intreaction

Because the framework is in an interaction. This generally happens when $dyn.callFunction calls are deeply nested in code. The framework will however alert you via the browser's console. When debugging it is always important to monitor the browser's console (you can do this by pressing the F12 key in most browsers). In those cases you must "yield" the JavaScript code. When you "yield" a call you do not call the function directly, you call it via window.setTimeout and you convert a synchronous call into an asynchronous one and the call will then execute when the framework is ready. This is a simple example:

        m_YieldCallFunction = function () {
            var oParams = { };
            $dyn.callFunction(self.SomeMethod, self, oParams);
        }
        window.setTimeout(m_YieldCallFunction, 0);

This article has a more thorough explanation of these two concepts:

Dynamics 365 for Finance and Operations Extensible Controls: Server Side to Client Side and Back.


Appendix A - NAMNavigationMenuCtrl Final Code

[FormControlAttribute("NAMNavigationMenuCtrl","",classstr(NAMNavigationMenuCtrlBuild))]
class NAMNavigationMenuCtrl extends FormTemplateContainerControl
{
    private FormProperty m_iBackgroundColor;
    private NAMTiles m_oTiles;
    protected void new(FormBuildControl _build, FormRun _formRun)
    {
        super(_build, _formRun);
        this.setResourceBundleName("/resources/html/NAMNavigationMenuCtrl");
        m_iBackgroundColor = this.addProperty(methodstr(NAMNavigationMenuCtrl, BackgroundColor), Types::Integer);
        m_oTiles = new NAMTiles();
    }
    [FormPropertyAttribute(FormPropertyKind::Value, "BackgroundColor")]
    public int BackgroundColor(int _iValue = m_iBackgroundColor.parmValue())
    {
        m_iBackgroundColor.setValueOrBinding(_iValue);
        return m_iBackgroundColor.parmValue();
    }
    public NAMTiles Tiles()
    {
        return m_oTiles;
    }
    [FormCommandAttribute("GetTiles")]
    public str GetTiles()
    {
        str sReturn;
        sReturn = FormJsonSerializer::serializeClass(m_oTiles);
        print "sReturn: ", sReturn;
        return sReturn;
    }
    [FormCommandAttribute("OpenForm")]
    public void OpenForm(str _sMenu)
    {
        Args args;
        args = new Args();
        args.caller(this);
        new MenuFunction(_sMenu, MenuItemType::Display).run(args);
    }
}

Appendix B - NAMNavigationMenuCtrlBuild Final Code

[FormDesignControlAttribute("NAMNavigationMenuCtrl")]
class NAMNavigationMenuCtrlBuild extends FormBuildContainerControl
{
}

Appendix C - NAMNavigationMenuCtrlBuild.css Final Code

.tile-big-dark {
    width: 182px;
    height: 80.5px;
    position: absolute;
    background-color: #002050;
}
.tile-big-light {
    width: 182px;
    height: 80.5px;
    position: absolute;
    background-color: #0d62aa;
}
.tile-small-dark {
    width: 80.5px;
    height: 80.5px;
    position: absolute;
    background-color: #002050;
}
.tile-small-light {
    width: 80.5px;
    height: 80.5px;
    position: absolute;
    background-color: #0d62aa;
}
.tile-text-big {
    color: #fff;
    max-width: 180.5px;
    position: absolute;
    bottom: 0;
    left: 0;
    font: normal normal 300 12px/14px 'Segoe UI',tahoma,sans-serif;
    max-height: 35px;
    line-height: 14.1px;
    overflow: hidden;
    margin: 7px 0 2px 0;
    padding: 7px;
    -webkit-line-clamp: 3;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
}
.tile-text-small {
    color: #fff;
    max-width: 78.5px;
    position: absolute;
    bottom: 0;
    left: 0;
    font: normal normal 300 12px/14px 'Segoe UI',tahoma,sans-serif;
    max-height: 35px;
    line-height: 14.1px;
    overflow: hidden;
    margin: 7px 0 2px 0;
    padding: 7px;
    -webkit-line-clamp: 3;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-box-orient: vertical;
}

Appendix D - NAMNavigationMenuCtrlBuild.htm Final Code

<script src="/resources/scripts/NAMNavigationMenuCtrl.js"></script>
<link href="/resources/styles/NAMNavigationMenuCtrl.css" rel="stylesheet" type="text/css" />
<div id="NAMNavigationMenuCtrl" data-dyn-bind="sizing: { height: $data.Height, width: $data.Width }, visible: $data.Visible">
</div>

Appendix E - NAMNavigationMenuCtrlBuild.js Final Code

GlobalObj = {};
(function () {
    'use strict';
    $dyn.ui.defaults.NAMNavigationMenuCtrl = {};
    $dyn.controls.NAMNavigationMenuCtrl = function (data, element) {
        var self = this;
        $dyn.ui.Control.apply(self, arguments);
        $dyn.ui.applyDefaults(self, data, $dyn.ui.defaults.NAMNavigationMenuCtrl);
        $(element).css("border", "1px solid black");
        $(element).css("position", "relative");
        $dyn.observe(this.BackgroundColor, function (_iParam) {
            switch (_iParam)
            {
                case 0: //Gray
                    $(element).css("background-color", "#EAEAEA");
                    break;
                case 1: //Light blue
                    $(element).css("background-color", "#C7E0F4");
                    break;
                case 2: //Blue
                    $(element).css("background-color", "#002050");
                    break;
                case 3: //Black
                    $(element).css("background-color", "#000000");
                    break;
            }
        });
        GlobalObj.m_OnClick = function ()
        {
            var oSource = event.srcElement;
            if (oSource.id == "") {
                oSource = oSource.parentElement;
            }
            var oParams = { _sMenu: oSource.id };
            $dyn.callFunction(self.OpenForm, self, oParams);
        }
        self.m_AddTilesToControl = function (_sJSON)
        {
            var oTiles = $.parseJSON(_sJSON, true);
            var i = 0;
            for (i = 0; i <= oTiles.TileList.length - 1; i++)
            {
                var oTile = oTiles.TileList[i];
                var sClass = "tile-big-dark";
                var sSpanClass = "tile-text-big";
                switch(oTile.TileType)
                {
                    case 1:
                        sClass = "tile-big-dark";
                        sSpanClass = "tile-text-big";
                        break;
                    case 2:
                        sClass = "tile-big-light";
                        sSpanClass = "tile-text-big";
                        break;
                    case 3:
                        sClass = "tile-small-dark";
                        sSpanClass = "tile-text-small";
                        break;
                    case 4:
                        sClass = "tile-small-light";
                        sSpanClass = "tile-text-small";
                        break;
                }
                $(element).append("<div id='" + oTile.Menu + "' onclick='GlobalObj.m_OnClick();' class='" + sClass + "' style='left:" + oTile.Left.toString() + "px;top:" + oTile.Top.toString() + "px;'><span class='" + sSpanClass + "'>" + oTile.Caption + "</span></div>");
            }
        }
        var oParams = {};
        $dyn.callFunction(self.GetTiles, self, oParams, self.m_AddTilesToControl);
    }
    $dyn.controls.NAMNavigationMenuCtrl.prototype = $dyn.extendPrototype($dyn.ui.Control.prototype, {});
})();

Appendix F - NAMTile Final Code

[DataContractAttribute]
class NAMTile extends FormDataContract
{
    FormProperty m_iTop;
    FormProperty m_iLeft;
    FormProperty m_iTileType;
    FormProperty m_sCaption;
    FormProperty m_sMenu;
    public void new()
    {
        super();
        m_iTop = this.properties().addProperty(methodStr(NAMTile, Top), Types::Integer);
        m_iLeft = this.properties().addProperty(methodStr(NAMTile, Left), Types::Integer);
        m_iTileType = this.properties().addProperty(methodStr(NAMTile, TileType), Types::Integer);
        m_sCaption = this.properties().addProperty(methodStr(NAMTile, Caption), Types::String);
        m_sMenu = this.properties().addProperty(methodStr(NAMTile, Menu), Types::String);
    }
    [DataMemberAttribute("Top")]
    public int Top(int _value = m_iTop.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_iTop.parmValue(_value);
        }
        return _value;
    }
    [DataMemberAttribute("Left")]
    public int Left(int _value = m_iLeft.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_iLeft.parmValue(_value);
        }
        return _value;
    }
    [DataMemberAttribute("TileType")]
    public int TileType(int _value = m_iTileType.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_iTileType.parmValue(_value);
        }
        return _value;
    }
    [DataMemberAttribute("Caption")]
    public str Caption(str _value = m_sCaption.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_sCaption.parmValue(_value);
        }
        return _value;
    }
    [DataMemberAttribute("Menu")]
    public str Menu(str _value = m_sMenu.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_sMenu.parmValue(_value);
        }
        return _value;
    }
}

Appendix G - NAMTiles Final Code

[DataContractAttribute]
class NAMTiles extends FormDataContract
{
    FormProperty m_oTileList;
    public void new()
    {
        super();
        m_oTileList = this.properties().addProperty(methodStr(NAMTiles, TileList), Types::Class);
        m_oTileList.parmValue(new List(Types::Class));
    }
    [DataMemberAttribute('TileList'), DataCollectionAttribute(Types::Class, classStr(NAMTile))]
    public List TileList(List _value = m_oTileList.parmValue())
    {
        if(!prmisDefault(_value))
        {
            m_oTileList.parmValue(_value);
        }
        return _value;
    }
}

Appendix F - All Project Files in .axpp format

Click Here to Download Navigation Menu AXPP files

Dynamics 365 F&O Development Services

Does your dev team have too much on their plate? We can help by handling big or small Dynamics 365 customization projects at a competitive rate. Click here.

File based integration system for Dynamics 365 Finance and Operations

The Atlantic Oak Document Exchange System allows your Dynamics 365 for Finance and Supply Chain Management system to directly pull or push XML or flat files to and from external systems via SFTP, FTP, FTPS, Azure Storage, Azure Files and other file servers. Click here.

×