11 min read

Introduction

Normally, forms are created using AOT by creating a form object and adding form controls like tabs, tab pages, grids, groups, data fields, images, and other. Form behavior is controlled by its properties or the code in its member methods. The behavior and layout of form controls are also controlled by their properties and the code in their member methods. Although it is very rare, forms can also be created dynamically from code.

In this article, we will cover various aspects of using Dynamics AX forms. We start with building Dynamics AX dialogs, which actually are dynamic forms, and explain how to handle their events. The article will also show how to add dynamic controls to existing forms, how to build dynamic forms from scratch, how to make forms modal, and how to change the appearance of all application forms with a few lines of code.

This article also discusses the usage of splitters, tree controls, creating checklists, saving last user selections, modifying application version, and other things.

Creating Dialogs

Dialogs are a way to present users with a simple input form. They are commonly used for small user tasks like filling in report values, running batch jobs, presenting only the most important fields to the user when creating a new record, etc. Dialogs are normally created from X++ code without storing actual layout in AOT.

The application class Dialog is used to build dialogs. Other application classes like DialogField, DialogGroup , DialogTabPage, and so on, are used to create dialog controls. One of the common ways is to use dialogs within RunBase framework classes that need user input.

In this example, we will see how to build a dialog from code using the RunBase framework class. The dialog will contain customer table fields shown in different groups and tabs for creating a new record. There will be two tab pages, General and Details. The first page will have Customer account and Name input controls. The second page will be divided into two groups, Setup and Payment, with relevant fields. The actual record will not be created, as it is out of scope of this example. However, for demonstration purposes, the information specified by the user will be displayed in the Infolog.

How to do it…

  • Open AOT, and create a new class CustCreate with the following code:
  • class CustCreate extends RunBase
    {
    DialogField fieldAccount;
    DialogField fieldName;
    DialogField fieldGroup;
    DialogField fieldCurrency;
    DialogField fieldPaymTermId;
    DialogField fieldPaymMode;
    CustAccount custAccount;
    CustName custName;
    CustGroupId custGroupId;
    CurrencyCode currencyCode;
    CustPaymTermId paymTermId;
    CustPaymMode paymMode;
    }
    public container pack()
    {
    return connull();
    }
    public boolean unpack(container packedClass)
    {
    return true;
    }
    protected Object dialog()
    {
    Dialog dialog;
    DialogTabPage tabGeneral;
    DialogTabPage tabDetails;
    DialogGroup groupCustomer;
    DialogGroup groupPayment;
    ;

    dialog = super();
    dialog.caption("Customer information");
    tabGeneral = dialog.addTabPage("General");
    fieldAccount = dialog.addField(
    typeid(CustVendAC),
    "Customer account");
    fieldName = dialog.addField(typeid(CustName));
    tabDetails = dialog.addTabPage("Details");
    groupCustomer = dialog.addGroup("Setup");
    fieldGroup = dialog.addField(typeid(CustGroupId));
    fieldCurrency = dialog.addField(typeid(CurrencyCode));
    groupPayment = dialog.addGroup("Payment");
    fieldPaymTermId = dialog.addField(typeid(CustPaymTermId));
    fieldPaymMode = dialog.addField(typeid(CustPaymMode));
    return dialog;
    }
    public boolean getFromDialog()
    {;
    custAccount = fieldAccount.value();
    custName = fieldName.value();
    custGroupId = fieldGroup.value();
    currencyCode = fieldCurrency.value();
    paymTermId = fieldPaymTermId.value();
    paymMode = fieldPaymMode.value();
    return true;
    }
    public void run()
    {;
    info("You have entered customer information:");
    info(strfmt("Account: %1", custAccount));
    info(strfmt("Name: %1", custName));
    info(strfmt("Group: %1", custGroupId));
    info(strfmt("Currency: %1", currencyCode));
    info(strfmt("Terms of payment: %1", paymTermId));
    info(strfmt("Method of payment: %1", paymMode));
    }
    static void main(Args _args)
    {
    CustCreate custCreate = new CustCreate();
    ;

    if (custCreate.prompt())
    {
    custCreate.run();
    }
    }
  • To test the dialog, run the class. The following form should appear with the General tab page open initially:
  • Microsoft Dynamics AX 2009 Development Cookbook

  • When you click on the Details tab page, you will see the following screen:
  • Microsoft Dynamics AX 2009 Development Cookbook

  • Enter some information into the fields, and click OK. The results are displayed in the Infolog:
  • Microsoft Dynamics AX 2009 Development Cookbook

    How it works…

    Firstly, we create a new class CustCreate. By extending it from RunBase, we utilize the standard approach of running such kinds of dialogs. RunBase will also automatically add the required buttons to the dialog. Then we declare class member variables, which will be used later. DialogField type variables are actual user input fields. The rest are used to store the values returned from user input.

    The pack() and unpack() methods are normally used to convert an object to a container, which is a format to store an object in the user cache (SysLastValue) or to transfer it between Server and Client tiers. RunBase requires those two methods to be present in all its subclasses. In this example, we are not using any of the pack()/unpack() features, but because those methods are mandatory, we return an empty container from pack() and true from unpack().

    The layout of the actual dialog is constructed in the dialog() member method. Here, we define local variables for the dialog itself, tab pages, and groups. Those variables, as opposed to the dialog fields, do not store any value to be processed further.

    The super() of the RunBase framework creates the initial dialog object for us. The object is created using the Dialog application class. The class actually uses the Dynamics AX form as a base, automatically adds the relevant controls, including OK and Cancel buttons, and presents it to the user as a dialog.

    Additional dialog controls are added to the dialog by using the addField(), addGroup(), and addTabPage() methods . There are more methods to add different types of controls like addText(), addImage(), addMenuItemButton(), and others. All controls have to be added to the dialog object directly. Adding an input control to groups or tabs is done by calling addField() right after addGroup() or addTabPage(). In the example above, we add tab pages, groups, and fields in logical sequence, so every control appears in the right position.

    The method returns a prepared dialog object for further processing.

    Values from the dialog controls are assigned to variables by calling the value() member method of DialogField. If a dialog is used within the RunBase framework, as in this example, the best place to assign dialog control values to variables is the getFormDialog() member method. RunBase calls this method right after the user clicks OK.

    The main processing is done in run(). For demonstration purposes, this example contains only variable output to Infolog.

    In order to make this class runable, the static method main() has to be created. Here, we create a new CustCreate object, invoke user dialog by calling prompt(), and once the user finishes entering customer details by clicking OK, we call run() to process the data.

    Handling dialog events

    Sometimes, the user interface requires us to change the status of a field, depending on the status of another field. For example, if the user marks the Show filter checkbox, another field, Filter, appears or becomes enabled. In standard Dynamics AX forms, this can be done using input control event modified() . But sometimes such features are required on dialogs where handling events is not that straightforward.

    Very often, I find myself in a situation where existing dialogs need to be adjusted to support events. The easiest way of doing that is of course to build a form in AOT, which will replace the original dialog. But in cases when the existing dialog is complex enough, probably a more cost effective solution would be to implement dialog event handling. It is not as flexible as AOT forms, but in most cases it does the job.

    In this recipe, we will create a dialog very similar to the previous one, but instead of entering the customer number, we will be able to select it from the list. Once the customer is selected, the rest of the fields will be filled automatically by the system from the customer record.

    How to do it…

    1. In AOT, create a new class named CustSelect with the following code:
        class CustSelect extends RunBase
        {
        DialogField fieldAccount;
        DialogField fieldName;
        DialogField fieldGroup;
        DialogField fieldCurrency;
        DialogField fieldPaymTermId;
        DialogField fieldPaymMode;
        }
        public container pack()
        {
        return connull();
        }
        public boolean unpack(container packedClass)
        {
        return true;
        }
        protected Object dialog()
        {
        Dialog dialog;
        DialogTabPage tabGeneral;
        DialogTabPage tabDetails;
        DialogGroup groupCustomer;
        DialogGroup groupPayment;
        ;

        dialog = super();
        dialog.caption("Customer information");
        dialog.allowUpdateOnSelectCtrl(true);
        tabGeneral = dialog.addTabPage("General");
        fieldAccount = dialog.addField(
        typeid(CustAccount),
        "Customer account");
        fieldName = dialog.addField(typeid(CustName));
        fieldName.enabled(false);
        tabDetails = dialog.addTabPage("Details");
        groupCustomer = dialog.addGroup("Setup");
        fieldGroup = dialog.addField(typeid(CustGroupId));
        fieldCurrency = dialog.addField(typeid(CurrencyCode));
        fieldGroup.enabled(false);
        fieldCurrency.enabled(false);
        groupPayment = dialog.addGroup("Payment");
        fieldPaymTermId = dialog.addField(typeid(CustPaymTermId));
        fieldPaymMode = dialog.addField(typeid(CustPaymMode));
        fieldPaymTermId.enabled(false);
        fieldPaymMode.enabled(false);
        return dialog;
        }
        public void dialogSelectCtrl()
        {
        CustTable custTable;
        ;

        custTable = CustTable::find(fieldAccount.value());
        fieldName.value(custTable.Name);
        fieldGroup.value(custTable.CustGroup);
        fieldCurrency.value(custTable.Currency);
        fieldPaymTermId.value(custTable.PaymTermId);
        fieldPaymMode.value(custTable.PaymMode);
        }
        static void main(Args _args)
        {
        CustSelect custSelect = new CustSelect();
        ;

        if (CustSelect.prompt())
        {
        CustSelect.run();
        }
        }

    2. Run the class, select any customer from the list, and move the cursor to the next control. Notice how the rest of the fields were populated automatically with the customer information:
    3. Microsoft Dynamics AX 2009 Development Cookbook

    4. When you click on Details tab page, you will see the details as in following screenshot:
    5. Microsoft Dynamics AX 2009 Development Cookbook

    How it works…

    The new class CustSelect is a copy of CustCreate from the previous recipe with few changes. In its declaration, we leave all DialogField declarations. We remove all other variables apart from Customer account. The Customer account input control is the only editable field on the dialog, so we have to keep it for storing its value.

    The methods pack()/unpack() remain the same as we are not using any of their features.

    In the dialog() member method, we call allowUpdateOnSelect() with the argument true to enable input control event handling. We also disable all fields apart from Customer account by calling enable() with parameter false for every field.

    The member method dialogSelectCtrl() of the RunBase class is called every time the user modifies any input control in the dialog. It is the place where we have to add all the required code to ensure that, in our case, all controls are populated with the correct data from the customer record, once the Customer account is chosen.

    Static main() method ensures that we can run this class.

    There’s more…

    Usage of dialogSelectCtrl() sometimes might appear a bit limited as this method is only invoked when the dialog control loses its focus. No other events can be controlled, and it can become messy if more controls needs to be processed. Actually, this method is called from the selectControl() of the form, which is used as a base for the dialog.

    As mentioned earlier, dialogs created using the Dialog class are actually forms, which are dynamically created during runtime. So in order to extend event handling functionality on dialogs, we should utilize form event handling features.

    The Dialog class does not provide direct access to form event handling functions, but we can easily access the form object within the dialog. Although we cannot create the usual event handling methods on runtime form controls, we can override this behavior. Let’s modify the previous example to include more events. We will add an event on the second tab page, which is triggered once the page is activated. First, we have to override the dialogPostRun() method on the CustSelect class:

    public void dialogPostRun(DialogRunbase dialog)
    {;
    dialog.formRun().controlMethodOverload(true);
    dialog.formRun().controlMethodOverloadObject(this);
    super(dialog);
    }

    Here, we enable event overriding on the form after it is fully created and is ready for displaying on the screen. We also pass the CustSelect object as argument for the controlMethodOverloadObject() to make sure that form “knows” where overridden events are located.

    Next, we have to create the method that overrides the tab page event:

    void TabPg_2_pageActivated()
    {;
    info('Tab page activated');
    }

    The method name consists of the control name and event name joined with an underscore. But before creating such methods, we first have to get the name of the runtime control. This is because the dialog form is created dynamically, and Dynamics AX defines control names automatically without allowing the user to choose them. In this example, I have temporary added the following code to the bottom of dialog(), which displayed the name of the Details tab page control when the class was executed:

    info(tabDetails.name());

    Now, run the class again, and select the Details tab page. The message should be displayed in the Infolog.

    Creating dynamic menu buttons

    Normally, Dynamics AX forms are created in AOT by adding various controls to the form’s design and do not change during runtime. But besides that, Dynamics AX allows developers to add controls dynamically during form runtime.

    Probably, you have already noticed that the Document handling form in the standard Dynamics AX application has a nice option to create a new record by clicking the New button and selecting the desired document type from the list. This feature does not add any new functionality to the application, but it provides an alternative way of quickly creating a new record and it makes the form more user-friendly. The content of this button is actually generated dynamically during the initialization of the form and may vary depending on the document handling setup.

    There might be other cases when such features can be used. For example, dynamic menu buttons could be used to display a list of statuses, which depends on the type of the selected record.

    In this recipe, we will explore the code behind this feature. As an example, we will modify the Ledger budget button on the Chart of accounts form to display a list of available budget models relevant only for the selected ledger account. That means the list is going to be generated dynamically and may be different for different accounts.

    LEAVE A REPLY

    Please enter your comment!
    Please enter your name here