Working with Forms in Dynamics AX: Part 2

0
165
13 min read

Adding form splitters

Commonly used forms like Sales orders or Projects in Dynamics AX have multiple grids. Normally, one grid is in the upper section and another one is in the bottom section of the form. Sometimes grids are placed next to each other.

The size of the data in each grid may vary, and that’s why most of the forms with multiple grids have splitters in the middle so users can resize both grids at once by dragging the splitter with the help of a mouse. It is a good practice to add splitters to newly created forms.

Although Microsoft developers did a good job by adding splitters to most of the multi-grid forms, there is still at least one that has not got it. It is the Account reconciliation form in the Bank module, which is one of the most commonly used forms. It can be opened from Bank | Bank Account Details, Functions | Account reconciliation button, and then the Transactions button. In the following screenshot, you can see that the size of the bottom grid cannot be changed:

Microsoft Dynamics AX 2009 Development Cookbook

In this recipe, we will demonstrate the usage of splitters by resolving this situation. We will add a form splitter in the middle of the two grids in the mentioned form. It will allow users to define the sizes of both grids to make sure that the data is displayed optimally.

How to do it…

  • Open the BankReconciliation form in AOT, and create a new Group at the very top of the form’s design with the following properties:
  •  

    Property

    Value

    Name

    Top

    AutoDeclaration

    Yes

    FrameType

    None

    Width

    Column width

     

  • Move the AllReconciled, Balances, and Tab controls into the newly created group.
  • Create a new Group right below the Top group with properties:
  • Property

    Value

    Name

    Splitter

    AutoDeclaration

    Yes

    Width

    Column width

    Height

    5

    FrameType

    Raised 3D

    BackgroundColor

    Window background

    HideIfEmpty

    No

    AlignChild

    No

  • Add the following line of code to the bottom of the form’s class declaration:

  • SysFormSplitter_Y fs;
  • Add the following line of code to the bottom of the form’s init():
  • fs = new SysFormSplitter_Y(Splitter, Top, element);
  • Override three methods in the Splitter group with the following code:
  • public int mouseDown(
    int _x,
    int _y,
    int _button,
    boolean _ctrl,
    boolean _shift)
    {
    return fs.mouseDown(_x, _y, _button, _ctrl, _shift);
    }
    public int mouseMove(
    int _x,
    int _y,
    int _button,
    boolean _ctrl,
    boolean _shift)
    {
    return fs.mouseMove(_x, _y, _button, _ctrl, _shift);
    }
    public int mouseUp(
    int _x,
    int _y,
    int _button,
    boolean _ctrl,
    boolean _shift)
    {
    return fs.mouseUp(_x, _y, _button, _ctrl, _shift);
    }
  • Change the following properties of the existing BankTransTypeGroup group:
  • Property

    Value

    Top

    Auto

    Width

    Column width

    Height

    Column height

  • Change the following property of the exiting TypeSums grid located in BankTransTypeGroup group:

    Property

    Value

    Height

    Column height

  • In AOT the Modified BankReconciliation form should look like the following screenshot:
  • Microsoft Dynamics AX 2009 Development Cookbook

  • Now, to test the results, open Bank | Bank Account Details, select any bank account, click Functions | Account reconciliation, choose an existing or create a new account statement, and click the Transactions button. Notice that now the form has a nice splitter in the middle, which makes the form look better and allows defining the size of each grid:
  • Microsoft Dynamics AX 2009 Development Cookbook

    How it works…

    Normally a splitter is placed between two form groups. In this recipe, to follow that rule, we need to adjust the BankReconciliation form’s design. The filter AllReconciled, the group Balances and the tab Tab are moved to a new group called Top. We do not want this new group to be visible to user, so we set FrameType to None. Setting AutoDeclaration to Yes allows us to access this object from X++ code. And finally, we make this group automatically expand in the horizontal direction by setting its Width to Column width. At this stage, visual form layout did not change, but now we have the upper group ready.

    The BankTransTypeGroup group could be used as a bottom group with slight changes. We change its Top behavior to Auto and make it fully expandable in the horizontal and vertical directions. The Height of the grid inside this group also has to be changed to Column height in order to fill all the vertical space.

    In the middle of those two groups, we add a splitter. The splitter is nothing else but another group, which looks like a splitter. In order to achieve that, we set Height to 5, FrameType to Raised 3D, and BackgroundColor to Windows background. This group does not hold any other controls inside. Therefore, in order to make it visible, we have to set the property HideIfEmpty to No. The value No of the property AlignChild makes the splitter begin on the very left side of the form and the Column width value of the property Width forces the splitter to automatically fill the form’s width.

    Mouse events are handled by the SysFormSplitter_Y application class. After it has been declared in the form’s class declaration, we create the actual object in the form’s init(). We pass the name of the splitter control, the name of the top group and the form itself as arguments when creating it.

    A fully working splitter requires three mouse event handlers. It is implemented by overriding the mouseMove(), mouseDown(), and mouseUp() methods in the splitter group control. All arguments are passed to the respective member methods of the SysFormSplitter_Y class which does all the job.

    In this way, horizontal splitters can be easily added to any form. The Dynamics AX application also contains nice examples about splitters, which can be found in AOT in the Tutorial_Form_Split form. Vertical splitters can also be added to forms using a very similar approach. For this, we need to use another application class called SysFormSplitter_X.

    Creating modal forms

    During my trainings and working with Dynamics AX users, I noticed that people who are not familiar with computers and software tend to get lost among open application windows. The same could be applied to Dynamics AX. I experienced many times when a user opened one form, clicked some button to open another one, and then went back to the first one without closing the second one. Sometimes this happens intentionally, sometimes—not, but the result is that the second form is hidden behind the first one and the user starts wondering why it is not possible to close or edit the first form.

    Such issues could be easily solved by making the child form a modal window. In other words, the second form always stays on top of the fi rst one until closed. In this recipe, we will do exactly that. As an example, we will make the Create sales order form a modal window.

    How to do it…

  • Open the SalesCreateOrder form in AOT, and set its Design property:
  • Property

    Value

    WindowType

    Popup

  • To test, open Accounts receivable | Sales Order Details, and start creating a new order. Notice that now the sales order creation form always stays on top of the Sales order form:
  • Microsoft Dynamics AX 2009 Development Cookbook

    How it works…

    Dynamics AX form design has a WindowType property, which is set to Standard by default. In order to make a form behave as a modal window, we have to change it to Popup. Such forms will always stay on top of the parent form.

    There’s more…

    We already know that some of the Dynamics AX forms are created dynamically using the Dialog class. If we look deeper into the code, we could find that the Dialog class actually creates a runtime Dynamics AX form. That means we can apply the same principle, i.e. change the relevant form’s design property. The following code could be added to the Dialog object and would do the job:

    dialog.dialogForm().buildDesign().windowType(
    FormWindowType::Popup);

    We get a reference to the form’s design, by first using dialogForm() of the Dialog object to get a reference to the DialogForm object, and then we call buildDesign() on the latter object. Then, we set the design’s property by calling its windowType() with an argument FormWindowType::Popup.

    Changing common form appearance

    In every single multi-company Dynamics AX project, in order to prevent user mistakes, I was asked to add functionality that allows setting the background color of every form per company. By doing that, users clearly see in which company account they are at the moment and can easily work within multiple companies at the same time.

    In this recipe, we will modify SysSetupFormRun class to change the background color for every form in Dynamics AX.

    How to do it…

  • Open SysSetupFormRun in AOT, and override its run() with the following code:

  • public void run()
    {;
    super();
    this.design().colorScheme(FormColorScheme::RGB);
    this.design().backgroundColor(WinAPI::RGB2int(255,0,0));
    }
  • To test the results, open any Dynamics AX form, for example, General ledger | Chart of Accounts Details and notice how the background color is changed to red:
  • Microsoft Dynamics AX 2009 Development Cookbook

    How it works…

    SysSetupFormRun is the application class that is called by the system every time a user runs a form. The best place to add our custom code is to override the run() method and place it under the super().

    We use this.design() to get a reference to the form’s design. By calling colorScheme() and backgroundColor(), we set the color scheme to red/green/blue and the color code to red.

    We use WinAPI::RGB2int() to transform the human-readable red/green/blue code into the numeric color code.

    There’s more…

    This recipe showed a very basic principle of how to change the common appearance of all forms with few lines of code. You noticed that the color in this recipe does not fi ll all areas of the form, which does not make the form look nice. An alternative to this could be to dynamically add a colored rectangle or something similar to the top of the form. The possibilities are endless here. New controls like input fields, buttons, menu items, and others could also be added to all forms dynamically using this class. But do not overdo as it may impact system performance.

    Storing last form values

    Dynamics AX has a very useful feature, which allows saving the latest user choices per user per form. This feature is implemented across a number of standard reports, periodic jobs, and other objects, which require user input.

    When developing a new functionality for Dynamics AX, I always try to keep that practice. One of the frequently used areas is custom filters for grid-based forms. Although, Dynamics AX allows users to use standard filtering for any grid, in practice sometimes it is not very useful, especially when the user requires something specific.

    In this recipe, we will see how to store the latest user filter selections. To make it as simple as possible, we will use existing filters on the General journal form, which can be opened from General ledger | Journals | General journal. This form contains two filters—Show and Show user-created only. Show allows displaying journals by their posting status and Show user-created only toggles between all journals and the currently logged user’s journals.

    How to do it…

    1. Find the LedgerJournalTable form in AOT, and add the following code to the bottom of its class declaration:

      AllOpenPosted showStatus;
      NoYes showCurrentUser;
      #define.CurrentVersion(1)
      #localmacro.CurrentList
      showStatus,
      showCurrentUser
      #endmacro
    2. Create these additional form methods:

      public void initParmDefault()
      {;
      showStatus = AllOpenPosted::Open;
      showCurrentUser = true;
      }
      public container pack()
      {
      return [#CurrentVersion,#CurrentList];
      }
      public boolean unpack(container packedClass)
      {
      int version = RunBase::getVersion(packedClass);
      ;

      switch (version)
      {
      case #CurrentVersion:
      [version,#CurrentList] = packedClass;
      return true;
      default:
      return false;
      }
      return false;
      }
      public identifiername lastValueDesignName()
      {
      return element.args().menuItemName();
      }
      public identifiername lastValueElementName()
      {
      return this.name();
      }
      public UtilElementType lastValueType()
      {
      return UtilElementType::Form;
      }
      public userId lastValueUserId()
      {
      return curuserid();
      }
      public dataAreaId lastValueDataAreaId()
      {
      return curext();
      }
    3. xSysLastValue::getLast(this);
      AllOpenPostedField.selection(showStatus);
      ShowUserCreatedOnly.value(showCurrentUser);
      journalFormTable.designSelectionChangeAllOpenPosted();
      journalFormTable.designSelectionChangeShowUserCreateOnly();
    4. And the following code to the bottom of the form’s close():
      showStatus = AllOpenPostedField.selection();
      showCurrentUser = ShowUserCreatedOnly.value();
      xSysLastValue::saveLast(this);
    5. Now to test the form, open General ledger | Journals | General journal, change filter values, close it, and run again. The latest filter selections should stay:

    6. Microsoft Dynamics AX 2009 Development Cookbook

    How it works…

    First of all, we define some variables. We will store the journal posting status filter value in showStatus and the current user filter value in showCurrentUser.

    The macro #CurrentList is used to define a list of variables that we are going to store. Currently, we have two variables.

    The macro #CurrentVersion defines a version of saved values. In other words, it says that the variables defined by the #CurrentList, which will be stored in system cache later, can be addressed using the number 1.

    Normally, when implementing last value saving for the first time for particular object, #CurrentVersion is set to 1. Later on, if we decide to add new values or change existing ones, we have to change the value of #CurrentVersion, normally increasing it by 1. This ensures that the system addresses the correct list of variables in the cache and does not break existing functionality.

    The initParmDefault()method specifies default values if nothing is found in the system cache. Normally, this happens if we run a form for the first time, we change #CurrentVersion or clean the cache. Later, this method is called automatically by the xSysLastValue object.

    The methods pack() and unpack() are responsible for formatting a storage container from variables and extracting variables from a storage container respectively. In our case, pack() returns a container consisting of three values: version number, posting status, and current user toggle. Those values will be sent to the system cache after the form is closed. During an opening of the form, the xSysLastValue object uses unpack() to extract values from the stored container. It checks the container version from cache first, and if it matches the current version number, then the values from the cache are considered correct and are assigned to the form variables.

    A combination of lastValueDesignName(), lastValueElementName(), lastValueType(), and lastValueDataAreaId() return values form unique string representing saved values. This ensures that different users can store last values for different objects without overriding each other’s values in cache.

    The lastValueDesignName() method is meant to return the name of the object’s current design in cases where the object can have several designs. In this recipe, there is only one design, so instead of leaving it empty, I used it for a slightly different purpose. The same LedgerJournalTable AOT form can represent different user forms like Ledger journal, Periodic journal, Vendor payment journal, and so on depending on the location from which it was opened. To ensure that the user’s latest choices are saved correctly, we included the opening menu item name as part of the unique string.

    The last two pieces of code need to be added to the bottom of the form’s run() and close(). In the run() method, xSysLastValue::getLast(this) retrieves saved user values from cache and assigns them to the form’s variables. The next two lines assign the same values to the respective form controls. designSelectionChangeAllOpenPosted() and designSelectionChangeShowUserCreateOnly() execute a form query to apply updated filters. Although both of those methods currently perform exactly the same action, we keep both for the future in case this functionality is updated. Code lines in close() are responsible for assigning user selections to variables and saving them to cache by calling xSysLastValue::saveLast(this).

    LEAVE A REPLY

    Please enter your comment!
    Please enter your name here