Like most of the elements in
the AOT, forms can be customized to include additional information and
actions, such as fields and buttons, and to fulfill user requirements.
The design and behavior of a form are provided by a combination of the
form and the tables that are bound to the form.
Best Practices
Even
though you can implement all necessary customizations by modifying just
the form, we don’t recommend this approach. As a best practice, you
should implement application customizations at the lowest level
possible, preferably through changes to a table or a class rather than
through changing specific forms. |
The best way to
implement forms is to keep most of the business logic and design
decisions in tables and classes, focusing only on the positioning of
fields and menu items when designing the form. This approach has several
advantages:
X++ code in
forms is executed on the client tier only; X++ code in table methods can
be executed on the server tier to optimize performance.
Customizations
made to a form are restricted to that form; customizations made to a
table or a class apply to all forms that use that table or class as a
source of data. This results in a consistent user experience wherever
the table is used.
When
a form is customized, the entire form is copied to the current layer;
customizations to tables and classes are more granular. When fields,
field groups, and methods are customized, a copy of the specific element
is in the current layer only. This makes upgrading to service packs and
new versions easier.
X++
customizations to the validate, default, and database trigger methods
on forms, such as create, validateWrite, and write, affect only the
records that are modified through the user interface. If records are
modified somewhere other than that form, then that customized form’s X++
code doesn’t execute.
The following actions be customized only on the form, not by customizing a table:
However, you should
consider having a table or a class method determine the business logic
on the form. An example of this is shown in the following lines of X++
code from the InventTable form, in which a method on the table
determines whether a field can be edited.
void setItemDimEnabled()
{
boolean configActive = inventTable.configActive();
...
inventTable_ds.object(
fieldnum(InventTable,StandardConfigId)).allowEdit(configActive);
inventTable_ds.object(
fieldnum(InventTable, StandardConfigId)).skip(!configActive);
...
}
|
By moving these decision-making methods to a table or class, you make them available to other forms.
Learning Form Fundamentals
The rich client
user interface in Dynamics AX is made up of forms that are declared in
metadata and often contain associated code. Ideally, you should
customize these forms as changes to metadata and make any changes at the
lowest level (i.e., table level rather than form level) possible to
ensure the greatest amount of metadata and code reuse.
The most visible change from Dynamics AX 4.0 to Dynamics AX 2009 is the change from a predominantly multiple-document interface (MDI) to a predominantly single-document interface (SDI). Forms with a WindowType property value of Standard (the default) are now SDI forms, and the WindowType values of ListPage and ContentPage
have been added to fill the Workspace content area to provide a
navigation experience similar to that in Microsoft Office Outlook. The
different WindowType
values share the same object model, metadata, and method overrides, so
form customization skills are applicable across all forms.
Customizing with Metadata
Metadata
customization is preferred over code customization because metadata
changes (also called deltas) are easier to merge than code changes.
When customizing forms,
you should be aware of the important properties, the metadata
associations, and the metadata inheritance that is being used to fully
define the form and its contents.
Metadata associations
You edit the metadata in Dynamics AX by using the AOT. The base definitions for forms contained within the AOT\Forms
node is composed of a hierarchy of metadata that is located in other
nodes in the AOT. To fully understand a form, you should investigate the
metadata associations it makes. For example, a form uses tables that
are declared in the AOT\Data Dictionary\Tables node, security keys that are declared in the AOT\ Data Dictionary\Security Keys node, menu items that are declared in the AOT\Menu Items node, queries that are declared in the AOT\Queries node, and classes that are declared in the AOT\Classes node.
Metadata inheritance
You
need to be aware of the inheritance within the metadata used by forms.
For example, tables use Base Enums, Extended Data Types, and
Configuration Key. A simple example of inheritance is that the Image properties on a MenuItemButton are inherited from the associated MenuItem if they aren’t explicitly specified on that MenuItemButton. Table 1 shows important examples of pieces of metadata that are inherited from associated metadata.
Table 1. Examples of Metadata Inheritance
Type of Metadata | Sources |
---|
Labels and HelpText | MenuItem→MenuItemButton Control
Base Enum→Extended Data Type→Table Field→Form DataSource Field→Form Control
(The Base Enum Help property is the equivalent of the HelpText property found in the other types.) |
Relations | Extended Data Type→Table |
Security keys | Table Field→Table→Form Control
MenuItem→MenuItemButton Control
Form→Form Control |
Configuration keys | Base Enum→Extended Data Type→Table Field→Form DataSource
Field→Form Control |
Image properties (e.g., NormalImage) | MenuItem→MenuItemButton Control |
Inheritance
also occurs within forms. Controls that are contained within other
controls receive certain metadata property behaviors from their parents
unless different property values are specified, including HTMLHelpFile,
HTMLHelpTopic, Security Key, Configuration Key, Enabled, and the various
Font properties.
Menu definitions
Dynamics AX 2009 has
a number of new navigation capabilities in the form of area pages and
the address bar to complement the existing navigation pane (sometimes
referred to as the “WunderBar”). In terms of metadata, the area pages
and address bar are mostly just additional methods of exposing the
existing menu metadata defined in the AOT\Menus and AOT\Menu Items
nodes. The modules are defined in AOT\ Menus\MainMenu, and you can
follow the menu structure from that starting point. For example, the Accounts Receivable module is represented by the AOT\Menus\MainMenu\Cust MenuReference and is defined as AOT\Menus\Cust.
The menu metadata for list pages and content pages has some small changes. A primary list page is implemented as a submenu with IsDisplayedInContentArea=Yes, MenuItemType=Display, and MenuItemName
populated. A secondary list page, a list page that adds ranges to a
primary list page, is implemented as a menu item under the submenu of
its primary list page. The list pages and content pages are navigation
places, so all their menu item and submenu references are set to IsDisplayedInContentArea=Yes so that they
appear in the Places group in the area pages and the Places section in
the navigation pane. The other menu items in the root of each module’s
menu definition are displayed in the Common Forms group in the area
pages and in the root of the Forms section in the navigation pane.
Important metadata properties
Many properties are available to developers, but some are more important than others. Table 2 describes the most important form design properties, and Table 3 describes the most important form data source properties.
Table 2. Important Form Design Metadata Properties
Property | Explanation |
---|
Caption | The caption text shown in the title bar of a standard form or in the Filter Pane of a list page. |
TitleDataSource | The
data source information displayed in a standard form’s caption text and
used to provide filter information in the caption text of a list page. |
WindowType | Standard - (Default) A standard SDI form that opens as a separate window with a separate entry in the Windows taskbar.
ContentPage - A form that fills the Workspace content area.
ListPage
- A special style of ContentPageused to display records in a simple way
that provides quick access to filtering capabilities and actions. It
requires at least an Action Pane and a Grid.
Workspace - A form that opens as an MDI window within the workspace. Workspace forms should be developer-specific forms.
Popup
- A form that opens as a subform to its parent. Popup forms don’t have a
separate entry in the Windows taskbar and can’t be layered with other
windows. |
AllowFormCompanyChange | Specifies whether the form allows company changes when used as a child form with a cross-company dynalink.
No - (Default) Form closes if parent form changes its company scope.
Yes - Form dynamically changes company scope as needed. |
HTMLHelpFile | Specifies the path to the Help topic file. |
HTMLHelpTopic | Specifies the topic to use from the referenced Help file. |
Table 3. Important Form DataSource Metadata Properties
Property | Explanation |
---|
Name | Named reference for the data source. A best practice is to use the same name as the table name. |
Table | Specifies the table used as the data source. |
CrossCompanyAutoQuery | No - (Default) Data source gets data from the current company.
Yes - Data source gets data from all companies (e.g., retrieves customers from all companies). |
JoinSource | Specifies
the data source to link or join to as part of the query. For example,
in the SalesTable form, SalesLine is linked to SalesTable. Data sources
joined together are represented in a single query whereas links are
represented as a separate query. |
LinkType | Specifies the link or join type used between this data source and the data source specified in the JoinSource
property. Joins are required when two data sources are displayed in the
same grid. Joined data sources are represented in a single query
whereas a linked data source is represented in a separate query.
Links
Delayed
- (Default) A pause is inserted before linked child data sources are
updated, enabling faster navigation in the parent data source because
the records from the child data sources are not updated immediately. For
example, the user could be scrolling past several orders without
immediately seeing each order line.
Active
- The child data source is updated immediately when a new record in the
parent data source is selected. Continuous updates consume lots of
resources.
Passive
- Linked child data sources are not updated automatically. The link is
established by the kernel, but the application developer must trigger
the query to occur when desired by calling “ExecuteQuery” on the linked
data source.
Joins
InnerJoin
- Selects records from the main table that have matching records in the
joined table, and vice versa. There is one record for each match.
Records without related records in the other data source are eliminated
from the result.
OuterJoin
- Selects records from the main table whether or not they have matching
records in the joined table. An outer join doesn’t require each record
in the two joined tables to have a matching record.
ExistJoin - Selects a record from the main table for each matching record in the joined table.
NotExistJoin - Selects records from the main table that don’t have a match in the joined table. |
InsertIfEmpty | Yes - (Default) A record is automatically created for the user if none is present.
No -
The user needs to manually create the first record. This setting is
often used when a special record creation process or interface is used. |
Image metadata
Dynamics
AX 2009 makes greater use of images and icons throughout the
application to provide the user with additional visual cues. Icons are
used extensively in list pages to help users identify specific actions.
The metadata properties used to associate images and icons with buttons, menus (menu items), and other controls depends on their location. Table 4 describes the metadata properties used for the three common image locations.
Table 4. Image Metadata
Image Location | Explanation |
---|
Embedded | Embedded image resources are associated with buttons, menus, and other controls using the NormalResource and DisabledResource
properties. These resources are compiled into the kernel and therefore
you can’t add to the embedded resources list. The full list of embedded
resources can be seen in the Embedded Resources form (Tools\Development
Tools\Embedded Resources). |
File | File image resources are associated with buttons, menus, and other controls using the NormalImage and DisabledImage
properties. File image resources should be on the local computer
wherever possible for performance reasons, but can be on a file share if
needed. |
AOT | AOT
image resources cannot be utilized simply through metadata. AOT
resources can be utilized only by adding code that copies the AOT
resource into a temp folder and sets the NormalImage property at run time. The following code should be added into a dependable form method override (such as the Init method after the super call) for execution at run time:
SomeButton.normalImage(SysResource::getImagePath("<AOT
Resource Name>"));
|
Customizing with Code
You should customize
forms with code only as a last resort. Customizing with metadata is much
more upgrade friendly since metadata change conflicts are straight
forward to resolve whereas code change conflicts need deeper
investigation that sometimes involves creating a new merged method that
attempts to replicate the behavior from the two original methods.
When you start to customize Dynamics AX, the following ideas may provide good starting points for investigation:
Leverage examples in the base Dynamics AX 2009 codebase by using the Find command on the Forms node in the AOT (Ctrl+F).
Refer
to the system documentation entries (AOT\System Documentation) for
information about system classes, tables, functions, enumerations, and
other system elements that have been implemented in the AX kernel.
When
investigating the form method call hierarchy for a suitable location to
place customization code, add a debug breakpoint in the Form Init
method and step through the execution of method overrides. Note that
control events (e.g., clicked) do not trigger debugging breakpoints. An
explicit breakpoint (i.e., “breakpoint;”) keyword is needed in the X++ code.
To enable simpler code maintenance, the following rules should be followed:
- Utilize the table and field functions of FieldNum (e.g., fieldnum(SalesTable, SalesId)) and TableNum (e.g., tablenum(SalesTable)) when working with form data sources.
- Avoid hard coding strings by using sys labels (e.g., throw error(“@SYS88659”);) and functions like FieldStr (e.g., fieldstr(SalesTable, SalesId)) and TableStr (e.g., tablestr(SalesTable)).
- Use
as few method overrides as possible. Each additional method override
has a chance of causing merge issues during future upgrades, patch
applications, or code integrations.
When X++ code is
executed in the scope of a form, there are some form-specific global
variables created in X++ to help developers access important objects
related to the form. These global variables are described in Table 5.
Table 5. Form-Specific Global X++ Variables
Variable | Use and Example |
---|
Element | Variable that provides easy access to the FormRun object in scope. Commonly used to call methods or change the design.element.args().record().TableId == tablenum(SalesTable)
name = element.design().addControl(FormControlType::String,
"X");
|
DataSourceName (e.g., SalesTable) | Variable that provides easy access to the current/active record/ cursor in each data source. Commonly used to call methods or get/set properties on the current record.if (SalesTable.type().canHaveCreditCard())
|
DataSourceName_DS (e.g., SalesTable_DS) | Variable that provides easy access to each data source. Commonly used to call methods or get/set properties on the data source.SalesTable_DS.research();
|
DataSourceName_Q (e.g., SalesTable_Q) | Variable that provides easy access to each data source’s Query object. Commonly used to access the data source query to add ranges prior to query execution/run. Equivalent to SalesTable_ DS.query.rangeSalesLineProjId = salesLine1_q.dataSourceTable-
(tablenum(SalesLine)).addRange(fieldnum(SalesLine, ProjId));
rangeSalesLineProjId.value(ProjTable.ProjId);
|
DataSourceName_QR (e.g., SalesTable_QR) | Variable that provides easy access to each data source QueryRun object that contains a copy of the query that was most recently executed. The query inside the QueryRun object is copied during the FormDataSource ExecuteQuery method. Commonly used to access the query that was executed so that query ranges can be inspected. Equivalent to SalesTable_DS.queryRun.
SalesTableQueryBuildDataSource =
SalesTable_QR.query().dataSourceTable(tablenum(SalesTable));
|
ControlName (e.g., SalesTable_SalesId) | Variable created for each control set as AutoDeclaration=Yes. Commonly used to access controls not bound to a data source field, such as the fields used to implement custom filters.
backorderDate.dateValue(systemdateget());
|
Form
method overrides allow developers to influence the form life cycle and
how the form responds to some user-initiated events. The most important
form method overrides are described in Table 6. The two most overridden form methods are Init and Run.
Table 6. Form Method Override Explanations
Method | Explanation |
---|
Init | Called when the form is initialized. Prior to the call to super, much of the form (FormRun)
is not initialized, including the controls and the query. Commonly
overridden to access the form at the earliest stage possible. |
Run | Called when the form is initialized. Prior to the call to super,
the form is initialized but isn’t visible to the user. Commonly
overridden to make changes to form controls, layout, and cursor focus. |
Close | Called when the form is being closed. Commonly overridden to release resources and save user settings and selections. |
CloseOk | Called when the form is being closed via the Ok command/task, such as when the user clicks a CommandButton with a Command property of Ok. Commonly overridden on dialog forms to perform the action the user has initiated. |
CloseCancel | Called when the form is being closed via the Cancel command/task, such as when the user clicks a CommandButton with a Command property of Cancel. Commonly overridden on dialog forms to clean up after the user indicates that an action should be cancelled. |
CanClose | Called
when the form is being closed. Commonly overridden to ensure that data
is in a good state before the form is closed. Returning false aborts the close action and keeps the form open. |
Form data source
and form data source field method overrides allow developers to
influence how the form reads and writes its data and allows developers
to respond to user-initiated data-related events. The most important
form data source method overrides are described in Table 7. The five most overridden form data source methods are Init, Active, ExecuteQuery, Write, and LinkActive.
Table 7. Form Data Source Method Override Explanations
Method | Explanation |
---|
Active | Called
when the active/current record changes, such as when the user clicks a
different record. Commonly overridden to enable and disable buttons
based on whether or not they are applicable to the current record. |
Create | Called
when a record is being created, such as when the user presses Ctrl+N.
Commonly overridden to change the user interface in response to a record
creation. |
Delete | Called
when a record is being deleted, such as when the user presses Alt+F9.
Commonly overridden to change the user interface in response to a record
creation. |
ExecuteQuery | Called when the data source’s query is executed, such as when the form is run (from the super of the form’s Run
method) or when the user refreshes the form by pressing F5. Commonly
overridden to implement the behavior of a custom filter added to the
form. |
Init | Called when the data source is initialized during the super of the form’s Init method. Commonly overridden to add or remove query ranges or change dynalinks. |
InitValue | Called
when a record is being created. Record values set in this method count
as original values rather than changes. Commonly overridden to set the
default values of a new record. |
LeaveRecord | Called
when the user is moving focus from one data source join hierarchy to
another, which can happen when the user moves between controls. Commonly
overridden to coordinate between data sources, but developers are
encouraged to use the ValidateWrite and Write methods where possible. ValidateWrite and Write are called immediately after LeaveRecord. |
LinkActive | Called
when the active method in a dynalinked parent form is called. Commonly
overridden to change the user interface to correspond to a different
parent record (element.args().record()). |
MarkChanged | Called
when the marked set of records changes, such as when the user
multi-selects a set of records. Commonly overridden to enable/disable
buttons that work on a multi-selected (marked) set of records. |
ValidateDelete | Called
when the user attempts to delete a record. Commonly overridden to
provide form-specific deletion event validation. Return false to abort the delete. Use the ValidateDelete table method to provide record deletion validation across all forms. |
ValidateWrite | Called
when the record is being saved, such as when the user presses the Close
or Save buttons or clicks a field from another data source. Commonly
overridden to provide form-specific write/save event validation. Return false to abort the write. Use the ValidateWrite table method to provide record write/save validation across all forms. |
Write | Called
when the record is being saved after validation has succeeded. Commonly
overridden to perform additional form-specific write/save event logic
such as updating the user interface. Use the Write table method to respond to the record write/save event across all forms. |
Three commonly used form data source field method overrides are described in Table 8. The most overridden form data source field method is the Modified method.
Table 8. Form Data Source Field Method Override Explanations
Method | Explanation |
---|
Modified | Called
when the value of a field changes. Commonly overridden to make a
corresponding change to the user interface or to change other field
values. |
Lookup | Called when the Lookup button of the field is clicked. Commonly overridden to build a custom lookup form. Use the EDT.FormHelp property to provide lookup capabilities to all forms. |
Validate | Called
when the value of a field changes. Commonly overridden to perform
form-specific validation needed prior to saving or to validate. Return false to abort the change. Use the ValidateField table method to provide field validation across all forms. |