Showing posts with label Reflection. Show all posts
Showing posts with label Reflection. Show all posts

Wednesday, February 26, 2020

get class Name of a static method

in different occasions especially while using the SysExtension or SysOperation frameworks I found myself writing again the same code and I wanted something more generic the get the current class name. I didn’t find any built in method so I decided to make this simple helper method:

public static ClassName getClassNameFromFuncName(str _funcName)
{
    ClassName className;
    #define.ObjectMethodSeparator('.')
    #define.StaticMethodSeparator('::')
    
    if (strContains(_funcName, #ObjectMethodSeparator))
        className = any2str(conPeek(str2con(_funcName, #ObjectMethodSeparator), 1));
    
    if (strContains(_funcName, #StaticMethodSeparator))
        className = any2str(conPeek(str2con(_funcName, #StaticMethodSeparator), 1));
    
    if (!className2Id(className))
        throw(error(Error::wrongUseOfFunction(funcName())));
    
    return className;    
}

you can try it:

public static void main(Args _args)
{
    str fullName = funcName();
    
    str className = MYD_Functions::getClassNameFromFuncName(fullName);

    info(className);
}

here is how I used it to make a constructor more generic and reusable in other SysExtension instances:

class EF_ExtFrameworkSample3bisFactory extends EF_ExtFrameworkSample3bisBaseClass
{
}

public static Object construct(str _className, str _mySetting = '')
{
    EF_StringAtttibute attr;
    Object baseCl;
    ClassName factoryClassName;
    ClassId factoryClassId;
    ClassName baseClassName;
    ClassId baseClassId;
        
    factoryClassName = MYD_Functions::getClassNameFromFuncName(funcName());
    factoryClassId = className2Id(factoryClassName);
    baseClassId = SysDictClass::superClass(factoryClassId);
    baseClassName = classId2Name(baseClassId);

    attr = new EF_StringAtttibute(_className);
    baseCl = SysExtensionAppClassFactory::getClassFromSysAttribute(baseClassName, attr);

    if (!baseCl)
    {
    throw error(Error::wrongUseOfFunction(funcName()));
    }

    baseCl.initSetting(_mySetting);

    return baseCl;

}

Wednesday, September 4, 2019

utility method to get all table fields their Extended data type or Enum type and add them as parm methods in a class programmatically

I had to get the list of fields from a table with quite many fields to be used as properties in a class. quite and error prone task.

for example for CustTable you should have to do:

classDelaration
{
   CustAccount accountNum;
    CustInvoiceAccount invoiceAccount;
    CustGroupId custGroup;
   ….
}

I made this job that using a bit reflection does the work for you:

static void JobEF_TableGetFieldsListForProperties(Args _args)
{
    TableID tableID;
    DictTable dictTable;
    TableName tableName;
    FieldId fieldId;
    DictField dictField, dictFieldFrom;
    Common buffer;
    TableScope scope = TableScope::CurrentTableOnly;
    ExtendedDataTypeName fieldType;
    EnumName fieldEnum;
    FieldName fieldName;
    LabelString fieldTypeLabel, fieldEnumLabel;
    IdentifierName fieldDataType;
    TextBuffer txtb = new TextBuffer();
    int i;

    tableName = tableStr(CustTable);

    tableID = tablename2id(tableName);
    dictTable = new DictTable(tableID);

    if (!dictTable)
        throw error('invalid table');

    buffer = dictTable.makeRecord();

    fieldId   = dictTable.fieldNext(0, scope);

    while (fieldId && ! isSysId(fieldId))
    {
        dictField   = new DictField(tableID, fieldId);

        fieldType = Global::extendedTypeId2name(dictField.typeId());
        fieldEnum = Global::enumId2Name(dictField.enumId());
        fieldName = dictField.name();
        fieldTypeLabel = Global::extendedTypeId2pname(dictField.typeId());
        fieldEnumLabel = Global::enumId2pname(dictField.enumId());

        fieldDataType = fieldType ? fieldType : fieldEnum;

        if (i)
            txtb.appendText('\n');

        txtb.appendText(strFmt('%1 %2;', fieldDataType, MYD_Functions::stringLowerCaseFirst(fieldName)));

        i++;

        fieldId         = dictTable.fieldNext(fieldId, scope);
    }

    info(txtb.getText());
}

I have a helper class where I have all my utility methods, one is the one used in this code:

public static str stringLowerCaseFirst(str string)
{
    System.String myString, firstLetter;
    str firstLetterLwr, stringEnd, stringNew;

    if (strLen(string) < 2)
        return strLwr(string);

    myString = string;

    firstLetter = myString.Substring(0, 1);
    stringEnd = myString.Substring(1);
    firstLetterLwr = firstLetter.ToLowerInvariant();
    stringNew = strFmt('%1%2', firstLetterLwr, stringEnd);

    return stringNew;
    
    /*
    NOTE:
    you can do the same like \Classes\xppSource\parmMethod
    stringNew = strLwr(subStr(string,1,1))+subStr(string,2,strLen(string));
    I don't know which one is best. I come form C#
    */
}

I took it a step further and this other Job also auto generate the parm methods in a class for each table field.

To complete this task I used two Ax Foundation classes:

  • ClassBuild
  • XppSource

ClassBuild contains methods to programmatically create a class and methods

XppSource is the same class which you use behind the scene when you use shorcuts in the code editor or when you select a template under the script button in the editor toolbar.

static void JobEF_ClassMakeDataContractPerTblFields(Args _args)
{
    TableID tableID;
    DictTable dictTable;
    TableName tableName;
    FieldId fieldId;
    DictField dictField, dictFieldFrom;
    Common buffer;
    TableScope scope = TableScope::CurrentTableOnly;
    ExtendedDataTypeName fieldType;
    EnumName fieldEnum;
    FieldName fieldName;
    LabelString fieldTypeLabel, fieldEnumLabel;
    IdentifierName fieldDataType;
    TextBuffer txtb = new TextBuffer();
    int i;
    ClassName className;
    Source source;
    ClassBuild classBuild;
    XppSource xppSource;
    MethodName paramMethodName;
    str parmPrefixToFieldName;
    boolean addDataContractAttribute;

    //SET the table you want to get all fields from
    tableName = tableStr(CustTable);
    //SET the class you want the parm methods to be created
    className = classStr(MYDataContractClass);
    //SET optionally a prefix to all Table field names for the parm
    parmPrefixToFieldName = '';
    //SET if you want to add the [DataContractAttribute] attribute
    addDataContractAttribute = true;
    
    /*
    the hob will run and create a parm for each field of the table in the class.
    From the info log copy the parmeters and paste in the class declaration.
    */

    tableID = tablename2id(tableName);
    dictTable = new DictTable(tableID);


    if (!dictTable)
        throw error('invalid table');

    buffer = dictTable.makeRecord();

    fieldId   = dictTable.fieldNext(0, scope);

    while (fieldId && !isSysId(fieldId))
    {
        dictField   = new DictField(tableID, fieldId);

        fieldType = Global::extendedTypeId2name(dictField.typeId());
        fieldEnum = Global::enumId2Name(dictField.enumId());
        fieldName = strFmt('%1%2', parmPrefixToFieldName, dictField.name());
        fieldTypeLabel = Global::extendedTypeId2pname(dictField.typeId());
        fieldEnumLabel = Global::enumId2pname(dictField.enumId());

        fieldDataType = fieldType ? fieldType : fieldEnum;

        if (i)
            txtb.appendText('\n');

        txtb.appendText(strFmt('%1 %2;', fieldDataType, MYD_Functions::stringLowerCaseFirst(fieldName)));

        i++;

        classBuild = new ClassBuild(className);
        xppSource = new XppSource();
        source = xppSource.parmMethod(fieldDataType, fieldName);

        paramMethodName = strFmt('parm%1%2', parmPrefixToFieldName, strUpr(subStr(fieldName,1,1))+subStr(fieldName,2,strLen(fieldName)));
        
        if (addDataContractAttribute)
            source = strFmt('[DataContractAttribute]\n%1', source);
        
        classBuild.addMethod(paramMethodName, source);

        fieldId = dictTable.fieldNext(fieldId, scope);
    }

    info(txtb.getText());
}

I hope it helps!

Monday, September 2, 2019

Search Projects where an Element Is Used in Dynamics Ax using x++ Reflection

today I need to search in which project an object was customized and ask for the relevant FDD. It is always a good practice to add some notes when you customize code, but not everybody does… So I found in the book Microsoft Dynamics AX 2012 R3 Development Cookbook on pag 333 a good recipe. But unfortunately the object I was looking for was in a deeper node and this recipe did not work.

Let me first bring the original recipe for Searching for an object in a development project from the book

In Dynamics AX, any development changes to the application normally have to be organized
in development projects. The same object could belong to one or more projects, but Dynamics
AX does not provide an easy way to determine which development projects a specific object
belongs to.
In this recipe, we will create a class to search for an object in the development projects.
The class is only for demonstration purposes, but it can be easily converted to a standalone
tool and integrated into the right-click menu.

In the AOT, create a new class with the following code snippet:

class EF_SearchProjectsElementIsUsed
{
}

public static void main(Args _args)
{
    EF_SearchProjectsElementIsUsed::TestDevProjectSearch();
}

static server void TestDevProjectSearch()
{
    EF_SearchProjectsElementIsUsed search;
    search = new EF_SearchProjectsElementIsUsed();
    search.find(UtilElementType::Table, tableStr(CustTable));
}

void find(UtilElementType _type, IdentifierName _name)
{
    TreeNode projects;
    ProjectNode project;

    projects = SysTreeNode::getSharedProject();
    if (!projects)
    {
        return;
    }

    project = projects.AOTfirstChild();

    while (project)
    {
        if (this.findChildren(
                project.loadForInspection(),
                _type,
                _name))
        {
            info(project.AOTname());
        }
        project = project.AOTnextSibling();
    }
}

private boolean findChildren(
            TreeNode _parent,
            UtilElementType _type,
            IdentifierName _name)
{
    TreeNode child;
    TreeNodeIterator iterator;
    #TreeNodeSysNodeType
    iterator = _parent.AOTiterator();
    child = iterator.next();

    while (child)
    {
        if (child.treeNodeType().id() == #NT_PROJECT_GROUP)
        {
            return this.findChildren(child, _type, _name);
        }
        else if (child.AOTname() == _name &&
                        child.treeNodePath() &&
                        child.utilElement().recordType == _type)
        {
            return true;
        }
        child.treeNodeRelease();
        child = iterator.next();
    }
    return false;
}
How it works...

In this recipe, we create a new class with several methods. The first method is
findChildren() and is used for a recursive search operation within the AOT node. It
accepts three parameters: a TreeNode object, an element type, and an element name. In
this method, we go through all the children of the TreeNode object and check whether any
of them match the provided element type and name. If any of the child nodes contain more
nodes within, we use the same findChildren() method to determine whether any of its
children match the element type and name.
The second method is named find() and is used for the actual search, for the given element
type and name. The method goes through all of the shared development projects and calls the
findChildren() method to determine whether the given element is in one of its nodes.
The class can be called from anywhere in the system, but in this recipe, to demonstrate how it
works, we create a new job, define and instantiate the class, and use the find() method to
search for the CustTable table in all the shared projects.

Mine enhanced recipe using a dialog and TreeNodeTraverser to get also child nodes containing the object

I also added a filter to search only in projects belonging to a specific layer. So I can just search to which project in cus layer the object belong. This speeds the search quite significantly.

class EF_SearchProjectsElementIsUsed
{
}
public static void main(Args _args)
{
    Dialog dialog;
    DialogField elementType, layer, elementName;
    UtilElementType type;
    IdentifierName name;
    UtilEntryLevel projectLayer;

    dialog = new Dialog('Search element in projects');

    elementName = dialog.addField(extendedTypeStr(IdentifierName));
    layer = dialog.addField(enumStr(UtilEntryLevel), 'Project Layer');
    elementType = dialog.addField(enumStr(UtilElementType), 'Type');
    dialog.addText("NOTE: setting the type will traverse the node and search in child nodes");

    elementName.control().mandatory(true);

    if (dialog.run() && elementName.value() != '')
    {
        type = elementType.value();
        name = elementName.value();
        projectLayer = layer.value();

        EF_SearchProjectsElementIsUsed::TestDevProjectSearch(type, name, projectLayer);
    }


}
static server void TestDevProjectSearch(UtilElementType _type, IdentifierName _name, UtilEntryLevel _projectLayer)
{
    EF_SearchProjectsElementIsUsed search;
    search = new EF_SearchProjectsElementIsUsed();
    search.find(_type, _name, _projectLayer);
}
void find(UtilElementType _type, IdentifierName _name, UtilEntryLevel _projectLayer)
{
    TreeNode projects;
    ProjectNode project;
    Set objectsInProject;
    SetEnumerator objectsInProjectEnumerator;
    str objectInProjectPath;

    projects = SysTreeNode::getSharedProject();
    if (!projects)
    {
        return;
    }

    project = projects.AOTfirstChild();

    setprefix(strFmt('These projects contain %1', _name));

    while (project)
    {
        /* I just want to search in cus layer, so is faster.
        to search all remove the condition */
        if (project.AOTLayer() != _projectLayer)
        {
            project = project.AOTnextSibling();
            continue;
        }

        objectsInProject = this.findChildren(
                                    project.loadForInspection(),
                                    _type,
                                    _name);

        if (objectsInProject.elements())
        {
            setprefix(project.AOTname());
            objectsInProjectEnumerator = objectsInProject.getEnumerator();

            while (objectsInProjectEnumerator.moveNext())
            {
                objectInProjectPath = objectsInProjectEnumerator.current();
                info(objectInProjectPath);
            }

        }
        project = project.AOTnextSibling();
    }
}
private Set findChildren(
            TreeNode _parent,
            UtilElementType _type,
            IdentifierName _name)
{
    TreeNodeTraverser tnt;
    TreeNode treeNode;
    UtilElements utilElements;
    TreeNodeType nodeType;
    boolean isRoot;
    str treePath;
    boolean nodeTypeIsUtilElement;
    boolean isTreePath;
    boolean showObject;
    Set objectsInProject;

    tnt = new TreeNodeTraverser(_parent);
    objectsInProject = new Set(Types::String);

    while (tnt.next())
    {
        treeNode = tnt.currentNode();
        nodeType = treeNode.treeNodeType();
        showObject = true;

        if (_type)
        {
            showObject = false;
            nodeTypeIsUtilElement = nodeType.isUtilElement();
            isTreePath = treeNode.treeNodePath() ? true : false;
            isRoot = nodeType.isRootElement();

            if (nodeTypeIsUtilElement && isTreePath && isRoot)
            {
                utilElements = treeNode.utilElement();//need the above conditions otherwise treeNode.utilElement() might fail
                if (utilElements.recordType == _type)
                    showObject = true;
            }
            //Debug::printDebug(strFmt('x: %1 - %2', showObject, treeNode.AOTToString()));
        }


        if (treeNode.AOTname() == _name && showObject)
        {
            objectsInProject.add(strFmt('%1 - (is root: %2)', treeNode.AOTToString(), nodeType.isRootElement()));
        }

        treeNode.treeNodeRelease();
    }
    return objectsInProject;
}

this how it would look like without setting the type. (note that it finds every object, tables forms, menu items… and also child objects like form datasources)

image

image

while setting the type it will filter per object type.

image

image