Chapter 4. The X++ Programming Language

In this chapter

Introduction
Jobs
The type system
Syntax
Classes and interfaces
Code access security
Compiling and running X++ as .NET CIL
Design and implementation patterns

Introduction

X++ is an object-oriented, application-aware, and data-aware programming language. The language is object oriented because it supports object abstractions, abstraction hierarchies, polymorphism, and encapsulation. It is application aware because it includes keywords such as client, server, changecompany, and display that are useful for writing client/server enterprise resource planning (ERP) applications. And it is data aware because it includes keywords such as firstFast, forceSelectOrder, and forUpdate, as well as a database query syntax, that are useful for programming database applications.

You use the Microsoft Dynamics AX designers and tools to edit the structure of application types. You specify the behavior of application types by writing X++ source code in the X++ editor. The X++ compiler compiles this source code into bytecode intermediate format. Model data, X++ source code, intermediate bytecode, and .NET common intermediate language (CIL) code are stored in the model store.

The Microsoft Dynamics AX runtime dynamically composes object types by loading overridden bytecode from the highest-level definition in the model layering stack. Objects are instantiated from these dynamic types. Similarly, the compiler produces .NET CIL from the X++ source code from the highest layer. For more information about the Microsoft Dynamics AX layering technology, see Chapter 21.

This chapter describes the Microsoft Dynamics AX runtime type system and the features of the X++ language that are essential to writing ERP applications. It will also help you avoid common programming pitfalls that stem from implementing X++. For an in-depth discussion of the type system and the X++ language, refer to the Microsoft Dynamics AX 2012 software development kit (SDK), available on MSDN.

Jobs

Jobs are globally defined functions that execute in the Windows client run-time environment. Developers frequently use jobs to test a piece of business logic because they are easily executed from within the MorphX development environment, by either pressing F5 or selecting Go on the command menu. However, you shouldn’t use jobs as part of your application’s core design. The examples provided in this chapter can be run as jobs.

Jobs are model elements that you create by using the Application Object Tree (AOT). The following X++ code provides an example of a job model element that prints the string “Hello World” to an automatically generated window. The pause statement stops program execution and waits for user input from a dialog box.

static void myJob(Args _args)
{
    print "Hello World";
    pause;
}

The type system

The Microsoft Dynamics AX runtime manages the storage of value type data on the call stack and reference type objects on the memory heap. The call stack is the memory structure that holds data about the active methods called during program execution. The memory heap is the memory area that allocates storage for objects that are destroyed automatically by the Microsoft Dynamics AX runtime.

Value types

Value types include the built-in primitive types, extended data types, enumeration types, and built-in collection types:

  • The primitive types are boolean, int, int64, real, date, utcDateTime, timeofday, str, and guid.

  • The extended data types are specialized primitive types and specialized base enumerations.

  • The enumeration types are base enumerations and extended data types.

  • The collection types are the built-in array and container types.

By default, variables declared as value types are assigned their zero value by the Microsoft Dynamics AX runtime. These variables can’t be set to null. Variable values are copied when variables are used to invoke methods and when they are used in assignment statements. Therefore, two value type variables can’t reference the same value.

Reference types

Reference types include the record types, class types, and interface types:

  • The record types are table, map, and view. User-defined record types are dynamically composed from application model layers. Microsoft Dynamics AX runtime record types are exposed in the system application programming interface (API).

    Note

    Although the methods are not visible in the AOT, all record types implement the methods that are members of the system xRecord type, a Microsoft Dynamics AX runtime class type.

  • User-defined class types are dynamically composed from application model layers and Microsoft Dynamics AX runtime class types exposed in the system API.

  • Interface types are type specifications and can’t be instantiated in the Microsoft Dynamics AX runtime. Class types can, however, implement interfaces.

Variables declared as reference types contain references to objects that the Microsoft Dynamics AX runtime instantiates from dynamically composed types defined in the application model layering system and from types exposed in the system API. The Microsoft Dynamics AX runtime also performs memory deallocation (garbage collection) for these objects when they are no longer referenced. Reference variables declared as record types reference objects that the Microsoft Dynamics AX runtime instantiates automatically. Class type objects are programmatically instantiated using the new operator. Copies of object references are passed as reference parameters in method calls and are assigned to reference variables, so two variables can reference the same object.

Note

More Info Not all nodes in the AOT name a type declaration. Some class declarations are merely syntactic sugar—convenient, human-readable expressions. For example, the class header definition for all rich client forms declares a FormRun class type. FormRun is also, however, a class type in the system API. Allowing this declaration is syntactic sugar because it is technically impossible for two types to have the same name in the Microsoft Dynamics AX class type hierarchy.

Type hierarchies

The X++ language supports the definition of type hierarchies that specify generalized and specialized relationships between class types and table types. For example, a check payment method is a type of payment method. A type hierarchy allows code reuse. Reusable code is defined on base types defined higher in a type hierarchy because they are inherited, or reused, by derived types defined lower in a type hierarchy.

Tip

You can use the Type Hierarchy Context and Type Hierarchy Browser tools in MorphX to visualize, browse, and search the hierarchy of any type.

The following sections introduce the base types provided by the Microsoft Dynamics AX runtime and describe how they are extended in type hierarchies.

Caution

The Microsoft Dynamics AX type system is known as a weak type system because X++ accepts certain type assignments that are clearly erroneous and lead to runtime errors. Be aware of the caveats outlined in the following sections, and try to avoid weak type constructs when writing X++ code.

The anytype type

The Microsoft Dynamics AX type system doesn’t have a single base type from which all types ultimately derive. However, the anytype type imitates a base type for all types. Variables of the anytype type function like value types when they are assigned a value type variable and like reference types when they are assigned a reference type variable. You can use the SysAnyType class to explicitly box all types, including value types, and make them function like reference types.

The anytype type, shown in the following code sample, is syntactic sugar that allows methods to accept any type as a parameter or allows a method to return different types:

static str queryRange(anytype _from, anytype _to)
{
    return SysQuery::range(_from,_to);
}

You can declare variables by using anytype. However, the underlying data type of an anytype variable is set to match the first assignment, and you can’t change its type afterward, as shown here:

anytype a = 1;
print strfmt("%1 = %2", typeof(a), a); //Integer = 1
a = "text";
print strfmt("%1 = %2", typeof(a), a); //Integer = 0

The common type

The common type is the base type of all record types. Like the anytype type, record types are context-dependent types whose variables can be used as though they reference single records or as a record cursor that can iterate over a set of database records.

By using the common type, you can cast one record type to another (possibly incompatible) record type, as shown in this example:

//customer = vendor; //Compile error
common = customer;
vendor = common;     //Accepted

Tables in Microsoft Dynamics AX also support inheritance and polymorphism. This capability offers a type-safe method of sharing commonalities such as methods and fields between tables. It is possible to override table methods but not table fields. A base table can be marked as abstract or final through the table’s properties.

Table maps defined in the AOT are a type-safe method of capturing commonalities between record types across type hierarchies, and you should use them to prevent incompatible record assignments. A table map defines fields and methods that safely operate on one or more record types.

The compiler doesn’t validate method calls on the common type. For example, the compiler accepts the following method invocation, even though the method doesn’t exist:

common.nonExistingMethod();

For this reason, you should use reflection to confirm that the method on the common type exists before you invoke it, as shown in this example. For more information, see Chapter 20.

if (tableHasMethod(new DictTable(common.tableId), identifierStr(existingMethod)))
{
    common.existingMethod();
}

The object type

The built-in object type is a weak reference type whose variables reference objects that are instances of class or interface types in the Microsoft Dynamics AX class hierarchy.

The type system allows you to implicitly cast base type objects to derived type objects and to cast derived type objects to base type objects, as shown here:

baseClass = derivedClass;
derivedClass = baseClass;

The object type allows you to use the assignment operator and cast one class type to another, incompatible class type, as shown in the following code. The probable result of this action, however, is a run-time exception when your code encounters an object of an unexpected type.

Object myObject;
//myBinaryIO = myTextIO; //Compile error
myObject = myTextIO;
mybinaryIO = myObject;   //Accepted

Use the is and as operators instead of the assignment operator to prevent these incompatible type casts. The is operator determines if an instance is of a particular type, and the as operator casts an instance as a particular type, or null if they are not compatible. The is and as operators work on class and table types.

myTextIO = myObject as TextIO;
if (myBinaryIO is TextIO)
{
}

You can use the object type for late binding to methods, similar to the dynamic keyword in C#. Keep in mind that a run-time error will occur if the method invoked doesn’t exist.

myObject.lateBoundMethod();

Extended data types

You use the AOT to create extended data types that model concrete data values and data hierarchies. For example, the Name extended data type is a string, and the CustName and VendName extended data types extend the Name data type.

The X++ language supports extended data types but doesn’t offer type checking according to the hierarchy of extended data types. X++ treats any extended data type as its primitive type; therefore, code such as the following is allowed:

CustName customerName;
FileName fileName = customerName;

When used properly, extended data types improve the readability of X++ code. It’s easier to understand the intended use of a CustName data type than a string data type, even if both are used to declare string variables.

Extended data types are more than just type definitions that make X++ code more readable. On each extended data type, you can also specify how the system displays values of this type to users. Further, you can specify a reference to a table. The reference enables the form’s rendering engine to automatically build lookup forms for form controls by using the extended data type, even when the form controls are not bound to a data source. On string-based extended data types, you can specify the maximum string size of the type. The database layer uses the string size to define the underlying columns for fields that use the extended data type. Defining the string size in only one place makes it easy to change.

Syntax

The X++ language belongs to the “curly brace” family of programming languages (those that use curly braces to delimit syntax blocks), such as C, C++, C#, and Java. If you’re familiar with any of these languages, you won’t have a problem reading and understanding X++ syntax.

Unlike many programming languages, X++ is not case sensitive. However, using camel casing (camelCasing) for variable names and Pascal casing (PascalCasing) for type names is considered a best practice. (More best practices for writing X++ code are available in the Microsoft Dynamics AX 2012 SDK.) You can use the Source Code Titlecase Update tool (accessed from the Add-Ins submenu in the AOT) to automatically apply casing in X++ code to match the best practice recommendation.

Common language runtime (CLR) types, which are case sensitive, are one important exception to the casing guidelines. For information about how to use CLR types, see the CLR interoperability section later in this chapter.

Variable declarations

You must place variable declarations at the beginning of methods. Table 4-1 provides examples of value type and reference type variable declarations, in addition to example variable initializations. Parameter declaration examples are provided in the Classes and interfaces section later in this chapter.

Table 4-1. X++ variable declaration examples.

Type

Examples

anytype

anytype type = null;
anytype type = 1;

Base enumeration types

NoYes theAnswer = NoYes::Yes;

boolean

boolean b = true;

container

container c1 = ["a string", 123];
container c2 = [["a string", 123], c1];
container c3 = connull();

date

date d = 31122008;

Extended data types

Name name = "name";

guid

guid g = newguid();

int

int i = -5;
int h = 0xAB;

int64

int64 i = -5;
int64 h = 0xAB;
int64 u = 0xA0000000u;

Object types

Object obj = null;
MyClass myClass = new MyClass();
System.Text.StringBuilder sb = new
System.Text.StringBuilder();

real

real r1 = 3.14;
real r2 = 1.0e3;

Record types

Common myRecord = null;
CustTable custTable = null;

str

str s1 = "a string";
str s2 = 'a string';
str 40 s40 = "string 40";

TimeOfDay

TimeOfDay time = 43200;

utcDateTime

utcDateTime dt = 2008-12-31T23:59:59;

Note

String literals can be expressed using either single or double quotes. It is considered best practice to use single quotes for system strings, like file names, and double quotes for user interface strings. The examples in this chapter adhere to this guideline.

Declaring variables with the same name as their type is a best practice. At first glance, this approach might seem confusing. Consider this class and its getter/setter method to its field:

Class Person
{
    Name name;

    public Name Name(Name _name = name)
    {
        name = _name;
        return name;
    }
}

Because X++ is not case sensitive, the word name is used in eight places in the preceding code. Three refer to the extended data type, four refer to the field, and one refers to the method (_name is used twice). To improve readability, you could rename the variable to something more specific, such as personName. However, using a more specific variable name implies that a more specific type should be used (and created if it doesn’t already exist). Changing both the type name and the variable name to PersonName wouldn’t improve readability. The benefit of this practice is that if you know the name of a variable, you also know its type.

Note

Previous versions of Microsoft Dynamics AX required a dangling semicolon to signify the end of a variable declaration. This is no longer required because the compiler solves the ambiguity by reading one token ahead, except where the first statement is a static CLR call. The compiler still accepts the now-superfluous semicolons, but you can remove them if you want to.

Expressions

X++ expressions are sequences of operators, operands, values, and variables that yield a result. Table 4-2 summarizes the types of expressions allowed in X++ and includes examples of their use.

Table 4-2. X++ expression examples.

Category

Examples

Access operators

this                        //Instance member access
element                     //Form member access
<datasource>_ds             //Form data source access
<datasource>_q              //Form query access
x.y                         //Instance member access
E::e                        //Enum access
a[x]                        //Array access
[v1, v2] = c                //Container access
Table.Field                 //Table field access
Table.(FieldId)             //Table field access
(select statement).Field    //Select result access
System.Type                 //CLR namespace type access
System.DayOfWeek::Monday    //CLR enum access

Arithmetic operators

x = y + z                    // Addition
x = y - z                    // Subtraction
x = y * z                    // Multiplication
x = y / z                    // Division
x = y div z                  // Integer division
x = y mod z                  // Integer division remainder

Bitwise operators

x = y & z                    // Bitwise AND
x = y | z                    // Bitwise OR
x = y ^ z                    // Bitwise exclusive OR (XOR)
x = ~z                       // Bitwise complement

Conditional operators

x ? y : z

Logical operators

if (!obj)                    // Logical NOT
if (a && b)                  // Logical AND
if (a || b)                  // Logical OR

Method invocations

super()             //Base member invocation
MyClass::m()        //Static member invocation
myObject.m()        //Instance member invocation
this.m()            //This instance member invocation
myTable.MyMap::m(); //Map instance member invocation
f()                 //Built-in function call

Object creation operators

new MyClass()           //X++ object creation
new System.DateTime()   //CLR object wrapper and
                        //CLR object creation
new System.Int32[100]() //CLR array creation

Parentheses

(x)

Relational operators

x < y                        // Less than
x > y                        // Greater than
x <= y                       // Less than or equal
x >= y                       // Greater than or equal
x == y                       // Equal
x != y                       // Not equal
select t where t.f like "a*" // Select using wildcards

Shift operators

x = y << z                   // Shift left
x = y >> z                   // Shift right

String concatenation

"Hello" + "World"

Values and variables

"string"
myVariable

Statements

X++ statements specify object state and object behavior. Table 4-3 provides examples of X++ language statements that are commonly found in many programming languages. In-depth descriptions of each statement are beyond the scope of this book.

Table 4-3. X++ statement examples.

Statement

Example

.NET CLR interoperability statement

System.Text.StringBuilder sb;
sb = new System.Text.StringBuilder();
sb.Append("Hello World");
print sb.ToString();
pause;

Assignment statement

int i = 42;
i = 1;
i++;
++i;
i--;
--i;
i += 1;
i -= 1;
this.myDelegate += eventhandler(obj.handler);
this.myDelegate -= eventhandler(obj.handler);

break statement

int i;
for ( i = 0; i < 100; i++ )
{
    if ( i > 50 )
    {
        break;
    }
}

breakpoint statement

breakpoint; //Causes the debugger to be invoked

Casting statement

MyObject myObject = object as MyObject;
boolean isCompatible = object is MyObject;

changeCompany statement

MyTable myTable;
while select myTable
{
    print myTable.myField;
}
changeCompany("ZZZ")
{
   while select myTable
   {
       print myTable.myField;
   }
}
pause;

Compound statement

int i;
{
    i = 3;
    i++;
}

continue statement

int i;
int j = 0;
for( i = 0; i < 100; i++ )
{
    if ( i < 50 )
    {
        continue;
    }
    j++;
}

do while statement

int i = 4;
do
{
    i++;
}
while ( i <= 100 );

flush statement

MyTable myTable;
flush myTable;

for statement

int i;
for ( i = 0; i < 42; i++ )
{
    print i;
}
pause;

if statement

boolean b = true;
int i = 42;
if ( b == true )
{
    i++;
}
else
{
    i--;
}

Local function

static void myJob(Args _args)
{
     str myLocalFunction()
     {
        return "Hello World";
     }
     print myLocalFunction();
     pause;
}

pause statement

print "Hello World";
pause;

print statement

int i = 42;
print i;
print "Hello World";
print "Hello World" at 10,5;
print 5.2;
pause;

retry statement

try
{
    throw error("Force exception");
}
catch( exception::Error )
{
    retry;
}

return statement

int foo()
{
    return 42;
}

switch statement

str s = "test";
switch ( s )
{
    case "test" :
        print s;
        break;
    default :
        print "fail";
}
pause;

System function

guid g = newGuid();
print abs(-1);

throw statement

throw error("Error text");

try statement

try
{
    throw error("Force exception");
}
catch( exception::Error )
{
    print "Error";
    pause;
}
catch
{
    print "Another exception";
    pause;
}

unchecked statement

unchecked(Uncheck::TableSecurityPermission)
{
    this.method();
}

while statement

int i = 4;
while ( i <= 100 )
{
    i++;
}

window statement

window 100, 10 at 100, 10;
print "Hello World";
pause;

Data-aware statements

The X++ language has built-in support for querying and manipulating database data. The syntax for database statements is similar to Structured Query Language (SQL), and this section assumes that you’re familiar with SQL. The following code shows how a select statement is used to return only the first selected record from the MyTable database table and how the data in the record’s myField field is printed:

static void myJob(Args _args)
{
     MyTable myTable;
     select firstOnly * from myTable where myTable.myField1 == "value";
     print myTable.myField2;
     pause;
}

The “* from” part of the select statement in the example is optional. You can replace the asterisk (*) character with a comma-separated field list, such as myField2, myField3. You must define all fields, however, on the selection table model element, and only one selection table is allowed immediately after the from keyword. The where expression in the select statement can include any number of logical and relational operators. The firstOnly keyword is optional and can be replaced by one or more of the optional keywords. Table 4-4 describes all possible keywords. For more information about database-related keywords, see Chapter 17.

Table 4-4. Keyword options for select statements.

Keyword

Description

crossCompany

Forces the Microsoft Dynamics AX runtime to generate a query without automatically adding the where clause in the dataAreaId field. This keyword can be used to select records from all or from a set of specified company accounts. For example, the query

while select crosscompany:companies myTable { }

selects all records in the myTable table from the company accounts specified in the companies container.

firstFast

Fetches the first selected record faster than the remaining selected records.

firstOnly

firstOnly1

Returns only the first selected record.

firstOnly10

Returns only the first 10 selected records.

firstOnly100

Returns only the first 100 selected records.

firstOnly1000

Returns only the first 1,000 selected records.

forceLiterals

Forces the Microsoft Dynamics AX runtime to generate a query with the specified field constraints. For example, the query generated for the preceding code example looks like this: select * from myTable where myField1=’value’. Database query plans aren’t reused when this option is specified. This keyword can’t be used with the forcePlaceholders keyword.

forceNestedLoop

Forces the SQL Server query processor to use a nested-loop algorithm for table join operations. Other join algorithms, such as hash-join and merge-join, are therefore not considered by the query processor.

forcePlaceholders

Forces the Microsoft Dynamics AX runtime to generate a query with placeholder field constraints. For example, the query generated for the preceding code example looks like this: select * from myTable where myField1=?. Database query plans are reused when this option is specified. This is the default option for select statements that don’t join table records. This keyword can’t be used with the forceLiterals keyword.

forceSelectOrder

Forces the Microsoft SQL Server query processor to access tables in the order in which they are specified in the query.

forUpdate

Selects records for updating.

generateOnly

Instructs the SQL Server query processor to only generate the SQL statements—and not execute them. The generated SQL statement can be retrieved using the getSQLStatement method on the primary table.

noFetch

Specifies that the Microsoft Dynamics AX runtime should not execute the statement immediately because the records are required only by some other operation.

optimisticLock

Overrides the table’s OccEnabled property and forces the optimistic locking scheme. This keyword can’t be used with the pessimisticLock and repeatableRead keywords.

pessimisticLock

Overrides the table’s OccEnabled property and forces the pessimistic locking scheme. This keyword can’t be used with the optimisticLock and repeatableRead keywords.

repeatableRead

Locks all records read within a transaction. This keyword can be used to ensure consistent data is fetched by identical queries for the duration of the transaction, at the cost of blocking other updates of those records. Phantom reads can still occur if another process inserts records that match the range of the query. This keyword can’t be used with the optimisticLock and pessimisticLock keywords.

reverse

Returns records in the reverse of the select order.

validTimeState

Instructs the SQL Server query processor to use the provided date or date range instead of the current date. For example, the query

while select validTimeState(fromDate, toDate) myTable { }

selects all records in the myTable table that are valid in the period from fromDate to toDate.

The following code example demonstrates how to use a table index clause to suggest the index that a database server should use when querying tables. The Microsoft Dynamics AX runtime appends an order by clause and the index fields to the first select statement’s database query. Records are thus ordered by the index. The Microsoft Dynamics AX runtime can insert a query hint into the second select statement’s database query, if the hint is feasible to use.

static void myJob(Args _args)
{
    MyTable1 myTable1;
    MyTable2 myTable2;

    while select myTable1
        index myIndex1
    {
        print myTable1.myField2;
    }

    while select myTable2
        index hint myIndex2
    {
        print myTable2.myField2;
    }
    pause;
}

The following code example demonstrates how the results from a select query can be ordered and grouped. The first select statement specifies that the resulting records must be sorted in ascending order based on myField1 values and then in descending order based on myField2 values. The second select statement specifies that the resulting records must be grouped by myField1 values and then sorted in descending order.

static void myJob(Args _args)
{
    MyTable myTable;

    while select myTable
        order by Field1 asc, Field2 desc
    {
        print myTable.myField;
    }
    while select myTable
        group by Field1 desc
    {
        print myTable.Field1;
    }
    pause;
}

The following code demonstrates use of the avg and count aggregate functions in select statements. The first select statement averages the values in the myField column and assigns the result to the myField field. The second select statement counts the number of records the selection returns and assigns the result to the myField field.

static void myJob(Args _args)
{
     MyTable myTable;

     select avg(myField) from myTable;
     print myTable.myField;

     select count(myField) from myTable;
     print myTable.myField;
     pause;
}

Caution

The compiler doesn’t verify that aggregate function parameter types are numeric, so the result that the function returns could be assigned to a field of type string. The result will be truncated if, for example, the average function calculates a value of 1.5 and the type of myField is an integer.

Table 4-5 Describes the aggregate functions supported in X++ select statements.

Table 4-5. Aggregate functions in X++ select statements.

Function

Description

avg

Returns the average of the non-null field values in the records the selection returns.

count

Returns the number of non-null field values in the records the selection returns.

maxOf

Returns the maximum of the non-null field values in the records the selection returns.

minOf

Returns the minimum of the non-null field values in the records the selection returns.

sum

Returns the sum of the non-null field values in the records the selection returns.

The following code example demonstrates how tables are joined with join conditions. The first select statement joins two tables by using an equality join condition between fields in the tables. The second select statement joins three tables to illustrate how you can nest join conditions and use an exists operator as an existence test with a join condition. The second select statement also demonstrates how you can use a group by sort in join conditions. In fact, the join condition can comprise multiple nested join conditions because the syntax of the join condition is the same as the body of a select statement.

static void myJob(Args _args)
{
     MyTable1 myTable1;
     MyTable2 myTable2;


     MyTable3 myTable3;

     select myField from myTable1
        join myTable2
           where myTable1.myField1=myTable2.myField1;
     print myTable1.myField;

     select myField from myTable1
        join myTable2
          group by myTable2.myField1
          where myTable1.myField1=myTable2.myField1
             exists join myTable3
                where myTable1.myField1=myTable3.mField2;
     print myTable1.myField;
     pause;
}

Table 4-6 describes the exists operator and the other join operators that can be used in place of the exists operator in the preceding example.

Table 4-6. Join operators.

Operator

Description

exists

Returns true if any records are in the result set after executing the join clause. Returns false otherwise.

notExists

Returns false if any records are in the result set after executing the join clause. Returns true otherwise.

outer

Returns the left outer join of the first and second tables.

The following example demonstrates use of the while select statement that increments the myTable variable’s record cursor on each loop:

static void myJob(Args _args)
{
     MyTable myTable;

     while select myTable
     {
         Print myTable.myField;
     }
}

You must use the ttsBegin, ttsCommit, and ttsAbort transaction statements to modify records in tables and to insert records into tables. The ttsBegin statement marks the beginning of a database transaction block; ttsBegin-ttsCommit transaction blocks can be nested. The ttsBegin statements increment the transaction level; the ttsCommit statements decrement the transaction level. The outermost block decrements the transaction level to zero and commits all database inserts and updates performed since the first ttsBegin statement to the database. The ttsAbort statement rolls back all database inserts, updates, and deletions performed since the ttsBegin statement. Table 4-7 provides examples of these transaction statements for single records and operations and for set-based (multiple-record) operations.

The last example in Table 4-7 demonstrates the method RowCount. Its purpose is to get the count of records that are affected by set-based operations; namely, insert_recordset, update_recordset, and delete_from.

By using RowCount, it is possible to save one round-trip to the database in certain application scenarios; for example, when implementing insert-or-update logic.

Table 4-7. Transaction statement examples.

Statement Type

Example

delete_from

MyTable myTable;
Int64 numberOfRecordsAffected;

ttsBegin;
delete_from myTable
    where myTable.id == "001";
numberOfRecordsAffected = myTable.RowCount();
ttsCommit;

insert method

MyTable myTable;

ttsBegin;
myTable.id = "new id";
myTable.myField = "new value";
myTable.insert();
ttsCommit;

insert_recordset

MyTable1 myTable1;
MyTable2 myTable2;
int64 numberOfRecordsAffected;

ttsBegin;
insert_recordset myTable2 ( myField1, myField2 )
    select myField1, myField2 from myTable1;
numberOfRecordsAffected = myTable.RowCount();
ttsCommit;

select forUpdate

MyTable myTable;

ttsBegin;
select forUpdate myTable;
myTable.myField = "new value";
myTable.update();
ttsCommit;

ttsBegin

ttsCommit

ttsAbort

boolean b = true;

ttsBegin;
if ( b == true )
    ttsCommit;
else
    ttsAbort;

update_recordset

MyTable myTable;
int64 numberOfRecordsAffected;

ttsBegin;
update_recordset myTable setting
    myField1 = "value1",
    myField2 = "value2"
    where myTable.id == "001";
numberOfRecordsAffected = myTable.RowCount();
ttsCommit;

Exception handling

It is a best practice to use the X++ exception handling framework instead of programmatically halting a transaction by using the ttsAbort statement. An exception (other than the update conflict and duplicate key exceptions) thrown inside a transaction block halts execution of the block, and all of the inserts and updates performed since the first ttsBegin statement are rolled back. Throwing an exception has the additional advantage of providing a way to recover object state and maintain the consistency of database transactions. Inside the catch block, you can use the retry statement to run the try block again. The following example demonstrates throwing an exception inside a database transaction block:

static void myJob(Args _args)
{
    MyTable myTable;
    boolean state = false;

    try
    {
        ttsBegin;

        update_recordset myTable setting
            myField = "value"
            where myTable.id == "001";
        if(state==false)
        {
            throw error("Error text");
        }
        ttsCommit;
    }
    catch(Exception::Error)
    {
        state = true;
        retry;
    }
}

The throw statement throws an exception that causes the database transaction to halt and roll back. Code execution can’t continue inside the scope of the transaction, so the runtime ignores try and catch statements when inside a transaction. This means that an exception thrown inside a transaction can be caught only outside the transaction, as shown here:

static void myJob(Args _args)
{
    try
    {
        ttsBegin;
        try
        {
            ...
            throw error("Error text");
        }
        catch  //Will never catch anything
        {
        }
        ttsCommit;
    }
    catch(Exception::Error)
    {
        print "Got it";
        pause;
    }
    catch
    {
        print "Unhandled Exception";
        pause;
    }
}

Although a throw statement takes the exception enumeration as a parameter, using the error method to throw errors is considered best practice. The try statement’s catch list can contain more than one catch block. The first catch block in the example catches error exceptions. The retry statement jumps to the first statement in the outer try block. The second catch block catches all exceptions not caught by catch blocks earlier in the try statement’s catch list. Table 4-8 describes the Microsoft Dynamics AX system Exception data type enumerations that can be used in try-catch statements.

Table 4-8. Exception data type enumerations.

Element

Description

Break

Thrown when a user presses the Break key or Ctrl+C.

CLRError

Thrown when an unrecoverable error occurs in a CLR process.

CodeAccessSecurity

Thrown when an unrecoverable error occurs in the demand method of a CodeAccessPermission object.

DDEerror

Thrown when an error occurs in the use of a Dynamic Data Exchange (DDE) system class.

Deadlock

Thrown when a database transaction has deadlocked.

DuplicateKeyException

Thrown when a duplicate key error occurs during an insert operation. The catch block should change the value of the primary keys and use a retry statement to attempt to commit the halted transaction.

DuplicateKeyExceptionNotRecovered

Thrown when an unrecoverable duplicate key error occurs during an insert operation. The catch block shouldn’t use a retry statement to attempt to commit the halted transaction.

Error[a]

Thrown when an unrecoverable application error occurs. A catch block should assume that all database transactions in a transaction block have been halted and rolled back.

Internal

Thrown when an unrecoverable internal error occurs.

Numeric

Thrown when a mathematical error occurs like division by zero, logarithm of a negative number, or conversion between incompatible types.

PassClrObjectAcrossTiers

Thrown when an attempt is made to pass a CLR object from the client to the server tier or vice versa. The Microsoft Dynamics AX runtime doesn’t support automatic marshaling of CLR objects across tiers.

Sequence

Thrown by the Microsoft Dynamics AX kernel if a database error or database operation error occurs.

Timeout

Thrown when a database operation times out.

UpdateConflict

Thrown when an update conflict error occurs in a transaction block using optimistic concurrency control. The catch block should use a retry statement to attempt to commit the halted transaction.

UpdateConflictNotRecovered

Thrown when an unrecoverable error occurs in a transaction block using optimistic concurrency control. The catch block shouldn’t use a retry statement to attempt to commit the halted transaction.

[a] The error method is a static method of the global X++ class for which the X++ compiler allows an abbreviated syntax. The expression Global::error(“Error text”) is equivalent to the error expression in the code examples earlier in this section. Don’t confuse these global X++ methods with Microsoft Dynamics AX system API methods, such as newGuid.

UpdateConflict and DuplicateKeyException are the only data exceptions that a Microsoft Dynamics AX application can handle inside a transaction. Specifically, with DuplicateKeyException, the database transaction isn’t rolled back, and the application is given a chance to recover. DuplicateKeyException facilitates application scenarios (such as Master Planning) that perform batch processing and handles duplicate key exceptions without aborting the transaction in the midst of the resource-intensive processing operation.

The following example illustrates the usage of DuplicateKeyException:

static void DuplicateKeyExceptionExample(Args _args)
{
    MyTable myTable;

    ttsBegin;
    myTable.Name = "Microsoft Dynamics AX";
    myTable.insert();
    ttsCommit;

    ttsBegin;
    try
    {
        myTable.Name = "Microsoft Dynamics AX";
        myTable.insert();
    }
    catch(Exception::DuplicateKeyException)
    {
        info(strfmt("Transaction level: %1", appl.ttsLevel()));
        info(strfmt("%1 already exists.", myTable.Name));
        info(strfmt("Continuing insertion of other records"));
    }
    ttsCommit;
}

In the preceding example, the catch block handles the duplicate key exception. Notice that the transaction level is still 1, indicating that the transaction hasn’t aborted and the application can continue processing other records.

Note

The special syntax where a table instance was included in the catch block is no longer available.

Interoperability

The X++ language includes statements that allow interoperability (interop) with .NET CLR assemblies and COM components. The Microsoft Dynamics AX runtime achieves this interoperability by providing Dynamics AX object wrappers around external objects and by dispatching method calls from the Microsoft Dynamics AX object to the wrapped object.

CLR interoperability

You can write X++ statements for CLR interoperability by using one of two methods: strong typing or weak typing. Strong typing is recommended because it is type safe and less error prone than weak typing, and it results in code that is easier to read. The MorphX X++ editor also provides IntelliSense as you type.

The examples in this section use the .NET System.Xml assembly, which is added as an AOT references node. (See Chapter 1, for a description of programming model elements.) The programs are somewhat verbose because the compiler doesn’t support method invocations on CLR return types and because CLR types must be identified by their fully qualified name. For example, the expression System.Xml.XmlDocument is the fully qualified type name for the .NET Framework XML document type.

Caution

X++ is case sensitive when referring to CLR types.

The following example demonstrates strongly typed CLR interoperability with implicit type conversions from Microsoft Dynamics AX strings to CLR strings in the string assignment statements and shows how CLR exceptions are caught in X++:

static void myJob(Args _args)
{
   System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
   System.Xml.XmlElement rootElement;
   System.Xml.XmlElement headElement;
   System.Xml.XmlElement docElement;
   System.String xml;
   System.String docStr = 'Document';
   System.String headStr = 'Head';
   System.Exception ex;
   str errorMessage;

   try
   {
       rootElement = doc.CreateElement(docStr);
       doc.AppendChild(rootElement);
       headElement = doc.CreateElement(headStr);
       docElement = doc.get_DocumentElement();
       docElement.AppendChild(headElement);
       xml = doc.get_OuterXml();
       print ClrInterop::getAnyTypeForObject(xml);
       pause;
   }
   catch(Exception::CLRError)
   {
       ex = ClrInterop::getLastException();
       if( ex )
       {
           errorMessage = ex.get_Message();
           info( errorMessage );
       }
   }
}

The following example illustrates how static CLR methods are invoked by using the X++ static method accessor ::.

static void myJob(Args _args)
{
    System.Guid g = System.Guid::NewGuid();
}

The following example illustrates the support for CLR arrays:

static void myJob(Args _args)
{
    System.Int32 [] myArray = new System.Int32[100]();

    myArray.SetValue(1000, 0);
    print myArray.GetValue(0);
}

X++ supports passing parameters by reference to CLR methods. Changes that the called method makes to the parameter also change the caller variable’s value. When non-object type variables are passed by reference, they are wrapped temporarily in an object. This operation is often called boxing and is illustrated in the following example:

static void myJob(Args _args)
{
    int myVar = 5;

    MyNamespace.MyMath::Increment(byref myVar);

    print myVar;   // prints 6
}

The called method could be implemented in C# like this:

// Notice: This example is C# code
static public void Increment(ref int value)
{
    value++;
}

Note

Passing parameters by reference is supported only for CLR methods, not for X++ methods.

The second method of writing X++ statements for CLR uses weak typing. The following example shows CLR types that perform the same steps as in the first CLR interoperability example. In this case, however, all references are validated at run time, and all type conversions are explicit.

static void myJob(Args _args)
{
   ClrObject doc = new ClrObject('System.Xml.XmlDocument'),
   ClrObject docStr;

   ClrObject rootElement;
   ClrObject headElement;
   ClrObject docElement;
   ClrObject xml;

   docStr = ClrInterop::getObjectForAnyType('Document'),
   rootElement = doc.CreateElement(docStr);
   doc.AppendChild(rootElement);
   headElement = doc.CreateElement('Head'),
   docElement = doc.get_DocumentElement();
   docElement.AppendChild(headElement);
   xml = doc.get_OuterXml();
   print ClrInterop::getAnyTypeForObject(xml);
   pause;
}

The first statement in the preceding example demonstrates the use of a static method to convert X++ primitive types to CLR objects. The print statement shows the reverse, converting CLR value types to X++ primitive types. Table 4-9 lists the value type conversions that Microsoft Dynamics AX supports.

Table 4-9. Type conversions supported in Microsoft Dynamics AX.

CLR Type

Microsoft Dynamics AX Type

Byte, SByte, Int16, UInt16, Int32

int

Byte, SByte, Int16, UInt16, Int32, Uint32, Int64

int64

DateTime

utcDateTime

Double, Single

real

Guid

guid

String

str

Microsoft Dynamics AX Type

CLR Type

int

Int32, Int64

int64

Int64

utcDateTime

DateTime

real

Single, Double

guid

Guid

str

String

The preceding code example also demonstrates the X++ method syntax used to access CLR object properties, such as get_DocumentElement. The CLR supports several operators that are not supported in X++. Table 4-10 lists the supported CLR operators and the alternative method syntax.

Table 4-10. CLR operators and methods.

CLR Operators

CLR Methods

Property operators

get_<property>, set_<property>

Index operators

get_Item, set_Item

Math operators

op_<operation>(arguments)

The following features of CLR can’t be used with X++:

  • Public fields (can be accessed by using CLR reflection classes)

  • Events and delegates

  • Generics

  • Inner types

  • Namespace declarations

COM interoperability

The following code example demonstrates COM interoperability with the XML document type in the Microsoft XML Core Services (MSXML) 6.0 COM component. The example assumes that you’ve installed MSXML. The MSXML document is first instantiated and wrapped in a Microsoft Dynamics AX COM object wrapper. A COM variant wrapper is created for a COM string. The direction of the variant is put into the COM component. The root element and head element variables are declared as COM objects. The example shows how to fill a string variant with an X++ string and then use the variant as an argument to a COM method, loadXml. The statement that creates the head element demonstrates how the Microsoft Dynamics AX runtime automatically converts Microsoft Dynamics AX primitive objects into COM variants.

static void Job2(Args _args)
{
    COM doc = new COM('Msxml2.DomDocument.6.0'),
    COMVariant rootXml =
               new COMVariant(COMVariantInOut::In,COMVariantType::VT_BSTR);
    COM rootElement;
    COM headElement;

    rootXml.bStr('<Root></Root>'),
    doc.loadXml(rootXml);
    rootElement = doc.documentElement();
    headElement = doc.createElement('Head'),
    rootElement.appendChild(headElement);
    print doc.xml();
    pause;
}

Macros

With the macro capabilities in X++, you can define and use constants and perform conditional compilation. Macros are unstructured because they are not defined in the X++ syntax. Macros are handled before the source code is compiled. You can add macros anywhere you write source code: in methods and in class declarations.

Table 4-11 shows the supported macro directives.

Table 4-11. Macro directives.

Directive

Description

#define

#globaldefine

Defines a macro with a value.

#define.MyMacro(SomeValue)

Defines the macro MyMacro with the value SomeValue.

#macro

#endmacro

#localmacro

#endmacro

Defines a macro with a value spanning multiple lines.

#macro.MyMacro
   print "foo";
   print "bar";
#endmacro

Defines the macro MyMacro with a multiple-line value.

#macrolib

Includes a macro library. As a shorthand form of this directive, you can omit macrolib.

#macrolib.MyMacroLibrary
#MyMacroLibrary

Both include the macro library MyMacroLibrary, which is defined under the Macros node in the AOT.

#MyMacro

Replaces a macro with its value.

#define.MyMacro("Hello World")
print #MyMacro;

Defines the macro MyMacro and prints its value. In this example, “Hello World” would be printed.

#definc

#defdec

Increments and decrements the value of a macro; typically used when the value is an integer.

#defdec.MyIntMacro

Decrements the value of the macro MyIntMacro.

#undef

Removes the definition of a macro.

#undef.MyMacro

Removes the definition of the macro MyMacro.

#if

#endif

Conditional compile. If the macro referenced by the #if directive is defined or has a specific value, the following text is included in the compilation:

#if.MyMacro
print "MyMacro is defined";
#endif
 

If MyMacro is defined, the print statement is included as part of the source code:

#if.MyMacro(SomeValue)
print "MyMacro is defined and has value: SomeValue";
#endif

If MyMacro has SomeValue, the print statement is included as part of the source code.

#ifnot

#endif

Conditional compile. If the macro referenced by the #ifnot directive isn’t defined or doesn’t have a specific value, the following text is included in the compilation:

#ifnot.MyMacro
print "MyMacro is not defined";
#endif

If MyMacro is not defined, the print statement is included as part of the source code:

#ifnot.MyMacro(SomeValue)
print "MyMacro does not have value: SomeValue; or it is
not defined";
#endif

If MyMacro is not defined, or if it does not have SomeValue, the print statement is included as part of the source code.

The following example shows a macro definition and reference:

void myMethod()
{
    #define.HelloWorld("Hello World")

    print #HelloWorld;
    pause;
}

As noted in Table 4-11, a macro library is created under the Macros node in the AOT. The library is included in a class declaration header or class method, as shown in the following example:

class myClass
{
    #MyMacroLibrary1
}
public void myMethod()
{
    #MyMacroLibrary2


    #MacroFromMyMacroLibrary1
    #MacroFromMyMacroLibrary2
}

A macro can also use parameters. The compiler inserts the parameters at the positions of the placeholders. The following example shows a local macro using parameters:

void myMethod()
{
    #localmacro.add
        %1 + %2
    #endmacro

    print #add(1, 2);
    print #add("Hello", "World");
    pause;
}

When a macro library is included or a macro is defined in the class declaration of a class, the macro can be used in the class and in all classes derived from the class. A subclass can redefine the macro.

Comments

X++ allows single-line and multiple-line comments. Single-line comments start with // and end at the end of the line. Multiple-line comments start with /* and end with */. You can’t nest multiple-line comments.

You can add reminders to yourself in comments that the compiler picks up and presents to you as tasks in its output window. To set up these tasks, start a comment with the word TODO. Be aware that tasks not occurring at the start of the comment, (for example, tasks that are deep inside multiple-line comments,) are ignored by the compiler.

The following code example contains comments reminding the developer to add a new procedure while removing an existing procedure by changing it into a comment:

public void myMethod()
{
    //Declare variables
    int value;

//TODO Validate if calculation is really required
/*
    //Perform calculation
    value = this.calc();
*/
    ...
}

XML documentation

You can document XML methods and classes directly in X++ by typing three slash characters (///) followed by structured documentation in XML format. The XML documentation must be above the actual code.

The XML documentation must align with the code. The Best Practices tool contains a set of rules that can validate the XML documentation. Table 4-12 lists the supported tags.

Table 4-12. XML tags supported for XML documentation.

Tag

Description

<summary>

Describes a method or a class

<param>

Describes the parameters of a method

<returns>

Describes the return value of a method

<remarks>

Adds information that supplements the information provided in the <summary> tag

<exception>

Documents exceptions that are thrown by a method

<permission>

Describes the permission needed to access methods using CodeAccessSecurity.demand

<seealso>

Lists references to related and relevant documentation

The XML documentation is automatically displayed in the IntelliSense in the X++ editor.

You can extract the written XML documentation for an AOT project by using the Add-Ins menu option Extract XML Documentation. One XML file is produced that contains all of the documentation for the elements inside the project. You can also use this XML file to publish the documentation.

The following code example shows XML documentation for a static method on the Global class:

/// <summary>
/// Converts an X++ utcDateTime value to a .NET System.DateTime object.
/// </summary>
/// <param name="_utcDateTime">
/// The X++ utcDateTime to convert.
/// </param>
/// <returns>
/// A .NET System.DateTime object.
/// </returns>
static client server anytype utcDateTime2SystemDateTime(utcDateTime _utcDateTime)
{
    return CLRInterop::getObjectForAnyType(_utcDateTime);
}

Classes and interfaces

You define types and their structure in the AOT, not in the X++ language. Other programming languages that support type declarations do so within code, but Microsoft Dynamics AX supports an object layering feature that accepts X++ source code customizations to type declaration parts that encompass variable declarations and method declarations. Each part of a type declaration is managed as a separate compilation unit, and model data is used to manage, persist, and reconstitute dynamic types whose parts can include compilation units from many object layers.

You use X++ to define logic, including method profiles (return value, method name, and parameter type and name). You use the X++ editor to add new methods to the AOT, so you can construct types without leaving the X++ editor.

You use X++ class declarations to declare protected instance variable fields that are members of application logic and framework reference types. You can’t declare private or public variable fields. You can declare classes as abstract if they are incomplete type specifications that can’t be instantiated. You can also declare them final if they are complete specifications that can’t be further specialized. The following code provides an example of an abstract class declaration header:

abstract class MyClass
{
}

You can also structure classes into single-inheritance generalization or specialization hierarchies in which derived classes inherit and override members of base classes. The following code shows an example of a derived class declaration header that specifies that MyDerivedClass extends the abstract base class MyClass. It also specifies that MyDerivedClass is final and can’t be further specialized by another class. Because X++ doesn’t support multiple inheritance, derived classes can extend only one base class.

final class MyDerivedClass extends MyClass
{
}

X++ also supports interface type specifications that specify method signatures but don’t define their implementation. Classes can implement more than one interface, but the class and its derived classes should together provide definitions for the methods declared in all the interfaces. If it fails to provide the method definitions, the class itself is treated as abstract and cannot be instantiated. The following code provides an example of an interface declaration header and a class declaration header that implements the interface:

interface MyInterface
{
    void myMethod()
    {
    }
}
class MyClass implements MyInterface
{
    void myMethod()
    {
    }
}

Fields

A field is a class member that represents a variable and its type. Fields are declared in class declaration headers; each class and interface has a definition part with the name classDeclaration in the AOT. Fields are accessible only to code statements that are part of the class declaration or derived class declarations. Assignment statements are not allowed in class declaration headers. The following example demonstrates how variables are initialized with assignment statements in a new method:

class MyClass
{
    str s;
    int i;
    MyClass1 myClass1;

    public void new()
    {
        i = 0;
        myClass1 = new MyClass1();
    }
}

Methods

A method on a class is a member that uses statements to define the behavior of an object. An interface method is a member that declares an expected behavior of an object. The following code provides an example of a method declaration on an interface and an implementation of the method on a class that implements the interface:

interface MyInterface
{
    public str myMethod()
    {
    }
}
class MyClass implements MyInterface
{
    public str myMethod();
    {
        return "Hello World";
    }
}

Methods are defined with public, private, or protected access modifiers. If an access modifier is omitted, the method is publicly accessible. The X++ template for new methods provides the private access specifier. Table 4-13 describes additional method modifiers supported by X++.

Table 4-13. Method modifiers supported by X++.

Modifier

Description

abstract

Abstract methods have no implementation. Derived classes must provide definitions for abstract methods.

client

Client methods can execute only on a MorphX client. The client modifier is allowed only on static methods.

delegate

Delegate methods cannot contain implementation. Event handlers can subscribe to delegate methods. The delegate modifier is allowed only on instance methods.

display

Display methods are invoked each time a form is redrawn. The display modifier is allowed only on table, form, form data source, and form control methods.

edit

The edit method is invoked each time a form is redrawn or a user provides input through a form control. The edit modifier is allowed only on table, form, and form data source methods.

final

Final methods can’t be overridden by methods with the same name in derived classes.

server

Server methods can execute only on an Application Object Server (AOS). The server modifier is allowed on all table methods and on static class methods.

static

Static methods are called using the name of the class rather than the name of an instance of the class. Fields can’t be accessed from within a static method.

Method parameters can have default values that are used when parameters are omitted from method invocations. The following code sample prints “Hello World” when myMethod is invoked with no parameters:

public void myMethod(str s = "Hello World")
{
    print s;
    pause;
}

public void run()
{
    this.myMethod();
}

A constructor is a special instance method that is invoked to initialize an object when the new operator is executed by the Microsoft Dynamics AX runtime. You can’t call constructors directly from X++ code. The sample on the next page provides an example of a class declaration header and an instance constructor method that takes one parameter as an argument.

class MyClass
{
    int i;

    public void new(int _i)
    {
        i = _i;
    }
}

Delegates

The purpose of delegates is to expose extension points where add-ons and customizations can extend the application in a lightweight manner without injecting logic into the base functionality. Delegates are methods without any implementation. Delegates are always protected and cannot have a return value. You declare a delegate using the delegate keyword. You invoke a delegate using the same syntax as a standard method invocation:

class MyClass
{
    delegate void myDelegate(int _i)
    {
    }

    private void myMethod()
    {
        this.myDelegate(42);
    }
}

When a delegate is invoked, the runtime automatically invokes all event handlers that subscribe to the delegate. There are two ways of subscribing to delegates: declaratively and dynamically. The runtime does not define the sequence in which event handlers are invoked. If your logic relies on an invocation sequence, you should use mechanisms other than delegates and event handlers.

To subscribe declaratively, right-click a delegate in the AOT and then select New Event Handler Subscription. On the resulting event handler node in the AOT, you can specify the class and the static method that will be invoked. The class can be either an X++ class or a .NET class.

To subscribe dynamically, you use the keyword eventhandler. Notice that when subscribing dynamically, the event handler is an instance method. It is also possible to unsubscribe.

class MyEventHandlerClass
{
    public void myEventHandler(int _i)
    {
        ...
    }

    public static void myStaticEventHandler(int _i)
    {
        ...
    }

    public static void main(Args args)
    {
        MyClass myClass = new MyClass();
        MyEventHandlerClass myEventHandlerClass = new MyEventHandlerClass();

        //Subscribe
        myClass.myDelegate += eventhandler(myEventHandlerClass.myEventHandler);
        myClass.myDelegate +=
                       eventhandler(MyEventHandlerClass::myStaticEventHandler);

        //Unsubscribe
        myClass.myDelegate -= eventhandler(myEventHandlerClass.myEventHandler);
        myClass.myDelegate -=
                       eventhandler(MyEventHandlerClass::myStaticEventHandler);
    }
}

Regardless of how you subscribe, the event handler must be public, return void, and have the same parameters as the delegate.

Note

Cross-tier events are not supported.

As an alternative to delegates, you can achieve a similar effect by using pre- and post-event handlers.

Pre- and post-event handlers

You can subscribe declaratively to any class and record type method by using the same procedure as for delegates. The event handler is invoked either before or after the method is invoked. Event handlers for pre- and post-methods must be public, static, void, and either take the same parameters as the method or one parameter of the XppPrePostArgs type.

The simplest, type-safe implementation uses syntax where the parameters of the method and the event handler method match.

class MyClass
{
    public int myMethod(int _i)
    {
        return _i;
    }
}

class MyEventHandlerClass
{
    public static void myPreEventHandler(int _i)
    {
        if (_i > 100)
        {
            ...
        }
    }

    public static void myPostEventHandler(int _i)
    {
        if (_i > 100)
        {
            ...
        }
    }
}

If you need to manipulate either the parameters or the return value, the event handler must take one parameter of the XppPrePostArgs type.

To create such an event handler, right-click the class, and then select New pre- or post-event handler. The XppPrePostArgs class provides access to the parameters and the return values of the method. You can even alter parameter values in pre-event handlers and alter the return value in post-event handlers.

class MyClass
{
    public int myMethod(int _i)
    {
        return _i;
    }
}

class MyEventHandlerClass
{
    public static void myPreEventHandler(XppPrePostArgs _args)
    {
        if (_args.existsArg('_i') &&
            _args.getArg('_i') > 100)
        {
            _args.setArg('_i', 100);
        }

    }

    public static void myPostEventHandler(XppPrePostArgs _args)
    {
        if (_args.getReturnValue() < 0)
        {
            _args.setReturnValue(0);
        }
    }
}

Attributes

Classes and methods can be decorated with attributes to convey declarative information to other code, such as the runtime, the compiler, frameworks, or other tools. To decorate the class, you insert the attribute in the classDeclaration element. To decorate a method, you insert the attribute before the method declaration:

[MyAttribute("Some parameter")]
class MyClass
{
    [MyAttribute("Some other parameter")]
    public void myMethod()
    {
        ...
    }
}

The first attribute that was built in Microsoft Dynamics AX 2012 was the SysObsoleteAttribute attribute. By decorating a class or a method with this attribute, any consuming code is notified during compilation that the target is obsolete. You can create your own attributes by creating classes that extend the SysAttribute class:

class MyAttribute extends SysAttribute
{
    str parameter;

    public void new(str _parameter)
    {
        parameter = _parameter;
        super();
    }
}

Code access security

Code access security (CAS) is a mechanism designed to protect systems from dangerous APIs that are invoked by untrusted code. CAS has nothing to do with user authentication or authorization; it is a mechanism allowing two pieces of code to communicate in a manner that cannot be compromised.

Caution

X++ developers are responsible for writing code that conforms to Trustworthy Computing guidelines. You can find those guidelines in the white paper “Writing Secure X++ Code,” available from the Microsoft Dynamics AX Developer Center (http://msdn.microsoft.com/en-us/dynamics/ax).

In the Microsoft Dynamics AX implementation of CAS, trusted code is defined as code from the AOT running on the Application Object Server (AOS). The first part of the definition ensures that the code is written by a trusted X++ developer. Developer privileges are the highest level of privileges in Microsoft Dynamics AX and should be granted only to trusted personnel. The second part of the definition ensures the code that the trusted developer has written hasn’t been tampered with. If the code executes outside the AOS—on a client, for example—it can’t be trusted because of the possibility that it was altered on the client side before execution. Untrusted code also includes code that is executed through the runBuf and evalBuf methods. These methods are typically used to execute code generated at run time based on user input.

CAS enables a secure handshake between an API and its consumer. Only consumers who provide the correct handshake can invoke the API. Any other invocation raises an exception.

The secure handshake is established through the CodeAccessPermission class or one of its specializations. The consumer must request permission to call the API, which is done by calling CodeAccessPermission.assert. The API verifies that the consumer has the correct permissions by calling CodeAccessPermission.demand. The demand method searches the call stack for a matching assertion. If untrusted code exists on the call stack before the matching assertion, an exception is raised. This process is illustrated in Figure 4-1.

The following code contains an example of a dangerous API protected by CAS and a consumer providing the correct permissions to invoke the API:

class WinApiServer
{
    // Delete any given file on the server
    public server static boolean deleteFile(Filename _fileName)
    {
        FileIOPermission    fileIOPerm;

        // Check file I/O permission
        fileIOPerm = new FileIOPermission(_fileName, 'w'),
        fileIOPerm.demand();

        // Delete the file

        System.IO.File::Delete(_filename);
    }
}

class Consumer
{
    // Delete the temporary file on the server
    public server static void deleteTmpFile()
    {
        FileIOPermission    fileIOPerm;
        FileName            filename = @'c:	mpfile.tmp';

        // Request file I/O permission
        fileIOPerm = new FileIOPermission(filename, 'w'),
        fileIOPerm.assert();

        // Use CAS protected API to delete the file
        WinApiServer::deleteFile(filename);
    }
}
CAS stack frame walk.

Figure 4-1. CAS stack frame walk.

WinAPIServer::deleteFile is considered to be a dangerous API because it exposes the .NET API System.IO.File::Delete(string fileName). Exposing this API on the server is dangerous because it allows the user to remotely delete files on the server, possibly bringing the server down. In the example, WinApiServer::deleteFile demands that the caller has asserted that the input file name is valid. The demand prevents use of the API from the client tier and from any code not stored in the AOT.

Caution

When using assert, make sure that you don’t create a new API that is just as dangerous as the one that CAS has secured. When you call assert, you are asserting that your code doesn’t expose the same vulnerability that required the protection of CAS. For example, if the deleteTmpFile method in the previous example had taken the file name as a parameter, it could have been used to bypass the CAS protection of WinApi::deleteFile and delete any file on the server.

Compiling and running X++ as .NET CIL

All X++ code is compiled into Microsoft Dynamics AX runtime bytecode intermediate format. This format is used by the Microsoft Dynamics AX runtime for Microsoft Dynamics AX client and server code.

Further, classes and tables are compiled into .NET common intermediate language (CIL). This format is used by X++ code executed by the Batch Server and in certain other scenarios.

The X++ compiler only generates Microsoft Dynamics AX runtime bytecode to generate CIL code; you must manually press either the Generate Full IL or Generate Incremental IL button. Both are available on the toolbar.

The main benefit of running X++ as CIL is performance. Generally the .NET runtime is significantly faster than the X++ runtime. In certain constructions, the performance gain is particularly remarkable:

  • Constructs with many method calls—Under the covers in the X++ runtime, any method call happens through reflection, whereas in CIL, this happens at the CPU level.

  • Constructions with many short-lived objects—Garbage collection in the Microsoft Dynamics AX runtime is deterministic, which means that whenever an instance goes out of scope, the entire object graph is analyzed to determine if any objects can be deallocated. In the .NET CLR, garbage collection is indeterministic, which means that the runtime determines the optimal time for reclaiming memory.

  • Constructions with extensive use of.NET interop—When running as X++ code as CIL, all conversion and marshaling between the runtimes are avoided.

Note

The capability to compile X++ into CIL requires that X++ syntax be as strict as the syntax in managed code. The most noteworthy change is that overridden methods must now have the same signature as the base method. The only permissible discrepancy is the addition of optional parameters.

One real-life example where running X++ code as .NET CIL makes a significant difference is in the compare tool. The compare algorithm is implemented as X++ code in the SysCompareText class. Even though the algorithm has few method calls, few short-lived objects, and no .NET interop, the switch to CIL means that within a time frame of 10 seconds, it is now possible to compare two 3,500-line texts, whereas the AX runtime can only handle 600 lines in the same time frame. The complexity of the algorithm is exponential. In other words the performance gain gets even more significant the larger the texts become.

All services and batch jobs will automatically run as CIL. If you want to force X++ code to run as CIL in non-batch scenarios, you use the methods runClassMethodIL and runTableMethodIL on the Global class. The IL entry point must be a static server method that returns a container and takes one container parameter:

class MyClass
{
    private static server container addInIL(container _parameters)
    {
        int p1, p2;
        [p1, p2] = _parameters;
        return [p1+p2];
    }

    public server static void main(Args _args)
    {
        int result;
        XppILExecutePermission permission = new XppILExecutePermission();
        permission.assert();
        [result] = runClassMethodIL(classStr(MyClass),
                                    staticMethodStr(MyClass, addInIL), [2, 2]);
        info(strFmt("The result from IL is: %1", result));
    }
}

Design and implementation patterns

So far, this chapter has described the individual elements of X++. You’ve seen that statements are grouped into methods, and methods are grouped into classes, tables, and other model element types. These structures enable you to create X++ code at a higher level of abstraction. The following example shows how an assignment operation can be encapsulated into a method to clearly articulate the intention of the code.

control.show();

is at a higher level of abstraction than

flags = flags | 0x0004;

By using patterns, developers can communicate their solutions more effectively and reuse proven solutions to common problems. Patterns help readers of source code to quickly understand the purpose of a particular implementation. Bear in mind that even as a code author, you spend more time reading source code than writing it.

Implementations of patterns are typically recognizable by the names used for classes, methods, parameters, and variables. Arguably, naming these elements so that they effectively convey the intention of the code is the developer’s most difficult task. Much of the information in existing literature on design patterns pertains to object-oriented languages, and you can benefit from exploring that information to find patterns and techniques you can apply when you’re writing X++ code. Design patterns express relationships or interactions between several classes or objects. They don’t prescribe a specific implementation, but they do offer a template solution for a typical design problem. In contrast, implementation patterns are implementation specific and can have a scope that spans only a single statement.

This section highlights some of the most frequently used patterns specific to X++. More descriptions are available in the Microsoft Dynamics AX SDK on MSDN.

Class-level patterns

These patterns apply to classes in X++.

Parameter method

To set and get a class field from outside the class, you should implement a parameter method. The parameter method should have the same name as the field and be prefixed with parm. Parameter methods come in two flavors: get-only and get/set.

public class Employee
{
    EmployeeName name;

    public EmployeeName parmName(EmployeeName _name = name)
    {
        name = _name;
        return name;
    }
}

Constructor encapsulation

The purpose of the constructor encapsulation pattern is to enable Liskov’s class substitution principle. In other words, with constructor encapsulation, you can replace an existing class with a customized class without using the layering system. Just as in the layering system, this pattern enables changing the logic in a class without having to update any references to the class. Be careful to avoid overlayering because it often causes upgrade conflicts.

Classes that have a static construct method follow the constructor encapsulation pattern. The construct method should instantiate the class and immediately return the instance. The construct method must be static and shouldn’t take any parameters.

When parameters are required, you should implement the static new methods. These methods call the construct method to instantiate the class and then call the parameter methods to set the parameters. In this case, the construct method should be private:

public class Employee
{
    ...
    protected void new()
    {
    }

    protected static Employee construct()
    {
        return new Employee();
    }

    public static Employee newName(EmployeeName name)
    {
        Employee employee = Employee::construct();

        employee.parmName(name);
        return employee;
    }
}

Factory

To decouple a base class from derived classes, use the SysExtension framework. This framework enables the construction of an instance of a class based on its attributes. This pattern enables add-ons and customizations to add new subclasses without touching the base class or the factory method:

class BaseClass
{
    ...
    public static BaseClass newFromTableName(TableName _tableName)
    {
        SysTableAttribute attribute = new SysTableAttribute(_tableName);

        return SysExtensionAppClassFactory::getClassFromSysAttribute(
            classStr(BaseClass), attribute);
    }
}

[SysTableAttribute(tableStr(MyTable))]
class Subclass extends BaseClass
{
   ...
}

Serialization with the pack and unpack methods

Many classes require the capability to serialize and deserialize themselves. Serialization is an operation that extracts an object’s state into value-type data; deserialization creates an instance from that data.

X++ classes that implement the Packable interface support serialization. The Packable interface contains two methods: pack and unpack. The pack method returns a container with the object’s state; the unpack method takes a container as a parameter and sets the object’s state accordingly. You should include a versioning number as the first entry in the container to make the code resilient to old packed data stored in the database when the implementation changes.

public class Employee implements SysPackable
{
    EmployeeName name;
    #define.currentVersion(1)
    #localmacro.CurrentList
        name
    #endmacro
    ...

    public container pack()
    {
        return [#currentVersion, #currentList];
    }

    public boolean unpack(container packedClass)
    {
        Version version = RunBase::getVersion(packedClass);

        switch (version)
        {
            case #CurrentVersion:
                [version, #CurrentList] = packedClass;
                break;
            default:   //The version number is unsupported
                return false;
        }
        return true;
    }
}

Table-level patterns

The patterns described in this section—the find and exists methods, polymorphic associations (Table/Group/All), and Generic Record References—apply to tables.

Find and exists methods

Each table must have the two static methods find and exists. They both take the primary keys of the table as parameters and return the matching record or a Boolean value, respectively. Besides the primary keys, the Find method also takes a Boolean parameter that specifies whether the record should be selected for update.

For the CustTable table, these methods have the following profiles:

static CustTable find(CustAccount _custAccount, boolean _forUpdate = false)
static boolean exist(CustAccount _custAccount)

Polymorphic associations

The Table/Group/All pattern is used to model a polymorphic association to either a specific record in another table, a collection of records in another table, or all records in another table. For example, a record could be associated with a specific item, all items in an item group, or all items.

You implement the Table/Group/All pattern by creating two fields and two relations on the table. By convention, the name of the first field has the suffix Code; for example, ItemCode. This field is modeled using the base enum TableGroupAll. The name of the second field usually has the suffix Relation; for example, ItemRelation. This field is modeled by using the extended data type that is the primary key in the foreign tables. The two relations are of the type Fixed field relation. The first relation specifies that when the Code field equals 0 (TableGroupAll::Table), the Relation field equals the primary key in the foreign master data table. The second relation specifies that when the Code field equals 1 (TableGroupAll::Group), the Relation field equals the primary key in the foreign grouping table.

Figure 4-2 shows an example.

A polymorphic association.

Figure 4-2. A polymorphic association.

Generic record reference

The Generic Record Reference pattern is a variation of the Table/Group/All pattern. This pattern is used to model an association to a foreign table. It comes in three flavors: (a) an association to any record in a specific table, (b) an association to any record in a fixed set of specific tables, and (c) an association to any record in any table.

All three flavors of this pattern are implemented by creating a field that uses the RefRecId extended data type.

To model an association to any record in a specific table (flavor a), a relation is created from the RefRecId field to the RecId field of the foreign table, as illustrated in Figure 4-3.

An association to a specific table.

Figure 4-3. An association to a specific table.

For flavors b and c, an additional field is required. This field is created by using the RefTableId extended data type. To model an association to any record in a fixed set of specific tables (flavor b), a relation is created for each foreign table from the RefTableId field to the TableId field of the foreign table, and from the RefRecId field to the RecId field of the foreign table, as shown in Figure 4-4.

An association to any record in a fixed set of tables.

Figure 4-4. An association to any record in a fixed set of tables.

To model an association to any record in any table (flavor c), a relation is created from the RefTableId field to the generic table Common TableId field and from the RefRecId field to Common RecId field, as shown in Figure 4-5.

An association to any record in any table.

Figure 4-5. An association to any record in any table.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset