Working with Forms in Dynamics AX: Part 2

Packt Publishing

Solve real-world Microsoft Dynamics AX development problems with over 60 simple but incredibly effective recipes with this book and eBook

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:

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:

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:

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:

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:

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:

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:

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...

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

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:

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).

Using tree controls

Frequent users should notice that some of the Dynamics AX forms have an option to switch to a tree layout. In some cases, especially when there are parent-child relations among records, it is a much clearer way to show the whole hierarchy as compared to a flat list. For example, projects and their subprojects displayed in the Project Details form give a much better overview when switched to a tree layout.

This recipe will discuss the principles of how to build tree-based forms. As an example, we will use the Budget model form, which can be opened from General ledger | Setup | Budget | Budget model. This form contains a list of budget models and their submodels. Although the data is organized using a parent-child structure, currently this form does not have a hierarchy layout. The goal of this recipe is to correct that problem.

How to do it...

In AOT, create a new class called BudgetModelTree with the following code:

Override pageActivated() on the TabTree tab page with the following code:

public void pageActivated(){; super(); budgetModelTree.buildTree();}

In AOT the BudgetModel form should look like the following screenshot:

To test the tree control, open General ledger | Setup | Budget | Budget model, and select the Tree tab page. Notice how the ledger budget models are presented as a hierarchy:

How it works...

In order to separate the tree from the rest of the layout, we create a new tab page. Then, we add the actual tree control to the tab, which we use as a basis. Tree nodes are always generated from code. We also place all the tree-building logic into a separate class to make sure that it can be reused elsewhere, and the form itself does not get messy.

Besides the common new() and construct() methods, the class contains two methods, which actually generate the tree. The first method is createNode() and is used for creating a single budget model node or a whole branch. It is a recursive method, and it calls itself to generate the children of the current node. It accepts a parent node and a budget model as arguments. In this method, we create the node by calling the addTreeItem() method of the SysFormTreeControl class. The rest of the code loops through all submodels and creates subnodes (if there are any) for each of them.

Secondly, we create buildTree() where the whole tree is built. Before we actually start building it, we delete all nodes and lock the Tree control. Then, we add nodes by looping through all parent budget models and calling the previously mentioned createNode(). We call the expandTree() of the SysFormTreeControl class to show every parent budget model expanded. Once the hierarchy is ready, we unlock the Tree control.

Hierarchy generation might be time consuming, so we call it only when necessary, that is, when the tab page is actually opened. We override the tab page's pageActivated() and add a call to buildTree() there. Initially, to increase performance for bigger trees, only the first level of nodes has to be generated, and other nodes should be created only when the user clicks on the particular node. This could be achieved by placing the relevant code into the expanding() of the tree control in the form. Such an approach ensures that no time is spent on generating unused tree nodes.

There's more...

Besides hierarchical layout, tree controls also allow users to use drag-and-drop functionality. This makes daily operations much quicker and more effective. Let's modify the previous example to support drag-and-drop. We are going to allow users to move ledger budget submodels to different parents within the tree. In order to do that, we need to make some changes to the BudgetModelTree class and the BudgetModel form.

Now when you open General ledger | Setup | Budget | Budget model, you should be able to move budget models within the tree with a mouse.

The main element in the latter modification is the DragDrop property of the tree control. It enables the drag-and-drop function, once we set its value to Manual. The next step is to override drag-and-drop events on the tree control. Tree controls could have a number of methods covering various drag-and-drop events. A good place to start investigating them is the Tutorial_Form_TreeControl class in the standard Dynamics AX application. In this example, we will cover only three of them:

beginDrag() is executed when dragging begins. Here, we normally store the number of the item that is being dragged for later processing.

dragOver() is executed once the dragged item is over another node. This method is responsible for highlighting nodes when the dragged item is over them. Its return value defines the mouse cursor icon once the item is being dragged.

drop() is executed when the mouse button is released, i.e. dragged item is dropped over some node. Here, we normally place the code that does actual data modifications.

In this example, all logic is stored in the BudgetModelTree class. Each of the mentioned form methods calls the relevant method in the class. This is to reduce the amount of code placed on the form and allow the code to be reused on multiple forms. We added the following methods to the class:

canMove() checks whether the currently selected node can be dragged. Although there might be more conditions, for this demonstration, we only disallow dragging of top nodes.

move() is where the actual movement of the budget model is performed, i.e. submodel is assigned with another parent.

stateDropHilite() is responsible for highlighting and removing highlight from relevant items. Using stateDropHilited(), we highlight the current item and we remove highlight from the previously highlighted one. This ensures that as we move the dragged item over the tree, items are highlighted once the dragged item is over them and the highlight is removed once dragged item leaves them. This method is called later from several places to make sure node highlighting works correctly.

beginDrag() saves the item currently being dragged into a variable.

dragOver() first checks if the currently selected item can be moved. If not, then it returns FormDrag::None, which changes the mouse cursor to the forbidden sign. Otherwise, the cursor is changed to an icon representing node movement. This method also calls stateDropHilite() to ensure correct node highlighting.

drop() also checks if the item being dropped can be moved. If yes, then it uses move() to update the data and moveItem() to visually change the node's place in the tree. It also calls stateDropHilite() to update tree node highlighting.

Alerts & Offers

Series & Level

We understand your time is important. Uniquely amongst the major publishers, we seek to develop and publish the broadest range of learning and information products on each technology. Every Packt product delivers a specific learning pathway, broadly defined by the Series type. This structured approach enables you to select the pathway which best suits your knowledge level, learning style and task objectives.

Learning

As a new user, these step-by-step tutorial guides will give you all the practical skills necessary to become competent and efficient.

Beginner's Guide

Friendly, informal tutorials that provide a practical introduction using examples, activities, and challenges.

Essentials

Fast paced, concentrated introductions showing the quickest way to put the tool to work in the real world.

Cookbook

A collection of practical self-contained recipes that all users of the technology will find useful for building more powerful and reliable systems.

Blueprints

Guides you through the most common types of project you'll encounter, giving you end-to-end guidance on how to build your specific solution quickly and reliably.

Mastering

Take your skills to the next level with advanced tutorials that will give you confidence to master the tool's most powerful features.

Starting

Accessible to readers adopting the topic, these titles get you into the tool or technology so that you can become an effective user.

Progressing

Building on core skills you already have, these titles share solutions and expertise so you become a highly productive power user.