Ax2012 Enus Dev 3
Ax2012 Enus Dev 3
Ax2012 Enus Dev 3
DEVELOPMENT III
IN MICROSOFT DYNAMICS AX 2012
Table of Contents
Introduction
0-1
1-1
2-1
Chapter 3: Classes
3-1
Chapter 4: Forms
4-1
5-1
Chapter 6: Workflow
6-1
ii
Table of Contents
Create a Workflow Type ..................................................................................... 6-4
Enable Workflow on a Form ............................................................................... 6-5
Create a Workflow Approval ............................................................................... 6-8
Create Event Handlers ....................................................................................... 6-9
Author a Workflow ............................................................................................ 6-12
Lab 6.1 - Add Another Condition to the Submit Action ..................................... 6-15
Lab 6.2 - Enable Resubmit ............................................................................... 6-17
Summary .......................................................................................................... 6-20
Test Your Knowledge ....................................................................................... 6-21
Quick Interaction: Lessons Learned ................................................................. 6-22
Solutions ........................................................................................................... 6-23
iii
iv
Introduction
INTRODUCTION
Welcome
We know training is a vital component of retaining the value of your Microsoft
Dynamics AX 2012. investment. Our quality training from industry experts
keeps you up-to-date on your solution and helps you develop the skills necessary
for fully maximizing the value of your solution. Whether you choose Online
Training, Classroom Training, or Training Materials; there is a type of training to
meet everyone's needs. Choose the training type that best suits you so you can
stay ahead of the competition.
Online Training
Online Training delivers convenient, in-depth training to you in the comfort of
your own home or office. Online training provides immediate access to training
24 hours-a-day. It is perfect for the customer who does not have the time or
budget to travel. Our newest online training option, eCourses, combine the
efficiency of online training with the in-depth product coverage of classroom
training, with at least two weeks to complete each course.
Classroom Training
Classroom Training provides serious, in-depth learning through hands-on
interaction. From demonstrations to presentations to classroom activities, you
receive hands-on experience with instruction from our certified staff of experts.
Regularly scheduled throughout North America, you can be sure you will find a
class convenient for you.
Training Materials
Training Materials enable you to learn at your own pace, on your own time with
information-packed training manuals. Our wide variety of training manuals
feature an abundance of tips, tricks, and insights you can refer to again and again:
0-1
Challenge Yourself!
Level 3 exercises are the most challenging. These exercises are designed for the
experienced student who requires little instruction to complete the required task.
Step by Step
Level 1 exercises are geared towards new users who require detailed instructions
and explanations to complete the exercise. Level 1 exercises guide you through
the task, step by step, including navigation.
0-2
Introduction
Documentation Conventions
The following conventions and icons are used throughout this documentation to
help you quickly and effectively navigate through the information.
CAUTION: Cautions are found throughout the training manual and are preceded by
the word CAUTION in bold. Cautions are used to remind you of a specific result of a
specific action which may be undesirable.
HINT: Hints are found throughout the training manual and are preceded by the word
HINT in bold. Hints are used to suggest time-saving features or alternative methods for
accomplishing a specific task.
NOTE: Notes are found throughout the training manual and are preceded by the word
NOTE in bold. Notes are used to provide information which, while not critical, may be
valuable to an end user.
BEYOND THE BASICS: Advanced information found throughout the training manual
is preceded by the words BEYOND THE BASICS in bold. Beyond the Basics provides
additional detail, outside of standard functionality, that may help you to more optimally
use the application.
EXAMPLE: Examples are found throughout the training manual and are preceded by
the word EXAMPLE in bold. Examples bring to light business scenarios that may better
explain how an application can be used to address a business problem.
0-3
Student Objectives
What do you hope to learn by participating in this course?
List three main objectives below.
1.
2.
3.
0-4
Introduction
The X++ Unit Test framework allows for unit tests to be created along with the
code they are designed to test.
A unit test is code that verifies that some specific application code has been
implemented correctly. If you adhere to the principles of test-driven
development, it is best for the developer who is writing the application code to
write the unit tests either before or during development.
1-1
1-2
Attribute
Description
Applied to
SysTestMethodAttribute
Method
SysTestCheckInAttribute
Method or Class
SysTestNonCheckInAttribute
Method
Description
Applied to
SysTestTargetAttribute
Class
SysTestInactiveTestAttribute
Method
Another variable has been created in the ClassDeclaration, of the type of the class
being tested.
A test method will typically contain several assertions that are required to hold
true, for the test to be successful.
Note the use of the assertEquals method. This method, and other assert methods,
are available as part of the SysTestCase framework. These methods also have an
optional string parameter called _message, to specify the message that would go
into the infolog, if the assertion fails. The following is a list of available methods:
Method
Description
assertEquals
assertNotEqual
1-3
Description
assertTrue
assertFalse
assertNull
assertNotNull
assertSame
assertNotSame
assertRealEquals
assertExpectedInfolog
Message
fail
Setup Method
The next step is to override the setUp method of the SysTestClass. This will tell
the Unit Test framework what parameters need to be setup before the test is run.
In this case, you need to instantiate the SysDictTable object using the table name.
public void setUp()
{
sysDictTable = new SysDictTable(TableNum(CustTable));
super();
}
The setUp method can also be used to insert or update any data necessary for the
test.
TearDown Method
At the completion of a test run, it may be necessary to "undo" any data created in
the setUp method. This can be done in the tearDown method, which is called at
the end of the test. The tearDown method can be overridden on any class
extending SysTestCase.
1-4
The SysDictTableTest test should pass on the first run. Try changing the
expected values in the test* methods, or the table id used in the setUp method, to
emulate a test that fails, then run the test again. To run the test again, click the
Run button on the Unit Test toolbar.
If the test fails, click the Details button to see more details of the results.
1-5
1-6
Test projects
Test suites
Test Projects
Test projects are groupings of test classes and appear in the Development
Project tree along with other project types.
1-7
Once test classes have been added to a Test project, the entire collection of tests
can be run at the same time, by right-clicking the root node of the project, and
clicking Run.
Test Suites
Collections of tests can also be created with X++ code. These collections extend
the SysTestSuite class, and are referred to as Suites.
Use the following procedure to create a test suite class:
1. In the AOT, create a new Class.
2. Open the new class in the Code Editor.
3. Edit the ClassDeclaration so that the class extends the SysTestSuite
class.
4. Override the new method on the class.
5. In the new method, call the add method to add test cases to the suite,
as shown.
public void new()
{
// Create an instance of a test suite.
SysTestSuite suiteDictTableTest = new
SysTestSuite(classstr(SysDictTableTest));
;
super();
1-8
The setUp and tearDown methods can also be used in the scope of a Test Suite.
Both methods are available to override on any class extending the SysTestSuite
class. In this way, the setting up of data and variables can be done before the
entire suite of tests is run.
Isolation
The isolation level of a test case varies based on the changes that the test case
will make to the data. Each test case could have different needs for isolation
based on what data it will change. The Unit Test framework provides four test
suite base classes that provide different levels of isolation. The following table
describes the test suites by the level of isolation that they provide.
Test suite class
Description
SysTestSuite
SysTestSuiteCompanyIsolate
Class
SysTestSuiteCompanyIsolate
Method
SysTestSuiteTTS
SysTestSuiteCompIsolate
ClassWithTts
This is a combination of
SysTestSuiteCompanyIsolateClass and
SysTestSuiteTTS.
To apply a specific test suite to a test class, override the createSuite method on
the test class, and return an object of the type of the suite required.
1-9
1-10
Challenge Yourself!
Create a test case class, and add a test method to it. The test case should check
that when an address record's Zip code is changed, the correct City is
automatically updated. Once complete, create a test suite class that enforces TTS
isolation, and link the test case class to it. Finally, run the test.
Step by Step
1.
2.
3.
4.
1-11
8.
9.
10.
11.
1-12
Summary
This lesson explains how to use the Unit Test framework to test X++ code during
the development lifecycle.
The concepts of test cases, assertions, projects, suites and isolation are
introduced, demonstrating the rich features available to build robust logic tests.
1-13
1-14
1-15
2.
3.
1-16
Solutions
Test Your Knowledge
1. What class does a test class need to extend?
MODEL ANSWER:
SysTestCase
2. What is the setUp method on the SysTestCase class used for?
MODEL ANSWER:
It is used to prepare variables and data that are required for the test.
3. Where can the Test jobs form be opened from?
MODEL ANSWER:
Tools > Unit test > Test jobs.
4. What is meant by "isolation" in the context of the Unit Test framework?
MODEL ANSWER:
Isolation refers to the scope in which company data should be created and
destroyed during a test run.
1-17
1-18
Introduction
A Microsoft Dynamics AX application processes large amounts of data. Most
functions involve sending data between the client and Application Object Server
(AOS) and between the AOS and database server. It is important to use the
correct approach to database access when developing in Microsoft Dynamics
AX. Almost every performance bottleneck is associated with database traffic.
2-1
While Select
This section describes the different qualifiers and options that can be used in the
select statement, to achieve optimal database access performance.
The complete syntax for the select statement is as follows.
General Optimization
To optimize general performance, the following tools and keywords may be
used.
Fieldlist
One way to optimize communication with the database is to specify which fields
are returned. For example, for a table with 40 fields, reading the information
from only four fields will reduce the amount of data sent from the database server
by up to 90 percent.
2-2
NOTE: Use this optimization with care. If the record returned from the database
is subsequently passed as a parameter to other methods, that method may have
been written on the assumption that all fields are set. Only use field lists when
controlling access to the information locally.
Aggregation
To obtain a sum of records, consider instructing the database to calculate the sum
and only return the result, as an alternative to reading all the records and making
the aggregation yourself. To receive a sum specified for one or more fields in the
table, combine the aggregation with a group by clause. The following
aggregation clauses are available.
Aggregation clause
Description
sum
avg
maxof
minof
count
2-3
To process 500 records in the main table, this approach would have 501 SQL
statements executed on the database.
Alternatively, making a single "while select" statement with a join clause reduces
the number of SQL statements to just 1.
The following example illustrates using a join clause (and fieldlists for extra
performance).
while select recId from inventTable
join qty from inventTrans
where inventTrans.itemId == inventTable.itemId
{
qty += inventTrans.qty;
}
ForceLiterals
ForceLiterals instructs the kernel to reveal the actual values used in the "where"
clauses to the database server at the time of optimization. This is the default
behavior in all "join" statements involving more than one table from the
following table groups:
2-4
Miscellaneous
Main
Transaction
Worksheet Header
Worksheet Line
ForcePlaceholders
ForcePlaceholders instructs the kernel not to reveal the actual values used in
where clauses to the database server at the time of optimization. This is the
default in all non-join statements. The advantage of using this keyword is that the
kernel can reuse the access plan for other similar statements with other search
values. The disadvantage is that the access plan is computed without considering
that data distribution might not be even, or that the access plan is an "on average"
access plan.
The following X++ statement is an example of when to use this keyword.
static void DemoForcePlaceholders()
{
SalesTable salesTable;
SalesLine salesLine;
;
while select forcePlaceholders salesLine
join salesTable
where salesTable.SalesId == salesLine.SalesId
&& salesTable.SalesId == '10'
{
}
}
2-5
FirstFast
FirstFast instructs the SQL-database to prioritize fetching the first few rows fast
over fetching the complete result set. This also means that the SQL-database
might select an index fitting the order by clause over an index fitting the "where"
clause. The FirstFast hint is automatically issued from all forms, but is rarely
used directly from X++.
Firstonly
When Microsoft Dynamics AX fetches data from the database, it transfers a
package of records in each fetch. This is called read-ahead caching and is
performed to minimize calls to the database. If it is known that only one record
will be fetched, you can disable the read-ahead caching with this qualifier.
NOTE: It is best practice to use this in the "find" methods on the tables.
The following example illustrates the use of FirstOnly.
static CustTable findCustTable(CustAccount _custAccount)
{
CustTable custTable;
;
select firstonly custTable
where custTable.AccountNum == _custAccount;
return custTable;
}
2-6
ForceSelectOrder
This keyword forces the database server to access the tables in a join in the given
order. If two tables are joined the first table in the statement is always accessed
first. This keyword is frequently combined with the forceNestedLoop keyword.
One situation where it can be interesting to force a select order is when you use
index hint on a join. The following construction is an example of the
ForceSelectOrder.
static void DemoForceSelectOrder()
{
InventTrans inventTrans;
InventDim inventDim;
;
while select inventTrans
index hint ItemIdx
where inventTrans.ItemId == 'X'
join inventDim
where inventDim.inventDimId ==
inventTrans.inventDimId
&& inventDim.inventBatchId == 'Y'
{
}
Give the database a hint on using the index ItemIdx on the table InventTrans.
This works well if the database searches this table first. But if the database, with
the help of the generated statistics, starts with the table InventDim and then finds
records in InventTrans for each occurrence of InventDimId, the use of the
index ItemIdx may not be an appropriate approach. To hint indexes in a join,
consider specifying forceSelectOrder, as shown in the following example.
static void DemoForceSelectOrder()
{
InventTrans inventTrans;
InventDim inventDim;
;
while select forceSelectOrder inventTrans
index hint ItemIdx
where inventTrans.ItemId == 'X'
join inventDim
2-7
ForceNestedLoops
This keyword forces the database server to use a nested-loop algorithm to process
a given SQL statement that contains a join. This means that a record from the
first table is fetched before trying to fetch any records from the second table.
Generally other join algorithms like hash-joins, merge-joins, and others are also
considered. This keyword is frequently combined with the forceSelectOrder
keyword.
Review the previous example with the tables InventTrans and InventDim. You
could risk that the database finds all InventTrans records by the index ItemIdx
and all the InventDim records by the BatchId. (If you hint the index DimIdIdx
this will be used for this search.) The two collections of records are hashed
together. For the database to find the inventTrans and then the inventDim for
each inventTrans, specify forceNestedLoops, as shown in the following
example.
static void DemoForceSelectOrder()
{
InventTrans inventTrans;
InventDim inventDim;
;
while select forceSelectOrder forceNestedLoop
inventTrans
index hint ItemIdx
where inventTrans.ItemId == 'X'
join inventDim
where inventDim.inventDimId ==
inventTrans.inventDimId
&& inventDim.inventBatchId == 'Y'
{
}
2-8
You also have the option of adding a container variable of company identifiers
immediately after the crosscompany keyword (separated by a colon). The
container restricts the selected rows to those with a dataAreaId that match a value
in the container. The following example illustrates the use of the crossCompany
keyword.
CustTable custTable;
container conCompanies = [ '001', '002', 'dat' ];
;
while select crosscompany : conCompanies * from custTable
order by dataAreaId
{
//do something
}
Query
A query is an object-oriented interface to the SQL database. A query is composed
of objects from different classes.
Various objects are available to manipulate a query object from the AOT.
Use the queryRun object to execute the query and fetch data.
The query object is the definition master. It has its own properties and has one or
more related data sources.
The queryBuildDataSource defines access to a single table in the query. If one
data source is added below another data source, they form a join between the two
tables.
The queryBuildFieldList object defines which fields to fetch from the database.
The default is a dynamic field list that is equal to a "select * from ". Each data
source has only one queryBuildFieldList object which contains information
about all selected fields. You can also specify aggregate functions like sum,
count, and avg with the field list object.
2-9
qbds1, qbds2;
qbr;
q
= new Query();
qbds1
= q.addDataSource(tablenum(InventTable));
qbds2
= qbds1.addDataSource(tablenum(InventTrans));
qbds2.relations(TRUE); //this enforces a relationship
between this datasource and its parent. Relationships
defined in the Data Dictionary are used by default.
qbr
= qbds1.addRange(fieldnum(InventTable, ItemId));
qbr.value(SysQuery::value("1706")); //SysQuery object
provides various static methods to assist in defining Query
criteria. The SysQuery::value() method should always be
used when defining a singular value for a range.
qr = new QueryRun(q);
while(qr.next())
{
//do something
}
You can build a query in the AOT using MorphX or as shown in the previous
topic, by dynamically creating the query by X++ code. Both approaches are used
in the standard application. One advantage of making the query dynamic is that it
is not public in the AOT and is protected against unintentional AOT changes.
Alternatively, an advantage of creating the query in the AOT is that it can be
reused in various places, saving lines of identical code, and making widereaching query adjustment easier.
2-10
2-11
Challenge Yourself!
Make the following query "Count the number of customers from all companies,
limited to a specific currency." For this example, make USD the limiting value.
Perform the following actions:
1. Create a job which makes the above select using "while select". The
currency to use can be defined in the code.
2. Create a new query in the AOT. Prompt the user for the currency to
use when the query is run.
3. Create a job which executes the query defined in step 2.
4. Create a new job which builds the same query dynamically using the
query classes. Prompt the user for the currency to use when the query
is run.
5. Verify that the three implementations return the same data.
Step by Step
1. The following shows query made in a job using "while select".
static void SelectCustomerJob(Args _args)
{
CustTable custTable;
Counter recordsFound;
;
while select crosscompany custTable
where custTable.Currency =="USD"
{
recordsFound++;
}
info(strFmt("Customers found: %1", recordsFound));
}
2-12
FIGURE 2.1
3. The following shows the job that executes query from step 2.
static void SelectCustomerRunQuery(Args _args)
{
QueryRun queryRun;
Counter recordsFound;
;
queryRun = new QueryRun(queryStr(CustTableByCurrency));
if (queryRun.prompt())
{
while (queryRun.next())
{
recordsFound++;
}
}
info(strFmt("Customers found: %1", recordsFound));
4. The following shows the job that dynamically builds the query
static void SelectCustomerQuery(Args _args)
{
Query query;
QueryBuildDataSource queryBuildDatasource;
QueryFilter queryFilter;
QueryRun queryRun;
Counter recordsFound;
;
query = new Query();
query.allowCrossCompany(TRUE);
queryBuildDataSource = query.addDataSource(tableNum(
CustTable));
queryFilter =
query.addQueryFilter(queryBuildDataSource,
2-13
2-14
inventTrans;
inventDim inventDim;
Enable the user to enter the item id when the query is run.
Perform the following actions:
1. Create a new job.
2. Use the Query, QueryBuildDataSource classes, and more, to create a
query dynamically.
3. Create another job that uses "while select" as shown.
4. Verify that the two implementations return the same data, using item
number 1706.
Step by Step
1.
2.
3.
4.
5.
6.
2-15
2-16
Caching
The previous sections have discussed how to optimize communication with the
database by structuring SQL statements that make the database access data in the
correct way. But the most optimal strategy is to have no communication with the
database or to minimize the number of communications with the database server.
The concept of caching is to remember the information retrieved from the
database and use this memory when the same data is needed at a later time.
However, this strategy has one large drawback, if the remembered information is
no longer valid, this could compromise the consistency of the database, as the
updates will be made based on invalid data.
This section covers data caching done by Microsoft Dynamics AX AOS and/or
client. Microsoft Dynamics AX supports the following kinds of caching:
Read-ahead caching
Single-record caching
The remainder of this section explains the first five above mentioned data
catching.
Read-ahead Caching
The SQL interface in Microsoft Dynamics AX provides read-ahead caching and
uses a buffer to pre-fetch rows. The read-ahead adapts to the queries executed.
As an example, a query to show rows in a form pre-fetches in chunks that
correspond to the number of rows currently visible in the form, whereas a multirow select with the FirstOnly keyword will not pre-fetch.
Single-Record Caching
Microsoft Dynamics AX offers record caching. Rows selected through a cachekey are cached and successive look-ups specifying the cache-key are then served
from the record cache. The record-cache holds the rows that match frequently
issued single-row queries.
The cache-key is the Primary Key on the cached table. If a Primary Key does not
exist, the first unique index is used.
2-17
Result
None
NotInTTS
Found
FoundAndEmpty
2-18
The caching only works when selecting exactly one record, with a
distinct where on the primary key.
The cache does not support data retrieved by joining more than one
table.
2-19
2-20
2-21
2-22
Display methods not based on cached data, are candidates for display method
caching.
Locking
So far you have seen that the database server can access data faster when the
number of requests to the database is minimized, by using efficient queries and
caching.
Another issue is if multiple users want to lock the same record at the same time.
Only one user can lock the record, and the rest wait in a queue until the record is
released. This has a substantial effect on the time used to perform a function.
Locks are typically done in transactions where more than one record is locked at
a time. The following example illustrates two different processes which lock
different items:
Process 1
Process 2
Item C - Locked
Item A - Locked
Item B - Locked
Item G - Locked
2-23
Try to lock central resources in the same order. This avoids the dead
lock situation discussed previously. This serializes the process,
which means that only one user can perform this at a time.
Locking is held for the time it takes to finish the process. If the process has poor
performance because of missing optimization of AOS and database
communication, the locking is a bigger issue and needs to be addressed first.
2-24
Challenge Yourself!
Modify the following job to reduce locking:
InventTable inventTable;
VendTable vendTable;
;
ttsbegin;
while select forupdate inventTable
{
if
(VendTable::find(inventTable.PrimaryVendorId).Blocked
== CustVendorBlocked::All)
{
inventTable.PrimaryVendorId ="";
inventTable.update();
}
}
ttscommit;
Step by Step
Solution 1: A new InventTable table variable is declared. Updates are done
against this variable, so that you do not need to lock the whole InventTable
forupdate.
static void Locking_Solution_1(Args _args)
{
InventTable inventTable;
InventTable inventTableUpdate;
VendTable vendTable;
;
ttsbegin;
// solution #1
2-25
Solution 2: If and find on VendTable are replaced with exist join on VendTable.
static void Locking_Solution_2(Args _args)
{
InventTable inventTable;
VendTable vendTable;
;
ttsbegin;
// solution #2
while select forupdate inventTable
exists join vendTable
where vendTable.AccountNum ==
inventTable.PrimaryVendorId &&
vendTable.Blocked == CustVendorBlocked::All
{
inventTable.PrimaryVendorId ="";
inventTable.update();
}
ttscommit;
}
2-26
Temporary Tables
A temporary table is defined in the AOT as a normal table, but with the
TableType property set to either InMemory or TempDB. Data is not persisted in
a temporary table, and only remains while the table is in scope. If TableType is
set to InMemory, the data is stored in memory or in a temporary file if memory is
full. TempDb means it is stored in a table in SQL, and can be joined to regular
tables at the database tier.
A typical example is a class which initializes a temporary table and inserts some
records, which should be shown in a form. The class has a variable _tmpTable
which holds the data and the form has a data source tmpTable_DS which should
show the same data. This is solved by using the method
tmptable_DS.setTmpData(_tmpTable). This disregards the individual file
allocated to tmptable_DS. Now, both _tmpTable and tmptable_DS will access
the same pool of data, as if they were normal table variables. The allocated file
now shared between _tmpTable and tmptable_DS is deleted once both variables
have gone out of scope.
Instead of making a dedicated definition of a temporary table in the AOT,
consider making a temporary instance of a database table (in other words, a nontemporary table which is part of the SQL database). Do this using the .setTmp()
method before accessing the variable. Be careful with this option, as you can
activate other methods which act as if it is real database data and cause
subsequent updates in other tables. See the following example, which copies all
customers from Australia to a temporary table.
static void CopyPersistedTableToTemp(Args _args)
{
CustTable custTable;
CustTable tmpCustTable;
;
tmpCustTable.setTmp();
while select custTable
where custTable.CountryRegionId == "AU"
{
tmpCustTable.data(custTable.data());
tmpCustTable.doInsert();
}
}
2-27
Challenge Yourself!
Make a job which works with two individual instances of temporary data (for
example, TmpSum).
Perform the following steps:
1. Make a job with two variables A and B of type TmpSum.
2. Insert some records in variable A. Does this have an effect on the
contents of variable B?
3. Copy all records from A to B. Verify that the contents of the two
tables are equal.
4. Delete one record from A. Does this have an effect on the contents of
variable B?
5. Call the B.setTempData(A) method. Repeat step 2 and 4.
Step by Step
If you declare two temporary table variables A and B, inserts, updates, and
deletes on table A do not have impact on B. This only happens if the method
variableB.setTempData(variableA) is set before updating.
1. Open the AOT create a new job.
2. Copy the following code in to the job.
3. Read through the code carefully, and try to understand what each
line is doing and what the result will be.
4. Press F5 to run the job. Verify the results are what you expected.
static void Temporary(Args _args)
{
TmpSum a;
TmpSum b;
Counter counter;
void countRecords(Tmpsum _tmptable, str _step, str
_name)
{
select count(RecId) from _tmptable;
info(strFmt("Step %1 - %2 records in %3", _step,
_tmptable.RecId, _name));
}
;
ttsbegin;
2-28
2-29
2-30
InitFrom
Often, a record has default values for fields initialized with data copied from
another table. For example, when a sales order is created and a customer account
is specified, fields including Invoice Account, Delivery Address, Tax Group, and
Currency are copied from the CustTable record to the SalesTable. This is done
in the SalesTable.InitFromCustTable() method which contains a list of field
assignments between the two tables. This is a common technique of initializing
fields in one table from another table, and is always the first place to analyze.
For example, to add the new field "Fragile" to the BOM table that defaults from
another new field "Fragile" in the InventTable, consider adding the following
line to BOM.InitFromInventTable():
this.fragile = inventTable.fragile;
Parm Tables
When sales orders, purchase orders, or production orders are updated to a new
status, Microsoft Dynamics AX uses Parm tables to store what will be updated.
This allows the user to view and modify what will be updated without affecting
the original order. It also enables the update to be processed by the batch system.
The user can view and edit the Parm tables in the SalesEditLines or
PurchEditLines forms. In the previous figure, the SalesParmLine table appears
on the Lines tab, and the SalesParmTable appears on the SalesOrders tab.
2-31
Date Effectiveness
Date effectiveness allows the application to associate valid from and to dates
with the application artifacts. For example, an agreement can be valid between a
range of dates. Similarly, interest rates are assigned based on start date and end
date association.
The date effectiveness feature is a central framework that is supported in all
framework components. It helps developers write less code, to develop more
consistent code, and to create forms that manage viewing and editing of current,
past, and future records.
2-33
2-34
7. The three variables that are declared are used to hold the physical
names of the view and the fields that are to be used.
8. The SysComputedColumn::if() method then converts three
expressions in to the T-SQL expression. By placing the cursor on the
if() method and pressing F12, you can see the method.
9. To view the final T-SQL code that is generated, you need to turn on
SQL-Tracing. Go to Tools > Options > SQL.
10. Check SQL Trace, and then check Multiple SQL Statements >
infolog.
11. Close the user options form.
12. In the AOT, right-click on view InventTableExpanded and select
Synchronize.
2-35
14. Note that one of the fields has the following : ,(CAST ((CASE
WHEN T5.PRODUCTNAME IS NULL THEN
T2.DISPLAYPRODUCTNUMBER ELSE T5.PRODUCTNAME
END) AS NVARCHAR(60))) AS PRODUCTNAME,
Data Integration
External data comes in many different formats. This section describes
mechanisms to read and write data in four different, but commonly used, formats.
With all external interaction in X++, code access permission must be asserted.
IO classes
The standard classes that extend the IO class are useful for importing and
exporting data to and from a simple text file format. Most commonly, these are
TXT or CSV files.
2-36
2-37
The following is sample code which writes a simple string to a text file:
FileName fileName = 'c:\\test.txt';
FileIoPermission permission;
FileIO fileIO;
str outputText;
#File
;
permission= new FileIoPermission(filename,#io_write);
permission.assert();
fileIO= new FileIO(filename, #io_write);
if (fileIO)
{
outputText = "text that will go into the text file.";
fileIO.write(outputText); //write the text to the
file.
fileIO.finalize();
//finish the file.
}
Creating XML
Extensible Markup Language (XML) is a text-based standard for representing
data and is commonly used for data exchange and business-to-business
communication.
The structure of XML files is similar to HTML in that it uses tags to define
sections of the file. Each tag defines either single or groups of data elements, and
can also have attributes (for example, a date or reference ID) attached to the tag.
The following is an example of a simple XML file:
<note>
<appl>Microsoft Dynamics AX</appl>
<version>2012</version>
<body>This is a simple XML file</body>
</note>
2-38
= XMLDocument::newBlank();
2-39
Reading XML
XML files can be read in an easy way or a hard way. The hard way is to read the
file line by line and determine the correct tag each time.
The easy way is to let the microsoft.xmldom COM object do the work. The
following example reads an XML file containing customer account number and
transaction details.
static void XMLReadCustTrans(Args _args)
{
FileIoPermission permission;
XMLDocument
doc;
XMLNode
rootNode, AccNode, NameNode, transNode;
XMLNodeList
custTransList;
XMLParseError xmlError;
int
i;
;
permission= new
FileIoPermission("C:\\CustTrans.xml",'r');
permission.assert();
// Get the XML document
doc = new XMLDocument();
doc.load("C:\\CustTrans.xml");
xmlError = doc.parseError();
if (xmlError && xmlError.errorCode() != 0)
throw error(strFmt("Error: %1",xmlError.reason()));
rootNode = doc.documentElement();
// Get the customer account and name from the document
AccNode =
rootNode.selectSingleNode("//CustomerAccount");
NameNode = rootNode.selectSingleNode("//CustomerName");
setprefix(AccNode.text()+ ' ' + NameNode.text());
// Select all the customer transactions into a nodelist
custTransList =
rootNode.selectNodes("//TransactionDetail");
for (i = 0; i < custTransList.length(); i++)
{
transNode = custTransList.item(i);
2-40
ODBC
Sometimes, data that you need to access from Microsoft Dynamics AX may be
stored in an alternative database. This is common when fetching data from an
external application, either on a regular basis, or during data conversion.
An Open DataBase Connection (ODBC) is a simple way for Microsoft Dynamics
AX to access an external database, and perform queries on it to fetch the required
data.
For this to work, an ODBC DataSource Name (DSN) needs to be created in
Windows. A DSN acts as a thin database client, usually including all
authentication information like username and password. The DSN needs to be
created on the tier where the X++ code will call it from (in other words, either the
client or the AOS). Best practice is to keep the DSN on the AOS, to help
scalability.
Once a DSN is available in Windows, code can be written to leverage its access
to the external database:
static void TestODBC()
{
LoginProperty loginProperty;
OdbcConnection odbcConnection;
Statement statement;
ResultSet resultSet;
str sql, criteria;
SqlStatementExecutePermission perm;
;
//Set information on the ODBC
loginProperty = new LoginProperty();
loginProperty.setDSN("dsn");
loginProperty.setDatabase("databaseName");
//Create connection to external DB
odbcConnection = new OdbcConnection(loginProperty);
if (odbcConnection)
{
sql = "SELECT * FROM MYTABLE WHERE FIELD =" +
criteria + " ORDER BY FIELD1,FIELD2 ASC"
//assert permission for sql string
perm = new SqlStatementExecutePermission(sql);
2-41
//Prepare statement
statement = odbcConnection.createStatement();
resultSet = statement.executeQuery(sql);
//Running statement
while (resultSet.next())
{
//It is not possible to get field 3 and then 1.
Always get fields in numerical order: 1,2,3,4,5,6
print resultSet.getString(1);
print resultSet.getString(3);
}
//Shutting down the connection
resultSet.close();
statement.close();
}
else
error("Failed to log on to the database");
}
The previous code uses a variable called "criteria", and includes it in the SQL
string that is executed against the example external database. It then prints the
string values from columns 1 and 3 in the result set. Columns in the result set can
only be called in numerical order. If you need to print column 3 value first, first
fetch the column 1 value and store it in a local variable, then fetch and print the
column 3 value, then print the local variable.
More methods are available on the ResultSet object, to fetch other types from the
record. These include, getReal(), getDate(), getInt().
The string that is passed to the statement.executeQuery() method must be an
SQL query in the format that the external database can understand. For example,
if you are querying an Oracle database, the string must contain a valid Oracle
SQL query.
Microsoft Excel
Microsoft Dynamics AX includes built-in administration tools for importing and
exporting table data using the Microsoft Excel spreadsheet format. However,
sometimes it may be necessary to perform similar Microsoft Excel data import
and export functions from a custom function. Often, this can be done using
Comma Separated Value (CSV) files and the CommaIO class, but sometimes
using XLS or XLSX formatted files is a requirement. To make this possible,
there are various classes to perform Microsoft Excel integration. These all start
with the prefix SysExcel and can be found in the Class branch of the AOT.
2-42
2-43
Challenge Yourself!
To complete this lab, you need to create an external data file. Create a new file
with the following structure:
Item number, Date, Amount
For example your file could look like this:
Save the file as a CSV file named SalesPrice.csv in the root C drive directory.
Write a job in X++ that will import the data from the SalesPrice.csv file that you
just created. The job will need to set some values for other fields - the price
should be for All customer accounts, use the current companies default currency,
be applicable for all quantities and for any inventory dimension.
Step by Step
1. Open the AOT and create a new Job.
2. Copy the following code to the job.
static void ImportSalesPrice(Args _args)
{
CommaIO
commaIO;
container
readCon;
str
column1, column2, column3;
PriceDiscTable priceDiscTable;
#File
;
2-44
2-45
Summary
Using the most efficient approaches to database communication will result in an
application that runs with the best performance possible. Using the tools
described in this chapter, modifications made to the standard application can
operate at the peak performance expected by the users.
2-46
2. You have the following X++ code, with two QueryBuildDatasource objects.
Modify the second line and add a third line, so that these two datasources are
joined:
qbds1 = query.addDatasource(tableNum(CustTable));
qbds2 = query.addDatasource(tableNum(CustTrans));
2-47
2-48
2-49
2.
3.
2-50
Solutions
Test Your Knowledge
1. Match the select statement clauses, with their correct purpose:
e 1. Fieldlist
g 2. FirstOnly
b 3. FirstFast
d 4.
ForceNestedLoop
f 5.
ForceSelectOrder
c 6.
CrossCompany
a 7. Index
2. You have the following X++ code, with two QueryBuildDatasource objects.
Modify the second line and add a third line, so that these two datasources are
joined:
qbds1 = query.addDatasource(tableNum(CustTable));
qbds2 = query.addDatasource(tableNum(CustTrans));
MODEL ANSWER:
qbds2 = qbds1.addDatasource(tableNum(CustTrans));
qbds2.relations(TRUE);
3. Which of the following are true for avoiding locking: (Select all that apply)
() Keep the transactions as short in time as possible.
( ) Try to lock central resources.
() Try to lock central resources in the same order.
( ) Try to implement dialogs with the user inside transactions.
2-51
2-52
Chapter 3: Classes
CHAPTER 3: CLASSES
Objectives
The objectives are:
Introduction
Microsoft Dynamics AX provides a large range of standard system classes that
you can use while developing X++ code. This lesson introduces some of the most
commonly used system classes, and demonstrates ways they can be used to
support modifications.
3-1
Collection Classes
X++ contains two compound data types: arrays and containers. They are useful
for aggregating values of simpler data types. However, you cannot store objects
in arrays or containers. The Microsoft Dynamics AX collection classes have been
designed for storing objects.
The following collection classes are available:
List
Map
Set
Array
Struct
RecordSortedList
RecordInsertList
List
A List object contains members that are accessed sequentially. Lists are
structures that can contain members of any X++ type. All the members in the
same list must be of the same type.
The following methods are commonly used on List objects:
The ListEnumerator class allows you to traverse through the elements within a
list. The following methods are commonly used on ListEnumerator objects:
3-2
moveNext() - moves the enumerator to the next item in the list. List
enumerators start before the first element in the list, so moveNext()
must be called to make it point to the first element in the list.
Chapter 3: Classes
The following example demonstrates how to create a list, then move through it
and extract values from it.
1. Create a new List object, specifying the data type it will contain.
2. Add members to the List object, using the List.addEnd() and
List.addStart() methods.
3. Set the ListEnumerator object using the List.getEnumerator()
method.
4. Go to the start of the list, using ListEnumerator.reset().
5. Move through the members of the list, using
ListEnumerator.moveNext().
6. Pull the value of the current member in the list, using
ListEnumerator.current().
List
integerList = new List(Types::Integer);
ListEnumerator enumerator;
// Add some elements to the list
integerList.addEnd(1);
integerList.addEnd(2);
integerList.addStart(3);
// Set the enumerator
enumerator = integerList.getEnumerator();
// Go to beginning of enumerator
enumerator.reset();
//Go to the first element in the List
enumerator.moveNext();
// Print contents of first and second elements
info(strfmt("First element is %1", enumerator.current()));
enumerator.moveNext();
info(strfmt("Second element is %1", enumerator.current()));
When the code is executed, the result in the infolog should be as follows:
First element is 3
Second element is 1
Map
A Map object associates one value (the key) with another value. Both the key
and value can be of any valid X++ type, including objects. The types of the key
and value are specified in the declaration of the map. The way in which maps are
implemented means that access to the values is very fast.
3-3
moveNext() - moves the enumerator to the next pair in the map. Map
enumerators start before the first pair in the map, so moveNext()
must be called to make it point to the first pair in the map.
3-4
Chapter 3: Classes
7. Move through the members of the map, using
MapEnumerator.moveNext().
8. Pull the value of the current member in the map, using
MapEnumerator.currentKey() and
MapEnumerator.currentValue().
Map
mapStateNumbers;
MapEnumerator enumerator;
CustTable
custTable;
mapStateNumbers = new Map(Types::String, Types::Integer);
while select custTable
{
if(mapStateNumbers.exists(custTable.stateName()))
{
mapStateNumbers.insert(custTable.stateName(),
mapStateNumbers.lookup(custTable.stateName())+1);
}
else
{
mapStateNumbers.insert(custTable.StateName(), 1);
}
}
enumerator = mapStateNumbers.getEnumerator();
while (enumerator.moveNext())
{
info(strfmt("%1 customers are located in %2.",
enumerator.currentValue(), enumerator.currentKey());
}
Set
A Set is used for the storage and retrieval of data from a collection in which the
members are unique. The values of the members serve as the key according to
which the data is automatically ordered. Thus, it differs from a List collection
class where the members are placed into a specific position, and not ordered
automatically by their value.
Set members may be of any X++ type. All members in the set must have the
same type.
When a value that is already stored in the set is added again, it is ignored and
does not increase the number of members in the set.
The following methods are commonly used on Set objects:
3-5
The SetEnumerator class allows you to traverse through the members within a
set. The following methods are commonly used on SetEnumerator objects:
moveNext() - moves the enumerator to the next value in the set. Set
enumerators start before the first value in the set, so moveNext()
must be called to make it point to the first value in the set.
The following example demonstrates how to create two sets, and then compares
each set to detect any shared members and remove them.
1. Create two new Set objects, specifying the data type they will
contain.
2. Add members to the Set objects, using the Set.insert() method.
3. Set a SetEnumerator object for one of the Sets, using the
Set.getEnumerator() method.
4. Go to the start of the set, using SetEnumerator.reset().
5. Move through the members in the set, using
SetEnumerator.moveNext().
6. Pull the value of the current member in the set, using
SetEnumerator.current().
7. Search for the value in the second set, using Set.in(). If found,
remove it using Set.remove().
Set
Set
SetEnumerator
Int
setOne;
setTwo;
enumerator;
value;
3-6
Chapter 3: Classes
while (enumerator.moveNext())
{
value = enumerator.current();
if (setTwo.in(value))
{
setTwo.remove(value);
}
Array
Array objects may hold values of any single type, including objects and records.
Objects of this type may be transferred to functions and methods. The values are
stored sequentially.
The following methods are commonly used on Array objects:
The following is an example using an Array object that holds three different
Query objects:
Array array = new Array (Types::Class);
array.value(1, new Query());
array.value(2, new Query());
array.value(3, new Query());
Struct
Struct objects (short for structures) are entities that can hold a number of values
of any X++ type. Structs are used to group information about a specific entity.
For example, there may be a need to store information about a customer's name,
group and address and then treat this compound information as one item.
The following methods are commonly used on struct objects:
3-7
The fields in a struct are specified by a data type and a name. Fields can be
added when the struct is created by using the new() method, or they can be
created after the struct has been created using the add() method. If a field is
added using the add() method, the value is specified for the field at the same
time, and the data type is inferred from this value. If a field is added during the
instantiation of the struct, the value() or the valueIndex() method needs to be
used to assign a value to it.
The fields in a struct are arranged in alphabetical order of the field names.
The following is an example using the Struct object.
Struct struct = new Struct();
int i;
// Add new fields and values
struct.add("Group", "DOM");
struct.add("Name", "Jane Doe");
struct.add("Shoesize", 45);
// Prints the type and name of all items in the struct
for (i = 1 ; i <= struct.fields() ; i++)
{
info(strfmt("%1 : %2", struct.fieldType(i),
struct.fieldName(i)));
}
3-8
String : Group
String : Name
Integer : Shoesize
Chapter 3: Classes
RecordSortedList
The RecordSortedList class inserts multiple records in a single database trip,
and can hold a subset of data from a table in a particular sort order that does not
exist as an index.
A RecordSortedList object holds records from a single table. The list has a
unique key that is defined by the fields listed by using the sortOrder method.
Records are automatically sorted as they are inserted, they do not have to be
inserted in sort sequence.
RecordSortedList objects are particularly useful for passing a result-set as a
parameter.
There is no limit to the size of a RecordSortedList object, but they are
completely memory-based, so there are potential memory consumption problems.
Use a RecordSortedList in preference to a temporary table when:
The number of records is not too high (to avoid memory problems).
Are faster
Require a call between the client and server per (grouped) read
del(common) - removes a record that has a key that matches the key
fields in the recordBuffer from the RecordSortedList.
first() - positions the list to the first record in the list, and copies its
contents to the recordBuffer.
next() - sets the record buffer to the contents of the next record in the
RecordSortedList and positions the list to the record returned.
3-9
RecordInsertList
The RecordInsertList class provides array insert capabilities in the kernel. This
allows you to insert more than one record into the database at a time, which
reduces communication between the application and the database.
Records are inserted only when the kernel finds the time appropriate, but they are
inserted no later than the call to the add() and insertDatabase() methods. These
same methods also return the accumulated number of records that are currently
inserted, which allows you to keep track of when the records are actually
inserted.
The array insert operation automatically falls back to classic record-by-record
inserts when non-SQL based tables are used (for example, temporary tables), or
when the insert method on the table has been overridden.
3-10
Chapter 3: Classes
RecordInsertList is similar to RecordSortedList, but it has built-in client/server
support (it automatically packs data from one tier to another when needed), and it
lacks the sort order features available in RecordSortedList.
The following methods are commonly used on RecordInsertList objects:
3-11
Challenge Yourself!
Make a map that holds a Country Name and a count of all Customers that have a
postal address in that Country. Enumerate through the map and print the result.
Step by Step
1. In the AOT create a new class called CreateCountryCountMap.
2. Add the following code to the classDeclaration and to four new
methods called searchCustomers, loopCountries, run and main.
3. Save your changes to the class.
4. Right-click on the class and select Open.
5. Verify the results are what you would expect.
public class CreateCountryCountMap
{
Map countryMap;
}
private void searchCustomers()
{
CustTable custTable;
Counter cnt;
LogisticsAddressCountryRegionId countryId;
LogisticsAddressCountryRegionName countryName;
countryMap = new Map(Types::String, Types::Integer);
3-12
Chapter 3: Classes
while select custTable
{
countryId =
custTable.postalAddress().CountryRegionId;
countryName =
LogisticsAddressCountryRegion::find(countryId).displayName(
);
cnt = 0;
if (countryMap.exists(countryName))
{
cnt = countryMap.lookup(countryName);
countryMap.remove(countryId);
}
cnt++;
countryMap.insert(countryName, cnt);
3-13
Form Classes
The Form classes enable you to manipulate, create, modify, or run forms by
using X++ code. You can also modify forms during run time so, for example, one
or more controls are hidden on a form, depending on the user's selections in the
preceding form.
The Form classes are all system classes and are prefixed with Form. For
example, FormRun, FormStringControl.
The most important classes are described in the following table:
3-14
Description
Form
FormRun
FormDesign
FormBuildDesign
FormDataSource
Chapter 3: Classes
System class name
Description
FormBuildDataSource
FormControl
Form<control
name>Control
(e.g. FormStringControl,
FormTreeControl, etc)
FormBuild<control
name>Control
(e.g.
FormBuildStringControl
,
FormBuildTreeControl,
and so on.)
The following example demonstrates how to create and then run a form using
X++ code:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
3-15
args;
form;
formRun;
fbDesign;
fbDS;
fbGrid;
fbStr1;
fbStr2;
fbTab;
fbTabPage1;
fbTabPage2;
dictTable;
idx, idx2, idx3;
fctTabPage
fctTab
fctGrid
fctString
=
=
=
=
FormControlType::TabPage;
FormControlType::Tab;
FormControlType::Grid;
FormControlType::String;
3-16
Chapter 3: Classes
fbTabPage2 = fbTab.addControl(fctTabPage,"General");
fbTabPage2.caption("General");
// Add grid onto tab 1 and data fields onto grid.
fbGrid = fbTabPage1.addControl(fctGrid,"Table Grid");
fbGrid.addDataField(fbDS.id(),
dictTable.fieldName2Id("AccountNum"));
fbGrid.addDataField(fbDS.id(),
dictTable.fieldName2Id("CustGroup"));
fbGrid.addDataField(fbDS.id(),
dictTable.fieldName2Id("Currency"));
// Add string fields to tab 2, and assign datasource
fbStr1 = fbTabPage2.addControl(fctString,"String 1");
fbStr1.dataSource(fbDS.id());
fbStr1.dataField(dictTable.fieldName2Id("AccountNum"));
fbStr2 = fbTabPage2.addControl(fctString,"String 2");
fbStr2.dataSource(fbDS.id());
fbStr2.dataField(dictTable.fieldName2Id("CustGroup"));
Query Classes
Query classes were introduced in "Chapter 2: Working with Data." This lesson
provides a demonstration using these classes.
This example demonstrates how to use the Query classes to build a query.
1.
2.
3.
4.
5.
3-17
3-18
Chapter 3: Classes
Challenge Yourself!
Create methods that build and use a query to display data. It should prompt the
user for the sales order number and then list all item id's and item names of lines
on that sales order.
Step by Step
1. In the AOT create a new class called SalesListItems.
2. Add the following code to the classDeclartion and to four new
methods called buildQuery, listItems, run and main.
3. Save your changes to the class.
4. Right-click on the class and select Open.
5. Verify the results are what you would expect.
class SalesListItems
{
QueryRun queryRun;
}
private void buildQuery()
{
Query query;
QueryBuildDataSource dataSource;
query = new query();
dataSource = query.addDataSource(tableNum(SalesLine));
query.addQueryFilter(dataSource, fieldStr(SalesLine,
SalesId));
}
3-19
if (queryRun.prompt())
this.listItems();
3-20
Chapter 3: Classes
Always use the application classes rather than using the system
classes directly.
Never instantiate these classes (except for the Session class). Use the
global variables listed below instead.
Application class
System class
Global variable
Runs on
Session
xSession
(none)
Application
xApplication
appl
Server
Company
xCompany
appl.company
Server
Info
xInfo
Infolog
Client
ClassFactory
xClassFactory
classFactory
Global
xGlobal
(none)
VersionControl
xVersionControl
versionControl
Client
xApplication
The xApplication class provides access to information about the current
application. It also provides some application level functions.
The Application class extends the xApplication class, and allows its methods to
be overridden.
3-21
xCompany
The xCompany class provides access to information about the current company
(dataArea). It also provides some application level functions.
The Company class extends the xCompany class, and allows its methods to be
overridden.
Some of the more commonly used and overridden methods on Company are:
xInfo
The xInfo class provides access to the InfoLog framework.
The Info class extends the xInfo class, and allows its methods to be overridden.
Some of the more commonly used and overridden methods on Info are:
xGlobal
The xGlobal class provides a large selection of functions, designed to be used
anywhere in the application.
The Global class extends the xGlobal class, and allows its methods to be
overridden, and for extra global methods to be created. Many of the methods on
Global provide useful data manipulation functions. (For example,
numeralsToTxt(), str2con(), utcDateTime2SystemDateTime()).
3-22
Chapter 3: Classes
The static methods on Global are special in that they can be called from any
place in the application, without having to specify the Global class. For example,
Global::time2StrHMS() can be called as time2StrHMS(). In this way, they
become more like the system functions.
xClassFactory
The xClassFactory class is used by the system to create objects from classes.
One analogy of this is, a Class is a blueprint, xClassFactory is a factory and an
Object is the finished product.
The ClassFactory class extends the xClassFactory class, and allows its methods
to be overridden.
Some of the more commonly used methods on ClassFactory are:
The following example demonstrates how to use ClassFactory to create and run
the CustTable form using X++:
1. Create a new Args object, passing in formStr(CustTable).
2. Call ClassFactory.formRunClass() passing in the args object, and
placing returned object into a FormRun variable.
3. Call the init(), run() and detach() methods on the FormRun object.
Args
args;
FormRun formRun;
args = new Args(formstr(CustTable));
formRun = classFactory.formRunClass(args);
formRun.init();
formRun.run();
formRun.detach();
3-23
Challenge Yourself!
Create a global method which converts a date value to its equivalent
utcDateTime value. Write a method to accept a date value and set the time
component equal to midnight. Use the new global method to output the
utcDateTime of a specified date to the screen using a job.
Step by Step
1.
2.
3.
4.
5.
6.
7.
8.
3-24
Chapter 3: Classes
RunBase Framework
The functions in Microsoft Dynamics AX can be categorized into three basic
groups:
Data entry/queries
Reporting
Data manipulation
Built-in support for common tasks like query criteria selection and
progress bar.
3-25
ClassDeclaration
pack
unpack
run
description (static)
main (static)
dialog
getFromDialog
If the dialog methods are not overridden, this provides a standard blank dialog.
ClassDeclaration
The classDeclaration consists of three types of definitions:
3-26
Chapter 3: Classes
fromDate,
toDate
#ENDMACRO
}
The individual fields of the dialog will be initialized in the method dialog().
When the dialog is accepted by the user, the contents of the dialog fields are read
in the method getFromDialog(). As both methods have to access the same
variables, they are defined as members of the class.
The manipulation in the method run() uses the information from the dialog. This
information is stored in variables with specific data types. These variables are
initialized in the method getFromDialog() and read in the method run().
The information for the last used dialog values is packed into a container
structure. The variables that should be packed are defined in the CurrentList
macro in the classDeclaration.
Dialog
This method builds a dialog and initializes the fields that will be used to capture
data from the user. These variables can be automatically initialized with the same
data selected in the last run.
Typically, developers initialize the dialog object by a call to super() and then add
fields and other controls, afterward. The dialog() method for this example has the
following contents.
protected Object dialog()
{
DialogRunBase dialog;
DialogGroup groupPeriod;
dialog = super();
dialogAccount =
dialog.addFieldValue(extendedTypeStr(CustAccount),
custAccount);
groupPeriod = dialog.addGroup("Period");
dialogFromDate =
dialog.addFieldValue(extendedTypeStr(FromDate), fromDate,
"Period from");
dialogToDate =
dialog.addFieldValue(extendedTypeStr(ToDate), toDate,
"Period to");
return dialog;
3-27
GetFromDialog
This method is called immediately after the dialog is accepted by the user, and
before the run() method is called. It is used to transfer the information from the
dialog fields into the class variables.
The method has the following content.
public boolean getFromDialog()
{
boolean ret;
ret = super();
custAccount = dialogAccount.value();
fromDate
= dialogFromDate.value();
toDate
= dialogToDate.value();
return ret;
}
3-28
Chapter 3: Classes
Pack
The task for this method is to return a container with the information. This can be
used to initialize a similar data manipulation on another computer and/or at
another time. The information packed should be sufficient to communicate
information from the dialog to the data manipulation in the run-method.
The container should contain a version number as the first element. This number
should control how the rest of the container is structured. If you change the
contents of the container, you should increment the version number.
The pack method frequently has the following basic contents. However, it cannot
be inherited as it contains references to macros defined locally in the class
declaration.
public container pack()
{
return [#CurrentVersion,#CurrentList];
}
To save and restore the dialog between each use of the class. The
information is usually saved on a per user/company basis.
To transfer the object from the client to the server. This is done to
optimize both the user dialog and the data manipulation in an AOS
environment.
Unpack
The unpack() method is the counterpart to the pack() method. It receives a
container as a parameter and restores the type specific variables of the class. The
method returns a Boolean with the value true if the information could be restored.
The unpack() method handles the current version number as a minimum, which
is defined in the ClassDeclaration. You can select to support unpacking of older
versions by extending the switch statement.
The unpack() method has the following content.
public boolean unpack(container _packedClass)
{
Version version = RunBase::getVersion(_packedClass);
switch (version)
{
case(#CurrentVersion) :
3-29
}
return true;
}
Run
The run() method controls the data manipulation. The method can use the
variables defined in the classDeclaration and initialized from the dialog. The
method does not receive any formal parameters.
The content of the run() method is unique for each data manipulation, based on
requirements. It will typically contain some local variables and a "try-catch"
statement with TTS calls that wrap all physical data manipulation into one logical
transaction.
The method for our example has the following content.
public void run()
{
CustTrans custTrans;
select
where
&&
&&
Description
The description() method returns a descriptive name for the data manipulation.
The value is used to identify the job in the batch queue and is used as the caption
in the dialog.
The method for this example is as follows.
static client server ClassDescription description()
{
return "Sum customer transactions";
}
3-30
Chapter 3: Classes
This method is static, which means that it can be executed on the opposite tier of
the object. In this case, because the method does not contain user interface or
data interface, the specification of both client and server results in an execution
of the method on the tier it is called from regardless of the settings on the class
properties.
Main
The main() method is the entry point when the class is executed from a menu
item. The method is static. It defines and initializes the object. Notice that new()
should never accept any parameters. The main method receives one formal
parameter that is an args() object. This object is described further.
The main() method is also responsible for calling prompt(), which executes the
dialog, and then calling run() to perform the manipulation.
static void main(Args _args)
{
DemoRunBase demoRunBase;
demoRunBase = new DemoRunBase();
if (demoRunBase.prompt())
{
demoRunBase.run();
}
}
With a main() method in place, a RunBase class can now be called from a menu
item in the AOT.
3-31
Parm methods would also be required for the toDate and fromDate variables.
The following illustrates how you can execute the class directly from X++,
instead of from a menu item.
static void DemoRunBaseJob(Args _args)
{
CustAccount custAccount;
FromDate
fromDate;
ToDate
toDate;
DemoRunBase demoRunBase;
custAccount = "4001";
fromDate
= mkdate(1,1,2006);
toDate
= mkdate(31,12,2006);
demoRunBase = new DemoRunBase();
demoRunBase.parmCustAccount(custAccount);
demoRunBase.parmFromDate(fromDate);
demoRunBase.parmToDate(toDate);
demoRunBase.run();
}
This approach can also be used to transfer information from the main() method
to the object. If you combine a dialog with the above approach, call the method
getLast() on the RunBase object before activating the parm*() methods.
Otherwise, the prompt() method restores the choices from the last execution and
overwrites the initialization.
Integration of a Query
To incorporate a query into the dialog of a RunBase class, implement the
following methods and modifications. The following examples will replace the
CustAccount, FromDate and ToDate dialog fields, with a single query on the
CustTrans table, to allow the user to specify their own criteria.
3-32
Chapter 3: Classes
classDeclaration
The classDeclaration needs some additional variables defined to incorporate a
query. Define a class variable of the type QueryRun. This variable holds a
handle to the object which executes the query.
The classDeclaration now resembles the following content.
public class DemoRunBaseQuery extends RunBase
{
QueryRun queryRun;
#DEFINE.CurrentVersion(1)
#LOCALMACRO.CurrentList
#ENDMACRO
}
InitParmDefault
The RunBase framework calls the initParmDefault() method when initializing a
new object of a RunBase class, if the class does not have previous initialization
data stored in Usage data.
The following content shows you how to build a query dynamically.
public void initParmDefault()
{
Query
query;
QueryBuildDataSource dataSource;
super();
query = new Query();
dataSource = query.addDataSource(tableNum(CustTrans));
dataSource.addRange(fieldNum(CustTrans, AccountNum));
dataSource.addRange(fieldNum(CustTrans, TransDate));
queryRun = new QueryRun(query);
}
If a class has data in Usage data, new objects are initialized with the unpack
method.
3-33
ShowQueryValues
The showQueryValues() method indicates if the query should be active in the
dialog. If the user must access the query in the dialog, override this method and
return true.
public boolean showQueryValues()
{
return true;
}
This method should now include the information stored in the queryRun object.
Do this by adding the result of the queryRun.pack() method to the container:
public container pack()
{
return [#CurrentVersion, queryRun.pack()];
}
When combining dialog fields with a query, the first container still contains the
#CurrentList macro:
public container pack()
{
return [#CurrentVersion, #CurrentList,
queryRun.pack()];
}
The unpack() method re-initializes the queryRun object from the saved
information. Do this as shown by the following content.
public boolean unpack(container _packedClass)
{
Version version = RunBase::getVersion(_packedClass);
container packedQuery;
switch (version)
{
3-34
Chapter 3: Classes
case #CurrentVersion:
[version, packedQuery] = _packedClass;
if (packedQuery)
{
queryRun = new QueryRun(packedQuery);
}
break;
default :
return false;
}
return true;
When combining dialog fields with a query, the unpack of the container contains
the #CurrentList macro.
[version, #CurrentList, packedQuery] = _packedClass;
Run
The run() method can use the queryRun object directly as it is initialized after
the user has accepted the dialog.
The run() method is implemented as shown in the following content.
public void run()
{
AmountMST amountMST;
CustTrans custTrans;
while (queryRun.next())
{
custTrans = queryRun.get(tableNum(CustTrans));
}
}
amountMST += custTrans.AmountMST;
3-35
3-36
Chapter 3: Classes
Challenge Yourself!
Make a new class called SalesCalcAverageSalesPrice, using the RunBase
framework. The class should prompt for an Item Number, and To and From
dates. The run() method should calculate the average sales price of all sales lines
for that Item, with order creation dates between the dialog To and From dates.
Put the result in the infolog.
Step by Step
1. In the AOT create a new class called SalesCalcAverageSalesPrice.
2. Add the following code to the classDeclartion
3. Override the methods run, pack, unpack, dialog and getFromDialog
and enter the code shown below.
4. Create a new method called main and enter the code shown below.
5. Save your changes to the class.
6. Right-click on the class and select Open.
7. Enter from and to dates in the dialog box and enter an item id.
8. Verify the results are what you would expect.
class SalesCalcAverageSalesPrice extends RunBase
{
DialogField dlgFromDate, dlgToDate, dlgItemId;
FromDate fromDate;
ToDate toDate;
ItemId itemId;
}
protected Object dialog()
{
Dialog dlg = super();
dlgFromDate = dlg.addField(identifierStr(FromDate));
dlgToDate = dlg.addField(identifierStr(ToDate));
dlgItemId = dlg.addField(identifierStr(itemId));
return dlg;
}
3-37
return ret;
3-38
Chapter 3: Classes
Args Object
The Args class defines information communicated between running application
objects. The application object that initiates the communication (the caller),
initializes an Args object containing some information and then passes it to the
application object that is being called.
Information transferred by the args object includes the following:
Method
Data Type
Information
record
Common
parmEnumType
Int
parmEnum
AnyType
An enumerated value.
parmObject
Object
parm
Str
Communication through the args object can occur automatically without any
X++ programming.
If the caller is activating the called object by a menu item the Args object is
automatically initialized and sent as a parameter to the object called. AOT
properties of the menu item will be used.
If the called object is a form or a report, it can automatically apply to the
information send by the Args object. Forms will automatically try to make a
delayed synchronize with the args.record().
However, you can program one or both sides of the communication yourself.
3-39
This code shows an example of how to replace a dialog object with a form from
the AOT, within the RunBase framework. The form called can access the
LedgerConsolidate class through args.caller(). This way, the form and class can
exchange information.
The following is an example of programming the called side
(Classes\BankChequeCancel\main).
public static void main(Args args)
{
BankChequeCancel chequeCancel;
Args localArgs = new Args();
;
if (args.record())
{
switch (args.dataset())
{
case tablenum(BankChequeTable) :
chequeCancel =
BankChequeCancel::newBankChequeTable(args.record());
break;
default :
throw
error(strfmt("@SYS22828","@SYS22512"));
}
if (chequeCancel.prompt())
{
localArgs.caller(chequeCancel);
localArgs.record(args.record());
3-40
Chapter 3: Classes
BankChequeCancel::serverRun(localArgs);
Notice that this method also declares, initializes, and uses a local Args object for
calling the static method BankChequeCancel::serverRun().
3-41
Challenge Yourself!
Modify the class SalesCalcAverageSalesPrice so when it is called with an active
InventTable record, the class should set the item id based on this record and not
prompt the user for the item id.
Step by Step
1. In the AOT, find the SalesCalcAverageSalesPrice class.
2. Add a new method parmItemId as shown below.
3. Modify the main, dialog and getFromDialog methods as shown
below.
4. Drag the class to the Action node of the Menu Items node in the
AOT.
5. In the property sheet of the menu item just created, set the label to
Average sales price.
6. In another AOT, find the form
EcoResProductPerCompanyListPage.
7. Drag the menu item to the node Designs > Design > ActionPane >
ActionPaneTabSell > ButtonGroupSellOrders.
8. Saves your changes to the form.
9. In the application workspace navigate to Product information
management > Common > Released products.
10. Select item 1001 and click Sell > Average sales price.
11. Enter a From date and a To date and click OK.
12. The infolog will display the average sales price for that item. Click
OK to close the infolog.
private itemId parmItemId(itemId _itemId = itemId)
{
itemId = _itemId;
return itemId;
}
static void main(Args _args)
{
SalesCalcAverageSalesPrice SalesCalcAverageSalesPrice
= new SalesCalcAverageSalesPrice();
InventTable inventTable;
3-42
Chapter 3: Classes
if (_args.dataset() == tableNum(InventTable))
{
InventTable = _args.record();
SalesCalcAverageSalesPrice.parmItemId(InventTable.ItemId);
}
if (SalesCalcAverageSalesPrice.prompt())
SalesCalcAverageSalesPrice.run();
return dlg;
return ret;
3-43
Summary
Using system classes in conjunction with your modifications will help leverage
existing frameworks, and access important system data, to ultimately provide a
richer and more consistent Microsoft Dynamics AX enhancement for end users.
3-44
Chapter 3: Classes
3-45
7. Which method on Args should only be used when no other methods will
suffice?
( ) parm()
( ) parmEnum()
( ) parmEnumType()
( ) record()
3-46
Chapter 3: Classes
2.
3.
3-47
Solutions
Test Your Knowledge
1. Which of the following are Collection classes? (Select all that apply)
() Struct
() Map
( ) Container
() List
2. What class is used to iterate through the elements stored in a Map object?
MODEL ANSWER:
MapEnumerator.
MapIterator is also correct, but a secondary choice.
3. RecordSortedList and RecordInsertList have some similarities but have
different uses. Which of the following is a major difference between the two
classes?
() RecordInsertList lacks the sort order features that are available in
RecordSortedList.
( ) RecordSortedList lacks the sort order features that are available in
RecordInsertList.
( ) RecordInsertList cannot insert multiple records into the database in
one trip.
( ) RecordSortedList cannot insert multiple records into the database in
one trip.
3-48
Chapter 3: Classes
4. Match the following Application Object classes to their description:
h 1. Form
f 2. FormRun
e 3. FormBuildDesign
a 4.
FormBuildDataSource
g 5.
FormBuildStringControl
c 6.
FormBuildTreeControl
b 7.
FormQueryDataSource
d 8. FormExecute
3-49
3-50
Chapter 4: Forms
CHAPTER 4: FORMS
Objectives
The objectives are:
Modify form methods to the control how the form behaves when it
opens and closes.
Introduction
This lesson provides a comprehensive foundation for using forms to interact with
the end-user.
The following topics will be discussed:
Forms architecture
Fetch of data
Advanced controls
Special forms
4-1
Architecture
A form supports the interaction between the user and the database. It focuses on
the following actions.
Filtering data
Sorting data
Modifying data
The application and business logic are not integrated in forms but programmed
on the underlying tables and classes.
The following figure illustrates the structure of a form as displayed in the AOT.
Methods
Data sources
Parts
Designs
Permissions
4-2
Chapter 4: Forms
Methods
System generated methods, also known as system methods, standard methods or
object methods, attached to the form are used to control the startup and shut
down of the form. These events are frequently overridden to make initialization
of the form. By default, the system methods are not displayed. To override a
system method, right-click on the Methods node, select Override Method and
then select the methods to override.
Other methods can be added and called from anywhere in the form. It is good
practice to add methods here rather than writing large chunks of code in the
objects deeper in the form design, as it makes locating code simpler.
Variables defined in the class declaration of the individual form can be accessed
from all methods in the form and can be used to hold the status of the form
controls.
The following illustration shows the CustTable form methods. The form system
methods task, run, init and close have been overridden. All other methods have
been added to the Methods node and are specific to this form.
Data Sources
Data source(s) define the interface between the form and the database. An entry
is added for each table. The properties of the individual data sources determine
the relationship between the data sources. Each data source also has system
methods that can be overridden to control the display of data or act on an event.
4-3
Use the list of fields as a source for drag-and-drop operation when you build the
design of the form. You cannot drag fields directly from the underlying table as
the form must know which data source the field is linked to.
Field groups display below the list of fields. They can also be used as the source
for drag-and-drop operation.
Notice that after accessing the form in the AOT the definition of the data source
is cached locally in the definition of the form. If changes are made when you
create a new field in the underlying table, you must restore the form to access the
new field in the form. Remember to save the form before clicking Restore from
the shortcut menu.
The following illustration shows the data source on the CustTable form.
Design
The design of the form defines the interface between the form and the user and
controls the layout and presentation of the data. The design consists of
components called controls. Some controls are used to edit and display data,
while others are used to control the layout of the form.
4-4
Chapter 4: Forms
The following illustrates the normal design of a form:
Design
Tab
TabPage
Grid
Fields to be displayed on Grid
TabPage
Field Groups
Fields in Field Group
All controls on the design have properties and methods that can be overridden.
Under the Designs node, as well as the Design node, there is also a DesignsList
node. Design is the hierarchy of controls built from the DesignList using the
HierarchyParent and ElementPosition properties. DesignList is the flat list of all
controls that populate metadata store.
FormRun
Element
FormDataSource
FormDataObject
FormDataSource.Object(<fieldId>)
FormDesign
FormRun.design()
FormControl
4-5
Query
QueryRun
The method control() on the element object can be used if the control is included
in an auto field group. In this situation you cannot use the auto declaration
feature.
Data Sources
Adding a data source to a form allows the form to display the data from the table
specified on the data source. A table in a data source can be sorted and filtered by
the user, which is an important feature of Microsoft Dynamics AX.
Forms can contain any number of data sources, and each data source can be
joined to another data source on the form using the defined relations between the
tables. Relations or Dynalinks can also be added in the form code.
A form data source produces a query that can be manipulated the same as any
other query in the system.
Joins
To display records from two tables on one form, you can limit the records in one
of the tables to the related records in the other table. You can do this by
specifying a join between the two tables.
The join is specified on the JoinSource property of the second or joined table.
Enter the data source name of the main data source here.
You can also specify the type of join or link. The types of link are described later
in the course.
4-6
Chapter 4: Forms
An example of a form that uses join is the form CustInvoiceJour. The
CustInvoiceTrans data source is joined to the CustInvoiceJour datasource, and
the InventDim data source is joined to the CustInvoiceTrans datasource. The
following figure shows the queries in the form and the properties of the
InventDim datasource.
4-7
4-8
Chapter 4: Forms
Combine the sorting with some aggregated fields, which makes the data source
display aggregate information from the table instead of transactions. Perform the
following steps to show the sum of quantity of inventory transactions shown per
Item Id:
1. Group the data by item id using the addGroupByField on the
datasource.
2. Add Sum(Qty) as a SelectionField
FormDataSource.InitValue
This method is used to initialize a new record with default values. The super() of
this method calls the initValue() method on the underlying table.
If you have initialization that applies system-wide, put the code on the table.
FormDataSource.Active
This event is called when a new record becomes active in the data source. This
method is typically overridden to change properties which depend on the
contents of the current record: Commonly this method will:
Enable/Disable buttons
FormDataSource.LinkActive
This method is the engine that joins data sources. This method is called on the
joined data source every time that the active record in the main data source is
changed. The method is also called when the form is opened as the system tries
to join the calling data source to the main data source of the called form.
4-9
FormDataSource.Write
This method controls the insert and update of records. The super() of this method
calls the corresponding method on the underlying table.
If you have form-specific tasks to perform in relation to the commitment of the
record, add it here.
4-10
Chapter 4: Forms
List of Datasource Properties
The following list describes each property available on a form data source. The
list can also be found in the Developer Help.
Property
Description
Name
Table
Index
CounterField
AllowCheck
AllowEdit,
AllowCreate,
AllowDelete
StartPosition
This option specifies if the form sets the first or the last
record active, when the form is opened. Only change this
property when you have to as the user pays a performance
penalty by finding the last record.
4-11
Description
AutoSearch,
AutoNotify,
AutoQuery
4-12
Cross
Company
AutoQuery
OnlyFetch
Active
JoinSource
Chapter 4: Forms
Property
Description
LinkType
4-13
Description
DelayActive
InsertAtEnd,
InsertIf
Empty
4-14
ValidTime
StateAuto
Query
ValidTime
StateUpdate
Chapter 4: Forms
List of Datasource Field Properties
The following list describes each property available on each field on a form data
source. The list can also be found in the Developer Help.
Property
Description
AllowEdit
Enabled
Skip
Visible
AllowAdd
Mandatory
4-15
Challenge Yourself!
Create a new form that has two data sources, CustTable and CustInvoiceJour.
CustInvoiceJour should be joined to CustTable. Ensure the join displays all
invoices but not customers that have not been invoiced. Add a filter that only
retrieves records posted after the first day of the current month.
You may have to post some invoices to get some that were during this month.
Step By Step
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
4-16
Chapter 4: Forms
public class FormRun extends ObjectRun
{
QueryBuildRange
QBRInvoiceDate;
}
// this is the init method on the CustInvoiceJour data
source
public void init()
{
super();
QBRInvoiceDate =
this.query().dataSourceTable(tableNum(CustInvoiceJour)).add
Range(fieldNum(CustInvoiceJour, InvoiceDate));
}
// this is the executeQuery method on the CustTable
dataSource.
public void executeQuery()
{
fromDate
fromDate =
mkDate(1,mthofyr(systemDateGet()), year(systemDateGet())) ;
QBRInvoiceDate.value(sysQuery::range(fromDate,
systemDateGet()));
}
super();
4-17
Form Controls
Form controls refers to all of the objects in a form design. Tabs, tab pages, field
groups, grids, fields and buttons are all examples of form controls. Controls are
arranged on the form using Intellimorph technology.
All data is displayed through field controls. As with fields on a table, each field is
a specific data type, and can also be an Extended Data Type. There are three
types of field controls: Bound, Unbound and Calculated.
Bound controls are associated with a field in an underlying table. Use bound
controls to display, enter, and update values from fields in the database.
Unbound controls do not have a data source, but can be a specific Extended Data
Types.
Calculated controls use a method as the data source. An example of a calculated
control is the sum of two fields on a form.
IntelliMorph
IntelliMorph is a technology that simplifies the modification of forms. For
example, IntelliMorph lets users hide and show fields. Individual users can safely
and swiftly rearrange fields to best suit their work preferences without modifying
the form through the AOT (Application Object Tree). IntelliMorph also makes it
possible to quickly redesign forms. Field labels can be modified without
disturbing field data or underlying business logic.
When the form is displayed the width of the labels are adjusted to the actual
language. Fields that are not active because of configuration or security are
removed. The formatting of date, time and numbers is performed according to the
settings in the operating system.
The design of the form should be made to support IntelliMorph. This includes the
following guidelines for the properties on the controls:
4-18
Use the values column width and column height for the properties
width and height to support manual resizing of the form.
Chapter 4: Forms
Display Methods
A display method is used to display a calculation or data from another table. The
method modifier Display is added to the beginning of the method definition line
to allow the returned value to be displayed on a form (or report).
Display methods can be created either on a table or on a form data source. If it is
created on the form data source, then it takes the table as a parameter. If possible
a display method should be created on the table itself, but there are situations
where it has to be on the form data source, for example, if it uses an unbound
control on the form.
The following example is from the InventJournalTable form, and displays the
user ID of the user that is locking a journal.
//BP Deviation documented
display JournalBlockInUseUserId inUseUserId(JournalTableMap
_journalTable)
{
return
journalFormTable.datasourceMethodInUseUserId(_journalTable)
;
}
Edit Methods
Edit methods are similar to display methods in that they can return calculated
values or data from another table. They can also be edited. An edit method takes
two more parameters than a display method. The first parameter is Boolean and
is True if the user has edited the field. The last parameter is the value that has
been entered in the case that a user has edited the field. If the edit method is on a
form data source, the second parameter is the table.
The following example is from the form CustTransOpen which is used to settle
transactions against each other. This method is used to mark which transactions
are to be settled.
// BP Deviation documented
public edit NoYes editMarkTrans(boolean _set, CustTransOpen
_custTransOpen, NoYes _markTrans)
{
NoYes returnMarkTrans;
;
if (_set)
{
manager.updateTransMarked(_custTransOpen,
_markTrans);
element.updateDesignDynamic();
this.refresh();
}
4-19
return returnMarkTrans;
Unbound Controls
Unbound fields are fields of a specific type, but not attached directly to a table.
These fields inherit the properties of the data type including any relations to a
table. An unbound control can be used for filtering record to be displayed or to
display images or static text.
The following illustration shows the properties of an unbound control in which
an item ID can be entered. The lookup of the item table is still available through
the relations on the Extended Data Type.
4-20
Chapter 4: Forms
Common Methods to Override
FormDataObject.Modified
Use this method when the user changes the contents of the field. This event can
be used to make calculations or other operations which depend on the field.
For system-wide functionality during a manual change of a database field,
override the modifiedField method on the table.
FormDataObject.Validate
Use this method to validate the contents of the field. This event can be used to
supplement or remove the ordinary validation.
For system-wide functionality during the validation of a database field,
implement this by overriding the validateField method on the table.
4-21
Challenge Yourself!
Add a new date control field to the form, and filter the records based on this date.
Step by Step
1. Add a new DateEdit control to the design of the form created in Lab
4.1. Rename the control to FromDate.
2. Open the properties of the FromDate control.
3. Set the ExtendedDataType property to FromDate.
4. Set AutoDeclaration to Yes.
5. Close the properties form.
6. Expand the FromDate node.
7. Right-click on the methods node and select Override Method >
Modified.
8. Modify the code as shown below.
9. Edit the ExecuteQuery() method on the CustTable data source.
10. Modify the code as follows.
public boolean modified()
{
boolean ret;
ret = super();
CustTable_ds.executeQuery();
}
return ret;
4-22
Chapter 4: Forms
Form Methods
Form methods control how the form opens, runs and closes. These methods can
be overridden as with other form system methods.
If your manipulation needs access to the objects of the form, wait until the
super() call has executed.
FormRun.Run
The run method is called immediately after the init method. The super() call
makes the form window appear on the screen and performs a database search for
the data to be displayed in the form.
Typical reasons for overriding the formRun.run() method include, activating or
de-activating some fields, setting default values for controls or modifying the
query.
You must call super() if you override formRun.run() method, and you should
place your code before the super() call.
FormRun.Close
This method shuts down the form. If you have cleanup in connection with
finishing the execution of the form, place it here.
One function available for implementation is when saving some options for
future use as in \Forms\ForecastPurch\Methods\close.
4-23
4-24
Chapter 4: Forms
Challenge Yourself!
When the form opens, the date field should be set to the first day of the current
month. Ensure that the records are then filtered to only show invoices posted
after this date.
Step by Step
1. Right-click on the methods node of the form you created in the Lab
4.1, select Override Method > Run.
2. Add the code to this method as shown below.
3. Save the method and run the form.
public void run()
{
FromDate.dateValue(systemDateGet() dayofmth(systemDateGet()) + 1);
super();
}
4-25
Placement of Code
The form offers many opportunities to place code on events. Try to minimize the
X++ code in forms for the following reasons:
To program system-wide logic, which is closely related to a record, put the logic
in object methods on the table. The table which inherits from the kernel class
xRecord has many events you can override and supplement with logic.
If designing logic which is more related to the function of the form, than the
individual records in the data source, consider making a class which supports the
form. If the form can work with different variations of functions or data, this
approach supports using polymorphism.
If putting the X++ code in the form, avoid programming on the design and put
your logic on the data source. Many events on the design are dependent on
exactly how the user navigates the form. Code on the form controls does not take
the possibility of the user modifying the contents of the form into account.
Additional Controls
The following introduces additional controls that can be used on forms.
4-26
Chapter 4: Forms
ds,
fieldstr(MyTable, LedgerDimension));
}
4-27
ListView
The listview control is used to present the user with a list of options. Select one
or more of them. One example of using listview can be found in the selection of
payment file formats in the vendor module as shown in the following figure.
4-28
Chapter 4: Forms
Table
The table control looks like a spreadsheet. The grid is predefined with fields as
columns and records as row. In the table you control which type of data (control)
and which data (value) is presented in each cell.
The table control has related controls as a grid. Decide which of these controls
should be used in each cell of the table. Use the following two options for
specifying which control to use for each cell:
Call the method cell() of the table control. Specify the actual row and
column as parameters. This returns a FormTableCell object. On this
object call the method editControl with a handle to the control as a
parameter. Use this to specify all the controls in a loop construction.
Override the method data() and return the actual value of the cell.
The method is similar to the editControl().
The labels of the row and columns are specified by overriding the methods
rowLabel and colLabel of the table control.
An example of a table control is the form SysDateLookup which is used to
select a value in a date field.
The forms tutorial_Form_Table illustrates many of the functions related to the
table control and how they are programmed in X++.
4-29
4-30
Chapter 4: Forms
Window
The window control can be used to display images. An example of this is the
form displaying the company logo.
To display an image in a grid, you need to add a window control to the grid and
set the method to be a method that returns the resource Id of the image.
Use the class ImageList to build a list of images that the window can display and
then set that ImageList on the control.
Most resource Ids are listed in the macros ResAppl and Resource. You can add a
new image through the Resource node in the AOT.
For an example to show an image in a grid, examine the code in the Init form
method and the errorExists method in the LedgerJournalTrans datasource in the
LedgerJouranlTransDaily form. Also look at the form
Tutorial_Form_Windowingrid.
Managed Host
Microsoft Dynamics AX supports the display of .net managed controls. Actions
taken by the user on those controls are also handled. The following procedure
shows how to add a windows calendar control to a form and handle an event that
arises from a user action.
1. In the AOT, right-click on the Forms node and select New Form.
2. Rename the new form to DateDifferenceCalculator.
3. Right-click on the Design node and select New Control >
ManagedHost. The Managed control selector form appears.
4. In the list find the System.Windows.Forms record.
5. In the Controls list, select MonthCalendar. Click OK.
6. Rename the new ManagedHost control to CalendarControl.
7. Right-click CalendarControl and select Events. The Events form is
displayed.
8. Highlight DateSelected and click Add.
9. Click Close.
4-31
ActiveX
The ActiveX control is used to host an ActiveX component. ActiveX is a
component of another program which is installed individually and can be
integrated in other applications such as Microsoft Dynamics AX or Web pages.
There are many ActiveX programs available.
4-32
Chapter 4: Forms
When you create the control, a list of ActiveX controls installed on the actual
computer displays.
Select from the list or click 'Enter class ID' to identify the ActiveX with unique
class ID.
An ActiveX control has auto declaration set to Yes as default, as you have to
address the control to access the specific methods of the ActiveX. When you
write X++ the specific methods are included in the list of methods.
4-33
The parameters to the methods and a help text is displayed in the window of the
explorer.
ActiveX Events
Add an event to the AOT by double-clicking the event. Afterward, you can edit
and delete the event directly from the methods-node of the control.
See the documentation of the individual ActiveX for a description of the
component.
4-34
Chapter 4: Forms
Challenge Yourself!
Add a window control to the grid of the form created in Labs 4.1, 4.2, and 4.3. If
the invoice has been fully paid then the window should be blank otherwise the
window should show a Red Error icon.
Step by Step
1. Expand the designs node of the form created in Labs 4.1, 4.2, and
4.3.
2. Right-click on the Grid control, select New Control > Window.
3. In the properties of the new Window Control, rename it to
WindowOpen, set the DataSource to CustInvoiceJour, and set
AutoDeclaration to Yes.
4. Modify the ClassDeclaration as shown below.
5. Modify the run method as shown below.
6. Create a new method in the CustInvoiceJour data source and add
the code as shown below.
7. In the properties on the WindowOpen control, set the dataMethod
property to invoiceIsOpen.
8. Save and run the form.
public class FormRun extends ObjectRun
{
QueryBuildRange
QBRInvoiceDate;
imageListAppl
imageListAppl;
}
public void run()
{
#resAppl
imageListAppl = new ImagelistAppl(
Imagelist::smallIconWidth(), Imagelist::smallIconHeight());
imageListAppl.add(#ImageError);
windowOpen.imageList(ImageListAppl.imageList());
FromDate.dateValue(systemDateGet() dayofmth(systemDateGet()) + 1);
4-35
super();
4-36
Chapter 4: Forms
Summary
This lesson discusses how to join data sources on a form, add controls that can
display the data from the data source as well as data from other tables and
calculations. It also discusses the methods and properties available when creating
a form and the additional options for control types and form types.
4-37
4-38
Chapter 4: Forms
2.
3.
4-39
Solutions
Test Your Knowledge
1. Which kernel class documents the individual field in a data source and how
do you get a handle to it?
MODEL ANSWER:
The kernel class FormDataObject documents fields on a data source. You
can get a handle by using FormDataSource.Object(<fielded>)
2. Which methods do you override to make changes to the structure of the
query which fetches data to the data source?
MODEL ANSWER:
You override FormDataSource.executeQuery and FormDataSource.init().
3. What are the characteristics of a table control?
MODEL ANSWER:
The developer can select which data type the rows and columns in the table
should have and the number of columns and rows.
4-40
Introduction
With Visual Studio tools built specifically for Microsoft Dynamics AX
development, managed code that seamlessly uses elements from the AOT and
interacts with existing Microsoft Dynamics AX business logic can be written.
5-1
Application Explorer
The Application Explorer in Visual Studio is a representation of the Microsoft
Dynamics AX AOT. It is organized in the same structure as the AOT.
Like other window panes in Visual Studio, it can be toggled between visible and
invisible. To show the Application Explorer, select it from the View menu in
Visual Studio. The Application Explorer can also be docked or undocked, like
other panes in Visual Studio.
After discussing how to create a project, the Application Explorer can be used
to leverage AOT elements within Visual Studio.
5-2
Types of Projects
There are three types of Visual Studio projects that can be used to develop
customizations for Microsoft Dynamics AX:
The Report modeling and Enterprise Portal Web application project types
have their own templates provided under the Microsoft Dynamics AX template
group. These topics are more thoroughly covered in the "Reporting in Microsoft
Dynamics AX 2012" and "MorphX and Enterprise Portal Development in
Microsoft Dynamics AX 2012" courses.
5-3
System;
System.Collections.Generic;
System.Linq;
System.Text;
namespace ClassLibrary1
{
public class Class1
{
public string GetCustomerPaymentMode(string
accountNum,
string
dataAreaId)
{
string paymentMode = String.Empty;
CustTable custTable = new CustTable();
// Search for the customer.
custTable = CustTable.findByCompany(dataAreaId,
accountNum);
if (custTable.Found)
{
// Get the value for the customer's payment
mode.
paymentMode = custTable.PaymMode;
return paymentMode;
5-4
The method created is empty, and ready for the pre-event code to be added. Do
not change the name of the method, or the event handler will not work.
5-5
The following deployment properties are available in the Properties pane for the
project:
Deploy to Server
Deploy to Client
Deploy to EP
Deploy to SSRS
Deploy to Server
If the Deploy to Server property is set to Yes, the system deploys the project
output (a DLL, for example) by copying it to the server's \bin directory. The
server \bin directory might be found at a file location such as the following:
C:\Program Files\Microsoft Dynamics
AX\60\Server\MicrosoftDynamicsAX\Bin\VSAssemblies
After the managed code has been deployed, the AOS must be restarted so that the
assembly can be loaded by the AOS. If you have hot swapping enabled, the
AOS does not have to be restarted. Hot swapping allows multiple application
domains on a single AOS, allowing new versions of managed code to be
deployed and be immediately available to any new client connections. Hot
swapping can be enabled by following these steps:
1. Open the Microsoft Dynamics Server Configuration Utility by
clicking Start > Administrative Tools > Microsoft Dynamics AX
2012 Server Configuration. This must be run with administrative
credentials.
2. On the Application Object Server tab, select Allow hot swapping
of assemblies when the server is running.
3. Click OK. You will see a prompt that asks whether you want to
restart the AOS service. The change will only take effect after the
AOS is restarted.
5-6
Deploy to Client
If the Deploy to Client property is set to Yes, the system deploys the project
output (a DLL, for example) by copying it to the client \bin directory. The client
\bin directory might be found at a file location such as the following:
C:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin
5-7
5-8
Challenge Yourself!
1. Create a managed code project that uses the C# language.
2. Add the SalesTableType X++ class to the project.
Step by Step
1. Open Visual Studio.
2. Click File > New > Project.
3. Select Visual C# and then Windows from the Installed Templates
list.
4. Select Class Library template.
5. Enter a project name into the Name field and then click OK.
6. Verify the project is created and is visible in the Solution Explorer.
7. Save the project.
8. Click Build and then Build Solution.
9. Right-click on the project in the Solution Explorer and select Add
<Project Name> to AOT. Notice the project icon now has the
Microsoft Dynamics icon incorporated, indicating this project is now
connected with the AOT.
10. Open the Application Explorer in Visual Studio.
11. In the Classes node, find the SalesTableType class.
12. Drag the SalesTableType class onto the project in the Solution
Explorer. The class should now appear as a proxy in the projects
contents.
5-9
Challenge Yourself!
1. Use the Application Explorer to find the method that needs to be
handled.
2. Create a post-event handler.
3. Ensure the event handler will be deployed on the Server and Client.
Step by Step
1. Open Visual Studio.
2. If you are reusing the project from Lab 5.1, skip to step 4. Otherwise,
create a Managed Code project.
3. Right-click on the project in the Solution Explorer and select Add
<Project Name> to AOT.
4. If the Application Explorer is not visible, open it.
5. In the code editor, open the class and place the cursor somewhere
inside the class.
6. In the Application Explorer, locate the SalesTableType class.
Expand it and locate the validateWrite method.
7. Right click the validateWrite method and select Add post-event
handler.
8. Notice that an event handler method is created in the class.
5-10
Summary
By using the integrated Microsoft Dynamics AX tool set inside Visual Studio,
developers can now write managed code in any .NET language to enhance or
modify Microsoft Dynamics AX business logic. The Application Explorer tool
offers a development environment similar to that within Microsoft Dynamics
AX, making switching between the two easier and more familiar.
5-11
2. What key step in creating a managed code project will connect the project
with the AX tool set within Visual Studio?
3. What four deployment properties are available for a Visual Studio project
that has been added to the Microsoft Dynamics AX AOT?
5-12
5-13
2.
3.
5-14
Solutions
Test Your Knowledge
1. What type of Visual Studio projects can be used to integrate with Microsoft
Dynamics AX?
MODEL ANSWER:
Report modeling projects, Enterprise Portal Web application projects, and
Managed code projects.
2. What key step in creating a managed code project will connect the project
with the AX tool set within Visual Studio?
MODEL ANSWER:
Right-clicking the project and selecting Add <Project Name> to AOT.
3. What four deployment properties are available for a Visual Studio project
that has been added to the Microsoft Dynamics AX AOT?
MODEL ANSWER:
Deploy to Server, Deploy to Client, Deploy to EP, Deploy to SSRS.
4. What needs to happen before the Visual Studio debugger will activate on
breakpoints inside managed code called from the Microsoft Dynamics AX
client or server?
MODEL ANSWER:
The Visual Studio Debugger needs to be attached to either the Server or
Client process for Microsoft Dynamics AX.
5-15
5-16
Chapter 6: Workflow
CHAPTER 6: WORKFLOW
Objectives
The objectives are:
Configure a workflow.
Introduction
Workflow is a system in Microsoft Dynamics AX 2012 that allows business
processes to be automated. For example, a purchase requisition may need to be
approved by a number of different employees according to the requisition's total
amount. Each employee has to approve it before the next employee in the
approval route.
A Workflow in Microsoft Dynamics AX uses a combination of Application
Object Tree (AOT) elements created by IT and additional set up that a user can
control. This course introduces the development side of creating a workflow. To
create a workflow you will need to use skills developed from this class and the
Morph X development class.
6-1
Workflow Configuration
There are three batch jobs that manage workflow processing. These jobs are all
run on the Application Object Server (AOS) using the Batch system. To set this
up, run System Administration > Setup > Workflow > Workflow
infrastructure configuration. Enter batch groups for each process. Batch groups
can be used to control which AOS instance each workflow batch job is run on.
6-2
Chapter 6: Workflow
7. Change the Module property to SalesOrder.
8. Right-click on the newly created workflow category and select Save.
Create a Query
A workflow uses a query to define the data that is used by the workflow. It can
define one or more tables and all or selected fields on that table.
6-3
9. Click Next.
10. Click Finish. A development project with a number of newly created
elements will be displayed.
6-4
Chapter 6: Workflow
6-5
6-6
Chapter 6: Workflow
9. Change the Label property to Submit.
10. Right-click on the SalesCreditLimitApprSubmitMenuItem menu
item and select Save.
void submit(Args args)
{
// Variable declaration.
recId recId = args.record().RecId;
WorkflowCorrelationId workflowCorrelationId;
// Hardcoded type name
WorkflowTypeName workflowTypeName =
workflowtypestr(SalesCreditLimitAppr);
// Initial note is the information that users enter
when they
// submit the document for workflow.
WorkflowComment note ="";
WorkflowSubmitDialog workflowSubmitDialog;
SalesTable SalesTable;
// Opens the submit to workflow dialog.
workflowSubmitDialog =
WorkflowSubmitDialog::construct(args.caller().getActiveWork
flowConfiguration());
workflowSubmitDialog.run();
if (workflowSubmitDialog.parmIsClosedOK())
{
recId = args.record().RecId;
SalesTable = args.record();
// Get comments from the submit to workflow dialog.
note = workflowSubmitDialog.parmWorkflowComment();
try
{
ttsbegin;
workflowCorrelationId =
Workflow::activateFromWorkflowType(workflowTypeName,
recId,
note,
NoYes::No);
SalesTable.CreditLimitApprovalStatus =
SalesCreditLimitApprovalStatus::Submitted;
// Send an Infolog message.
info("Submitted to workflow.");
6-7
}
args.caller().updateWorkFlowControls();
6-8
Chapter 6: Workflow
7. Enter Overview in Document preview field group.
8. Enter SalesTableListPage in Document menu item.
9. Click Next.
10. Click Finish. A development project with a number of newly created
elements is displayed.
11. Drag SalesCLApproval approval to the Supported elements node
on the SalesCreditLimitAppr workflow type.
12. Save your changes to the SalesCreditLimitAppr workflow type
Description
WorkflowStarted
EventHandler
WorkflowCompleted
EventHandler
6-9
Description
WorkflowCanceled
EventHandler
WorkflowConfig
DataChangeEvent
Handler
6-10
Event
Description
WorkflowElement
StartedEventHandler
WorkflowElement
CanceledEventHandler
Chapter 6: Workflow
Event
Description
WorkflowElement
CompletedEvent
Handler
WorkflowElement
ReturnedEventHandler
WorkflowElemChange
RequestedEvent
Handler
6-11
Author a Workflow
Now that you have created a workflow type and enabled it on a form, you can
configure it for use.
6-12
Chapter 6: Workflow
13. Drag the bottom node of the Start box to the top node of the
Approval box.
14. Drag the bottom node of the Approval box to the top node of the
End box.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
6-13
6-14
Chapter 6: Workflow
Challenge Yourself!
Add conditions to the posting functions on the sales order form that will prevent
posting to picking, packing or invoicing until the workflow has been approved. If
the credit limit has not been reached, then the postings should be allowed.
Step by Step
1. Add the following method CanPostCreditLimit to the salesTable
table.
2. Add the following code to the methods canPickingListBeUpdate(),
canPackingSlipBeUpdated() and canInvoiceBeUpdated() in the
salesTableType class.
boolean canPostCreditLimit()
{
amountMST
creditBalance;
custTable
custTable;
;
if (this.CreditLimitApprovalStatus ==
SalesCreditLimitApprovalStatus::Approved)
return true;
if (this.CreditLimitApprovalStatus ==
SalesCreditLimitApprovalStatus::Rejected
|| this.CreditLimitApprovalStatus ==
SalesCreditLimitApprovalStatus::Submitted)
return false;
custTable = this.custTable_InvoiceAccount();
if (!custTable.CreditMax)
return true;
creditBalance = custTable.CreditMax custTable.balanceMST();
if (this.amountRemainSalesFinancial() +
this.amountRemainSalesPhysical() < creditBalance)
return true;
return false;
}
6-15
6-16
Chapter 6: Workflow
Challenge Yourself!
When a workflow is rejected, it can be resubmitted. Modify the Submit to
Workflow class so that it can resubmit the workflow after a rejection
Use the PurchReqWorkflow class as model.
Step by Step
1. Find the class SalesCLApprovalResubmitActionMgr.
2. Modify the Main method and add a new method Resubmit as shown
in the following code:
public static void main(Args _args)
{
SalesCLApprovalResubmitActionMgr
SalesCLApprovalResubmitActionMgr = new
SalesCLApprovalResubmitActionMgr();
SalesCLApprovalResubmitActionMgr.resubmit(_args);
}
private void resubmit(Args _args)
{
// Variable declaration.
recId
_recId = _args.record().RecId;
WorkflowCorrelationId
_workflowCorrelationId;
// Hardcoded type name
WorkflowTypeName
_workflowTypeName =
workflowtypestr(SalesCreditLimitAppr);
// Initial note is the information that users enter
when they
// submit the document for workflow.
WorkflowComment
_initialNote ="";
WorkflowWorkItemActionDialog
WorkflowWorkItemActionDialog;
SalesTable
SalesTable;
;
6-17
ttsbegin;
WorkflowWorkItemActionManager::dispatchWorkItemAction(
_args.caller().getActiveWorkflowWorkItem(),
_initialNote,
curUserId(),
WorkflowWorkItemActionType::Resubmit,
_args.menuItemName(),
false);
SalesTable.CreditLimitApprovalStatus =
SalesCreditLimitApprovalStatus::Submitted;
// Send an Infolog message.
info("Resubmitted to workflow.");
6-18
Chapter 6: Workflow
}
ttscommit;
catch(exception::Error)
{
info("Error on workflow activation.");
}
_args.caller().updateWorkFlowControls();
}
6-19
Summary
The workflow system is highly configurable and flexible. By using Morph X and
some standard code patterns, it can be enabled for virtually any part of the
Microsoft Dynamics AX application.
This lesson explores some of the possibilities the workflow system offers, and
explores some of the different ways it can be used to cover most workflow
requirements.
6-20
Chapter 6: Workflow
6-21
2.
3.
6-22
Chapter 6: Workflow
Solutions
Test Your Knowledge
1. What application element is used to define to which module a workflow is
applicable?
( ) Workflow type
() Workflow category
( ) A field in the workflow configuration
( ) SalesTable
2. What type of AOT element needs to be created to specify which tables will
be processed by a workflow?
( ) Extended data type
( ) Class
( ) Form
() Query
3. Which three properties on a form design need to be modified to allow the
form to use a workflow?
() WorkflowType
() WorkflowEnabled
( ) WorkflowDocument
() WorkflowDatasource
6-23
6-24