Chapter 20. Reflection

In this chapter

Introduction

Reflection system functions

Reflection APIs

Introduction

Reflection is the process of obtaining information about assemblies and the types defined within them, and creating, invoking, and accessing type instances at run time. By using the reflection application programming interfaces (APIs) of the AX 2012 application model, you can read and traverse element definitions as though they were in a table, an object model, or a tree structure.

You can perform interesting analyses with the information that you get through reflection. The Reverse Engineering tool provides an excellent example of the power of reflection. By using the element definitions in MorphX, the tool generates Unified Modeling Language (UML) models and entity relationship diagrams (ERDs) that you can browse in Microsoft Visio.

You can also use reflection to invoke methods on objects. This capability is of little value to business application developers. But for framework developers, the power to invoke methods on objects can be valuable. Suppose you want to programmatically write any record to an XML file that includes all of the fields and display methods. With reflection, you can determine the fields and their values and invoke the display methods to capture their return values.

X++ features a set of system functions that you can use for reflection, in addition to three reflection APIs. The reflection system functions are as follows:

Image Intrinsic functions A set of functions that you can use to safely refer to an element’s name or ID

Image typeOf system function A function that returns the primitive type for a variable

Image classIdGet system function A function that returns the ID of the class for an instance of an object

The reflection APIs are as follows:

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

Image Dictionary A set of classes that provides 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.

Image 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 provide all information about anything in the AOT. You navigate the treenodes in the AOT through the API and query for metadata in a non–type-safe manner.

This chapter delves 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

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

All references you make by using intrinsic functions are captured by the Cross-Reference tool. 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 2, “The MorphX development environment and tools.”

Consider these two implementations:

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

Both lines of code have the same result: the string MyClass is printed. As a reference, the first implementation is weak. It will eventually break if 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 analyze the impact of your changes and correct any broken references.

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

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

The ID of an element is assigned when the element is created in the model store. In the preceding example, the ID 60001 is assigned to the first element field created in a table. (Element IDs are explained in Chapter 21, “Application models.”)

Two other intrinsic functions are worth noting: identifierStr and literalStr. The identifierStr function allows you to refer to elements if a more feature-rich intrinsic function isn’t available. The identifierStr function provides no compile-time checking and no cross-reference information. However, using the identifierStr function is still 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 might 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 AX 2012 runtime automatically converts any reference to a label ID to its corresponding label text. In most cases, this behavior is what you want; however, you can prevent the conversion by using literalStr. The literalStr function allows you to refer to a label ID without converting the label ID to the label text, as shown in this example:

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

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

typeOf system function

The typeOf system function takes a variable instance as a parameter and returns the base 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 base 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 in this example:

MyBaseClass c;
print classIdGet(c);  //Prints the ID of MyBaseClass

c = new MyDerivedClass();
print classIdGet(c);  //Prints the ID of MyDerivedClass
pause;

This function is particularly useful for determining the type of an object instance. Suppose you need to determine whether a class instance is of 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 myClass variable.

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

Notice the use of the classNum intrinsic function, which evaluates the parameter at compile time, and the use of classIdGet, which evaluates the parameter 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 operators. For more information, see Chapter 4, “The X++ programming language.”


Image Note

This book promotes customization through inheritance by using the Liskov substitution principle.


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. The following example shows one way to conduct your search:

static void findInventoryClasses(Args _args)
{
    SysModelElement modelElement;

    while select name from modelElement
        where modelElement.ElementType == UtilElementType::Class
           && modelElement.Name like 'Invent*'
    {
        info(modelElement.Name);
    }
}

The SysModelElement table provides access to all elements. The ElementType field holds the concept to search for. The data model for the model store contains nine tables, which are shown in Figure 20-1.

Image

FIGURE 20-1 The data model for the model store.


Image Note

The UtilElements table is still available for backward compatibility. It is implemented as an aggregated view on top of the SysModel tables. For performance reasons, you should limit usage of this compatibility feature and eventually rewrite your code to use the new API.


Because of the nature of the table data API, the SysModel tables can also be used as data sources in a form or a report. A form showing the table data is available from Tools > Model Management > Model Elements. In the form, you can use standard query capabilities to filter and search the data.

The SysModelElement table contains all of the elements in the model store; it is related to the SysModelElementData table, which contains the various definitions of each element. For each SysModelElement record, there is at least 1 SysModelElementData record—and perhaps as many as 16 if the element is customized across all 16 layers. In other words, the element defines the customization granularity. You cannot customize a unit that is smaller than an element. For example, even if you change just one property on an element, a new record is inserted into the SysModelElementData table that includes all properties of the element.


Image Note

System elements, as listed under the System Documentation node in the AOT, are not present in these tables.


Elements are structured in hierarchies. The root of a hierarchy is the root element—for example, a form. The form contains data source, control, and method elements. The hierarchy can encompass multiple levels; for example, a form control can have methods. The root element and parent element are exposed in the RootModelElement and ParentModelElement fields of the SysModelElement table. The job in the following code finds all elements under the CustTable form element and lists the name and type of each element, the name of the parent element, and the AOT path of the associated TreeNode class.

static void findElementsOnCustTable(Args _args)
{
    SysModelElement modelElement;
    SysModelElement rootModelElement;
    SysModelElement parentModelElement;
    SysModelElementType modelElementType;

    while select name from modelElement
        join Name from modelElementType
            where modelElementType.RecId == modelElement.ElementType
        join name from parentModelElement
            where parentModelElement.RecId == modelElement.ParentModelElement
        exists join rootModelElement
            where rootModelElement.RecId == modelElement.RootModelElement
               && rootModelElement.Name == formStr(CustTable)
               && rootModelElement.ElementType == UtilElementType::Form
    {
        info(strFmt("%1, %2, %3, %4",
            parentModelElement.Name, modelElementType.Name, modelElement.Name,
            SysTreeNode::modelElement2Path(modelElement)));
    }
}

Notice the use of the ElementType field in the two preceding examples. If the element type is a UtilElement, you will find a matching entry in the UtilElementType enum; alternatively, you can always join to the SysModelElementType table, which contains information about all element types. All root elements and a few former subelements are Utilelements. You can access them through the legacy UtilElements table. Data models with higher fidelity were introduced in AX 2012 to support more granular customizations, which among other things facilitate easier upgrade and simpler side-by-side installation of models. For more information, see Chapter 21.

Table 20-1 lists the reflection tables and views. See Figure 20-1 to learn how these tables relate to each other.

Image

TABLE 20-1 Reflection tables and views.


Image Note

Alternative versions of the tables in Table 20-1 exist. If you postfix the table name with the word Old, you can access the baseline model store instead of the primary model store. For example, the SysModelElementOld table contains the model elements in the baseline model store. The baseline model store is primarily used in upgrade scenarios.


You can use the Microsoft.Dynamics.AX.Framework.Tools.ModelManagement namespace provided by the AxUtilLib.dll assembly to create, import, export, and delete models. This assembly can be used from X++—the SysModelStore class wraps some of the functionality for easier consumption in X++.


Image 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 AX 2012. 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 example is a revision of the preceding example that finds inventory classes by using the dictionary API. This API gives you access to more detailed type information. This example lists only abstract classes that start with the string Invent:

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(dictClass.name());
        }
    }
}

The Dictionary class provides information about which elements exist and even includes system elements. For example, with this information, you can instantiate a DictClass object that provides information about the class, such as whether the class is abstract, final, or an interface; which class it extends; whether it implements any interfaces; what attributes it is decorated with; and what methods it includes. Notice that the DictClass class can also reflect on interfaces. Also notice that 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 20-2 shows the objects that support reflection in the dictionary API.

Image

FIGURE 20-2 The object model for the dictionary reflection API.

The following example lists the static methods on the CustTable table and reports their parameters:

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. The following example invokes the static find method on the CustTable table:

static void invokeFindOnCustTable(Args _args)
{
    DictTable dictTable = new DictTable(tableNum(CustTable));
    CustTable customer;

    customer = dictTable.callStatic(
        tableStaticMethodStr(CustTable, Ffind), '1201'),

    print customer.currencyName();    //Prints US Dollar
    pause;
}

Notice the use of the tableStaticMethodStr intrinsic function to reference 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 specified table name. The following example shows you how:

static 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, which 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 want to, you can 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 prints the content of the primary key field. Alternatively, you can use the getFieldValue method to get a value based on the name of the field. You can use the makeObject method on the DictClass class to create an object instance of a class.

All of 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 a mandatory property as a bit that is set in the return value of its flag method. Testing to determine whether a bit is set is somewhat cumbersome, and if the implementation of the flag changes, the consuming application breaks. The SysDictField class encapsulates the bit-testing logic in a mandatory method. The following example shows how to use the method:

static void mandatoryFieldsOnCustTable(Args _args)
{
    SysDictTable sysDictTable = SysDictTable::newName(tableStr(CustTable));
    SysDictField sysDictField;
    Enumerator enum = sysDictTable.fields().getEnumerator();

    while (enum.moveNext())
    {
        sysDictField = enum.current();

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

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

Treenodes API

The two reflection APIs discussed so far have limitations. The table data API can reflect only on the existence of elements and on 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 treenodes API is harder to use than the other reflection APIs, it can cause memory and performance problems, and it isn’t type-safe.

In the following code, the example from the “Table data API” section has been revised to use 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(className);
        }

        classNode = iterator.next();
    }
}

First, notice that 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, the examples in this chapter don’t use the macro. Also notice the use of a TreeNodeIterator class to iterate through the classes.

The following small job prints the source code for the find method on the CustTable table by calling the AOTgetSource method on the treenode object for the find method:

static void printSourceCode(Args _args)
{
    TreeNode treeNode =
        TreeNode::findNode(@'Data DictionaryTablesCustTableMethodsfind'),

    info(treeNode.AOTgetSource());
}

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

In the following code, the preceding example has been revised to find mandatory fields on the CustTable table:

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 traversing 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. 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. This macro contains text definitions for all property names. By using this macro, you avoid using literal names, like the reference to the Mandatory property in the preceding example.

Unlike the dictionary API, which can’t reflect all elements, the treenodes API reflects everything. The SysDictMenu class exploits this capability, 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 that the setPrefix function is used to capture the hierarchy and that the reportLevel function is called recursively.

You can also use the treenode API to reflect on forms and reports, and on 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 Type Hierarchy Browser.

Although this section has only covered the reflection functionality of the treenodes API, you can use the API just as you do 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 baseline model store. 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 catastrophic situations.

TreeNodeType

Different types of treenodes have different capabilities. The TreeNodeType class can be used to reflect on the treenode. The TreeNodeType class provides reliable alternatives to making assumptions about a treenode’s capabilities based on its properties. In previous versions of Microsoft Dynamics AX, fragile assumptions could be found throughout the code base; for example, it was assumed that a treenode supported version control if the treenode had a utilElementType and no parent ID.

The TreeNodeType class provides a method that returns the type identification, plus seven methods that return Boolean values providing information about the treenode’s capabilities. The usage of these methods is described later in this section. Figure 20-3 shows the information that the TreeNodeType class provides for each treenode in a project containing a table and a form. The left side of the illustration shows a screenshot of the project itself. The right side contains a table that, for each treenode, lists the treenode type ID and capabilities.

Image

FIGURE 20-3 Information provided by the TreeNodeType class for the treenodes in a table and a form.

The following list describes the treenode type ID and capabilities in more detail:

Image ID The ID of the treenode type is defined in the system and is available in the TreeNodeSysNodeType macro. Nodes with the same ID have the same behavior.

Image isConsumingMemory Tree nodes in MorphX contain data that the AX 2012 runtime doesn’t manage, and the memory for a node isn’t automatically deallocated. For each node where isConsumingMemory is true, you should call the treenodeRelease method to free the memory when you no longer reference any subnodes. Alternatively, you can use the TreeNodeTraverser class, because the class will handle this task for you. For an example of this, see the traverseTreeNodes method of the SysBpCheck class.

Image isGetNodeInLayerSupported With treenodes that support the getNodeInLayer method, you can navigate to versions of the node in other layers. In other words, you can access the nodes in the lower layers by using this method.

Image isLayerAware Treenodes that are layer-aware display a layer indicator in the AOT—for example, SYS or USR. You can retrieve the layer of a node by using the AOTLayer method, and you can retrieve all layers that are available by using the AOTLayers method. Note that the AOTLayers method does not roll up layers for subnodes; this method returns what is shown in the AOT. The roll-up layer information is available through the ApplObjectLayerMask method, which is used in the AOT to determine whether a node is shown in bold. If a node is bold in the AOT, either the node itself or one of its subnodes is present in the current layer.

Image isModelElement Treenodes that are model elements are represented by a record in the SysModelElement table.

Image isRootElement A root element is placed in the root of the treenode hierarchy, and the RootModelElement field for all submodel elements references the root element’s recid.

Image isUtilElement If the treenode is a UtilElement, a corresponding record can be found in the UtilElements view. Further, the primary key information can be retrieved through the treenode’s utilElement method.

Image isVCSControllableElement You can use the isVCSControllableElement method, shown in the following code example, to determine the granularity of the file-based artifacts that are stored in a version control system. In most cases, the granularity under version control is per root element; in other words, you are working on entire forms, classes, and tables under version control. However, for Microsoft Visual Studio elements, the granularity is different, and you are able to work on individual Visual Studio files—for example, .cs files.

if (treenode.treeNodeType().isVCSControllableElement())
{
    versionControl.checkOut(treenode);
}

The following example shows how to access the type information for a treenode:

static void GetTreeNodeTypeInfo(Args _args)
{
    TreeNode treeNode = TreeNode::findNode(
        @'Data DictionaryTablesCustTableMethodsfind'),
    TreeNodeType treeNodeType = treeNode.treeNodeType();

    info(strFmt("Id: %1", treeNodeType.id()));
    info(strFmt("IsConsumingMemory: %1", treeNodeType.isConsumingMemory()));
    info(strFmt("IsGetNodeInLayerSupported: %1",
        treeNodeType.isGetNodeInLayerSupported()));
    info(strFmt("IsLayerAware: %1", treeNodeType.isLayerAware()));
    info(strFmt("IsModelElement: %1", treeNodeType.isModelElement()));
    info(strFmt("IsRootElement: %1", treeNodeType.isRootElement()));
    info(strFmt("IsUtilElement: %1", treeNodeType.isUtilElement()));
    info(strFmt("IsVCSControllableElement: %1",
        treeNodeType.isVCSControllableElement()));
}


Image Note

You can use the TreeNodeType class to reflect on the meta-model. This class functions on a higher level of abstraction—instead of reflecting on the elements in the AOT, it reflects on element types. The SysModelMetaData class provides another way of reflecting on the meta-model.


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

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