Chapter 15. Reflection

The objectives of this chapter are to:

  • Introduce the concept of reflection.

  • Demonstrate the capabilities and limitations of the available reflection system functions and APIs.

Introduction

Reflection is a programmatic discoverability mechanism of the Microsoft Dynamics AX application model. In other words, reflection provides APIs for reading and traversing element definitions. By using the reflection APIs in the MorphX development environment, you can query metadata as though it were a table, an object model, or a tree structure.

You can do interesting analyses with the reflection information. The Reverse Engineering tool is an excellent example of the power of reflection. Based on element definitions in MorphX, the tool generates Unified Modeling Language (UML) models and Entity Relationship Diagrams (ERDs) that you can browse in Microsoft Office Visio.

Reflection also allows you to invoke methods on objects. This capability is of little value to business application developers who construct class hierarchies properly. For framework developers, however, the power to invoke methods on objects can be valuable. Suppose, for example, you want to programmatically write any record to an XML file that includes all of the fields and display methods. Reflection allows you to determine the fields and their values and also to invoke the display methods to capture their return values.

X++ features a set of system functions that you can use for reflection, as well as three reflection APIs. The reflection system functions follow:

  • Intrinsic functions. A set of functions that allow you to refer to an element’s name or ID safely at compile time

  • TypeOf system function. A function that returns the primitive type for a variable

  • ClassIdGet system functionA function that returns the ID of the class for an instance of an object

These are the reflection APIs:

  • Table data. A set of tables that contains all element definitions. The tables give you direct access to the contents of the .aod files. You can query for the existence of elements and certain properties, such as created by and created datetime. You can’t retrieve information about the contents or structure of each element.

  • Dictionary. A set of classes that provide a type-safe mechanism for reading metadata from an object model. Dictionary classes provide basic and more abstract information about elements in a type-safe manner. With few exceptions, this API is read-only.

  • Treenodes. A class hierarchy that provides the Application Object Tree (AOT) with an API that can be used to create, read, update, and delete any piece of metadata or source code. This API can tell you everything about anything in the AOT. You navigate the tree-nodes in the AOT through the API and query for metadata in a non-type-safe manner.

We spend the rest of this chapter delving into the details of these system functions and APIs.

Reflection System Functions

The X++ language features a set of system functions that can be used to reflect on elements. They are described in the following sections.

Intrinsic Functions

You should use intrinsic functions whenever you need to reference an element from within X++ code. Intrinsic functions provide a way to make a type-safe reference. The compiler recognizes the reference and verifies that the element being referenced exists. If the element doesn’t exist, the code doesn’t compile. Because elements have their own life cycles, a reference doesn’t remain valid forever; an element can be renamed or deleted. Using intrinsic functions ensures that you are notified of any broken references at compile time. A compiler error early in the development cycle is always better than a run-time error.

All references you make using intrinsic functions are captured by the Cross-reference tool. So you can determine where any element is referenced, regardless of whether the reference is in metadata or code. The Cross-reference tool is described in Chapter 3.

Consider these two implementations.

print "MyClass";          //Prints MyClass
print classStr(MyClass);  //Prints MyClass

They have exactly the same result: the string "MyClass" is printed. As a reference, the first implementation is weak. It will eventually break when the class is renamed or deleted, meaning that you’ll need to spend time debugging. The second implementation is strong and unlikely to break. If you were to rename or delete MyClass, you could use the Cross-reference tool to do an impact analysis of your changes and correct any broken references.

Using the intrinsic functions <Concept>Str, you can reference all the elements in the AOT by their names. You can also reference elements that have an ID with the intrinsic function <Concept>Num. Intrinsic functions are not limited to parent objects; they also exist for class methods, table fields, indexes, and methods. More than 50 intrinsic functions are available. Here are a few examples of intrinsic functions.

print fieldNum(MyTable, MyField);   //Prints 50001
print fieldStr(MyTable, MyField);   //Prints MyField
print methodStr(MyClass, MyMethod); //Prints MyMethod
print formStr(MyForm);              //Prints MyForm

An element’s ID is assigned when the element is created. The ID is a sequential ID dependant on an application model layer. In the preceding example, 50001 is the ID assigned to the first element created in the USR layer. Element IDs are explained in Chapter 1.

Two other intrinsic functions are worth noting: identifierStr and literalStr. IdentifierStr allows you to refer to elements when a more feature-rich intrinsic function isn’t available. IdentifierStr provides no compile-time checking and no cross-reference information. Using the identifierStr function is much better than using a literal, because the intention of referring to an element is captured. If a literal is used, the intention is lost—the reference could be to user interface text, a file name, or something completely different. The Best Practices tool detects the use of identifierStr and issues a best practice warning.

The Dynamics AX runtime automatically converts any reference to a label identifier to the label text for the label identifier. In most cases, this behavior is what you want; however, you can avoid the conversion by using literalStr. LiteralStr allows you to refer to a label identifier without converting the label ID to the label text, as shown here.

print "@SYS1";             //Prints Time transactions
print literalStr("@SYS1"); //Prints @SYS1

In the first line of the example, the label identifier (@SYS1) is automatically converted to the label text (Time transactions). In the second line, the reference to the label identifier isn’t converted.

TypeOf System Function

The TypeOf system function takes a variable instance as a parameter and returns the primitive type of the parameter. Here is an example.

int i = 123;
str s = "Hello world";
MyClass c;
Guid g = newGuid();

print typeOf(i);  //Prints Integer
print typeOf(s);  //Prints String
print typeOf(c);  //Prints Class
print typeOf(g);  //Prints Guid
pause;

The return value is an instance of the Types system enumeration. It contains an enumeration for each primitive type in X++.

ClassIdGet System Function

The ClassIdGet system function takes an object as a parameter and returns the class ID for the class element of which the object is an instance. If the parameter passed is Null, the function returns the class ID for the declared type, as shown here.

MyBaseClass c;
print classIdGet(c);  //Prints 50001

c = new MyDerivedClass();
print classIdGet(c);  //Prints 50002
pause;

This function is particularly useful for determining the type of an object instance. Suppose you need to determine whether a class instance is a particular class. The following example shows how you can use ClassIdGet to determine the class ID of the _anyClass variable instance. If the _anyClass variable really is an instance of MyClass, it’s safe to assign it to the variable myClass.

void myMethod(object _anyClass)
{
    MyClass myClass;
    if (classIdGet(_anyClass) == classNum(MyClass))
    {
        myClass = _anyClass;
        ...
    }
}

Notice the use of the intrinsic function, which evaluates at compile time, and the use of classIdGet, which evaluates at run time.

Because inheritance isn’t taken into account, this sort of implementation is likely to break the object model. In most cases, any instance of a derived MyClass class should be treated as an actual MyClass instance. The simplest way to handle inheritance is to use the is and as static methods on the SysDictClass class. You’ll recognize these methods if you’re familiar with C#. The is method returns true if the object passed in is of a certain type, and the as method can be used to cast an instance to a particular type. The as method returns null if the cast is invalid.

These two methods also take interface implementations into account. So with the as method, you can cast your object to an interface. Here is a revision of the preceding example using the as method.

void myMethod(object _anyClass)
{
    MyClass myClass = SysDictClass::as(_anyClass, classNum(MyClass));
    if (myClass)
    {
        ...
    }
}

Here is an example of an interface cast.

void myMethod2(object _anyClass)
{
    SysPackable packableClass =
        SysDictClass::as(_anyClass, classNum(SysPackable));
    if (packableClass)
    {
        packableClass.pack();
    }
}

Note

Note

This book promotes customization through inheritance using the Liskov substitution principle. Read more about Dynamics AX Smart Customization techniques in Chapter 18.

Reflection APIs

The X++ system library includes three APIs that can be used to reflect on elements. They are described in the following sections.

Table Data API

Suppose that you want to find all classes whose names begin with Invent and that have been modified within the last month. The following example shows one way to conduct your search.

static void findInventoryClasses(Args _args)
{
    UtilElements utilElements;

    while select name from utilElements
        where utilElements.RecordType == UtilElementType::Class
           && utilElements.Name like 'Invent*'
           && utilElements.ModifiedDateTime >
              DateTimeUtil::addDays(DateTimeUtil::getSystemDateTime(), -30)
    {
        info(strfmt("%1", utilElements.Name));
    }
}

The UtilElements table provides access to all elements. The RecordType field holds the concept. Other fields in the UtilElements table that can be reflected on are Name, CreatedBy, CreatedDateTime, ModifiedBy, and ModifiedDateTime.

Because of the nature of the table data API, the UtilElements table can also be used as a data source in a form or a report. A form showing the table data is available from Tools Development ToolsApplication ObjectsApplication Objects. In the form, you can use the standard query capabilities to filter and search the data.

Some elements have sub-elements associated with them. For example, a table has fields and methods. This parent/child association is captured in the ParentId field of the sub-element. The following job finds all static method elements on the CustTable table element by selecting only table static method elements whose ParentId equals the CustTable table ID.

static void findStaticMethodsOnCustTable(Args _args)
{
    UtilElements utilElements;

    while select name from utilElements
        where utilElements.recordType == UtilElementType::TableStaticMethod
           && utilElements.ParentId == tableNum(CustTable)
    {
        info(strfmt("%1", utilElements.name));
    }
}

Notice the use of field lists in the select statements in the examples in this section. Each record in the table also has a binary large object (BLOB) field that contains all the metadata, source code, and bytecode. This BLOB field can’t be interpreted from X++ code, so you don’t need to fetch it. When you specify a file list to the select statement with fields from the primary index, fetching the actual record is avoided, and the select statement returns the result much faster. The primary index contains these fields: RecordType, ParentId, Name, and UtilLevel.

The UtilLevel field contains the layer of the element. The following job finds all parent elements in the USR layer.

static void findParentElementsInUSRLayer(Args _args)
{
    UtilElements utilElements;

    while select recordType, name from utilElements
        where utilElements.ParentId == 0
           && utilElements.utilLevel == UtilEntryLevel::usr
    {
        info(strfmt("%1 %2", utilElements.recordType, utilElements.name));
    }
}

As you learned in Chapter 1, elements can have IDs. The UtilElements table can’t provide ID information. To get ID information, you must use the UtilIdElements table. The two tables are both views on the elements in the .aod files; the only difference is the inclusion of the ID field in the UtilIdElements table. The following code is a revision of the previous job that also reports IDs.

static void findParentElementsInUSRLayer(Args _args)
{
    UtilIdElements utilIdElements;

    while select RecordType, Id, Name from utilIdElements
        where utilIdElements.ParentId == 0
           && utilIdElements.UtilLevel == UtilEntryLevel::usr
    {
        info(strfmt("%1 %2 %3",
            utilIdElements.RecordType,
            utilIdElements.Name,
            utilIdElements.Id));
    }
}

Although we have discussed two tables that contain the .aod files in this section, all the application data files have a table reflection API similar to the ones we have mentioned. Table 15-1 lists some additional reflection tables.

Table 15-1. Reflection Tables

Table Name

Description

UtilElements, UtilIdElements

Tables containing the .aod files, which contain elements.

UtilElementsOld UtilIdElementsOld

Tables containing the .aod files in the Old application folder. This information is useful during code upgrades.

UtilApplHelp

Table containing the .ahd files, which contain online Help information for users.

UtilApplCodeDoc

Table containing the .add files, which contain developer documentation information for elements.

UtilCodeDoc

Table containing the .khd files, which contain developer documentation information for Dynamics AX system APIs.

All the tables listed in Table 15-1 have an associated class. These classes contain a set of static methods that are generally helpful. All the classes have the same name as the table, prefixed with an x.

Suppose you want to report the AOT path for MyForm from the table utilIdElements. You could use the xUtilIdElements function to return this information, as in the following code.

static void findAOTPathForMyForm(Args _args)
{
    UtilIdElements utilIdElements = xUtilIdElements::find(
        UtilElementType::Form, FormStr(MyForm));

    if (utilIdElements)
        info(xUtilIdElements::getNodePath(utilIdElements));
}

Note

Note

When you use the table data API in an environment with version control enabled, the values of some of the fields are reset during the build process. For file-based version control systems, the build process imports .xpo files into empty layers in Dynamics AX. The values of the CreatedBy, CreatedDateTime, ModifiedBy, and ModifiedDateTime fields are set during this import process and therefore don’t survive from build to build.

Dictionary API

The dictionary API is a type-safe reflection API that can reflect on many elements. The following code sample is a revision of the preceding example that finds inventory classes by using the dictionary API. You can’t use this API to get information about when an element was modified. Instead, this example reflects a bit more on the class information and lists only abstract classes.

static void findAbstractInventoryClasses(Args _args)
{
    Dictionary dictionary = new Dictionary();
    int i;
    DictClass dictClass;

    for(i=1; i<=dictionary.classCnt(); i++)
    {
        dictClass = new DictClass(dictionary.classCnt2Id(i));

        if (dictClass.isAbstract() &&
            strStartsWith(dictClass.name(), 'Invent'))
        {
            info(strfmt("%1", dictClass.name()));
        }
    }
}

The Dictionary class provides information about which elements exist. With this information, you can instantiate a DictClass object that provides specific information about the class, such as whether the class is abstract, final, or an interface; which class it extends; whether it implements any interfaces; and what methods it includes. Notice that the DictClass class can also reflect on interfaces. Also notice how the class counter is converted into a class ID; this conversion is required because the IDs aren’t listed consecutively.

When you run this job, you’ll notice that it’s much slower than the implementation that uses the table data API—at least the first time you run it! The job performs better after the information is cached.

Figure 15-1 shows the object model for the dictionary API. As you can see, some elements can’t be reflected upon by using this API.

The object model for the dictionary reflection API

Figure 15-1. The object model for the dictionary reflection API

The following example revises the FindStaticMethodsOnCustTable from the preceding code by using the dictionary API. It also reports the method parameters of the methods.

static void findStaticMethodsOnCustTable(Args _args)
{
    DictTable dictTable = new DictTable(tableNum(CustTable));
    DictMethod dictMethod;
    int i;
    int j;
    str parameters;

    for (i=1; i<=dictTable.staticMethodCnt(); i++)
    {
        dictMethod = new DictMethod(
            UtilElementType::TableStaticMethod,
            dictTable.id(),
            dictTable.staticMethod(i));

        parameters = '';
        for (j=1; j<=dictMethod.parameterCnt(); j++)
        {
            parameters += strfmt("%1 %2",
                extendedTypeId2name(dictMethod.parameterId(j)),
                dictMethod.parameterName(j));

            if (j<dictMethod.parameterCnt())
                parameters += ', ';
        }
        info(strfmt("%1(%2)", dictMethod.name(), parameters));
    }
}

As mentioned earlier, reflection can also be used to invoke methods on objects. This example invokes the static Find method on the table CustTable.

static void invokeFindOnCustTable(Args _args)
{
    DictTable dictTable = new DictTable(tableNum(CustTable));
    CustTable customer;
;
    customer = dictTable.callStatic(
        tableStaticMethodStr(CustTable, Find), '1201'),

    print customer.Name;     //Prints Sparrow Wholesales
    pause;
}

Notice the use of the intrinsic function tableStaticMethodStr to make a reference to the Find method.

You can also use this API to instantiate class and table objects. Suppose you want to select all records in a table with a given table ID. The following example shows you how.

void findRecords(TableId _tableId)
{
    DictTable dictTable = new DictTable(_tableId);
    Common common = dictTable.makeRecord();
    FieldId primaryKeyField = DictTable.primaryKeyField();

    while select common
    {
        info(strfmt("%1", common.(primaryKeyField)));
    }
}

First, notice the call to the makeRecord method that instantiates a table cursor object that points to the correct table. You can use the select statement to select records from the table. If you wanted to, you could also insert records by using the table cursor. Notice the syntax used to get a field value out of the cursor object; this syntax allows any field to be accessed by its field ID. This example simply prints the content of the primary key field. You can use the makeObject method on the class DictClass to create an object instance of a class.

All the classes in the dictionary API discussed so far are defined as system APIs. On top of each of these is an application-defined class that provides even more reflection capabilities. These classes are named SysDict<Concept>, and each class extends its counterpart in the system API. For example, SysDictClass extends DictClass.

Consider the following example. Table fields have a property that specifies whether the field is mandatory. The DictField class returns the value of the mandatory property as a bit set in the return value of its flag method. Testing of a bit set is somewhat cumbersome, and if the implementation of the flag changes, the consuming applications breaks. The SysDictField class encapsulates the bit-testing logic in a mandatory method. Here is how the method is used.

static void mandatoryFieldsOnCustTable(Args _args)
{
    DictTable dictTable = new DictTable(tableNum(CustTable));
    SysDictField sysDictField;
    int i;

    for (i=1; i<=dictTable.fieldCnt(); i++)
    {
        sysDictField = new SysDictField(
            dictTable.id(), dictTable.fieldCnt2Id(i));

        if (sysDictField.mandatory())
            info(sysDictField.name());
    }
}

You might also want to browse the SysDict classes for static methods. Many of these provide additional reflection information and better interfaces. For example, the SysDictionary class provides a classes method that has a collection of SysDictClass instances. You could use this method to simplify the earlier findAbstractInventoryClasses example.

Notice how all the examples instantiate the dictionary classes by using their new constructor. Some developers use an alternative way to instantiate the dictionary classes, but you should avoid it. Recall the hierarchy of the objects shown in Figure 15-1. A parent object can return an instance of a child object, as shown here.

DictTable dictTable = new DictTable(tableId);
DictField firstField, nextField;
firstField = dictTable.fieldObject(dictTable.fieldNext(0));
nextField = dictTable.fieldObject(dictTable.fieldNext(dictField.id()));

The primary reason to avoid this construct is that you can’t substitute Dict classes with SysDict classes. If you ever need reflection methods available only on the SysDict classes, you must refactor the code. Writing the code so that it is easy to substitute the class makes refactoring easier and lowers the risk of introducing bugs in the refactoring process. Another reason to avoid this construct is the lack of API consistency. The examples used in this section that instantiate dictionary classes all follow the same structure, which is consistent for all the classes in the dictionary API.

Treenodes API

The two reflection APIs discussed so far both have limitations. The table data API can reflect only on the existence of elements and a small subset of element metadata. The dictionary API can reflect in a type-safe manner but only on the element types that are exposed through this API.

The treenodes API can reflect on everything, but as always, power comes at a cost. The tree-nodes API is harder to use than the other reflection APIs discussed. It can cause memory and performance problems, and it isn’t type-safe.

The following example revises the example from the "Table Data API" section, now using the treenodes API to find inventory classes.

static void findInventoryClasses(Args _args)
{
    TreeNode classesNode = TreeNode::findNode(@'Classes'),
    TreeNodeIterator iterator = ClassesNode.AOTiterator();
    TreeNode classNode = iterator.next();
    ClassName className;

    while (classNode)
    {
        className = classNode.treeNodeName();
        if (strStartsWith(className, 'Invent'))
            info(strfmt("%1", className));

        classNode = iterator.next();
    }
}

First, notice how you find a node in the AOT based on the path as a literal. The AOT macro contains definitions for the primary AOT paths. For readability reasons, the examples in this chapter don’t use the macro. Also notice the use of a TreeNodeIterator class to loop over the classes.

If you stay at the class level in the AOT, you don’t encounter problems—but be careful if you go any deeper. Tree nodes in MorphX contain data that the Dynamics AX runtime doesn’t manage, and nodes’ memory isn’t automatically deallocated. For each parent node that is expanded, you should call the TreenodeRelease method to free the memory. For an example of this, see the doTreeNode method on the SysBpCheck class.

The following small job prints the source code for the doTreeNode method by calling the AOTgetSource method on the treenode object for the doTreeNode method.

static void printSourceCode(Args _args)
{
    TreeNode treeNode =
        TreeNode::findNode(@'ClassesSysBpCheckdoTreeNode'),
;
    info(treeNode.AOTgetSource());
}

The treenodes API provides access to the source code of nodes in the AOT. You can use the class ScannerClass to turn the string that contains the source code into a sequence of compilable tokens.

The following code revises the preceding example to find mandatory fields on the table CustTable.

static void mandatoryFieldsOnCustTable(Args _args)
{
    TreeNode fieldsNode = TreeNode::findNode(
        @'Data DictionaryTablesCustTableFields'),

    TreeNode field = fieldsNode.AOTfirstChild();

    while (field)
    {
        if (field.AOTgetProperty('Mandatory') == 'Yes')
            info(field.treeNodeName());

        field = field.AOTnextSibling();
    }
}

Notice the alternate way of looping over subnodes. Both this and the iterator approach work equally well. The only way to determine whether a field is mandatory with this API is to know that your node models a field and that field nodes have a property named Mandatory, which is set to Yes (not to True) for mandatory fields.

Use the Properties macro when referring to property names. It contains text definitions for all property names. By using this macro, you avoid using literal names, as in the reference to Mandatory in the preceding example.

Unlike the dictionary API, which can’t reflect all elements, the treenodes API reflects everything. The SysDictMenu class exploits this fact, providing a type-safe way to reflect on menus and menu items by wrapping information provided by the treenodes API in a type-safe API. The following job prints the structure of the MainMenu menu, which typically is shown in the navigation pane.

static void printMainMenu(Args _args)
{
    void reportLevel(SysDictMenu _sysDictMenu)
    {
        SysMenuEnumerator enumerator;

        if (_sysDictMenu.isMenuReference() ||
            _sysDictMenu.isMenu())
        {
            setPrefix(_sysDictMenu.label());
            enumerator = _sysDictMenu.getEnumerator();
            while (enumerator.moveNext())
                reportLevel(enumerator.current());
        }
        else
            info(_sysDictMenu.label());
    }

    reportLevel(SysDictMenu::newMainMenu());
}

Notice how the setPrefix function is used to capture the hierarchy and how the reportLevel function is called recursively.

The treenodes API also allows you to reflect on forms and reports, as well as their structure, properties, and methods. The Compare tool in MorphX uses this API to compare any node with any other node. The SysTreeNode class contains a TreeNode class and implements a cascade of interfaces, which makes TreeNode classes consumable for the Compare tool and the Version Control tool. The SysTreeNode class also contains a powerful set of static methods.

The TreeNode class is actually the base class of a larger hierarchy. You can cast instances to specialized TreeNode classes that provide more specific functionality. The hierarchy isn’t fully consistent for all nodes. You can browse the hierarchy in the AOT by clicking System Documentation, clicking Classes, right-clicking TreeNode, pointing to Add-Ins, and then clicking Application Hierarchy.

The xUtil classes shown in the table data API examples contain methods for transitioning between the class paradigm of TreeNode classes and the table paradigm of UtilElements tables. Here is an example.

TreeNode node1 = TreeNode::findNode(@'Data DictionaryTablesCustTable'),
UtilElements utilElements = xUtilElements::findTreeNode(custTableNode);
TreeNode node2 = xUtilElements::getNodeInTree(utilElements);

Although we’ve covered only the reflection functionality of the treenodes API, you can use the API just as you would use the AOT designer. You can create new elements and modify properties and source code. The Wizard Wizard uses the treenodes API to generate the project, form, and class implementing the wizard functionality. You can also compile and get layered nodes and nodes from the Old application folder (located in Program Files Microsoft Dynamics AX5.0ApplicationApplDynamicsAx1Old). The capabilities that go beyond reflection are very powerful, but proceed with great care. Obtaining information in a non-type-safe manner requires caution, but writing in a non-type-safe manner can lead to cataclysmic situations.

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

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