Showing posts with label SysExtension Framework. Show all posts
Showing posts with label SysExtension Framework. 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;

}

Friday, December 6, 2019

SysExtension framework for Dynamics Ax example

The benefits of using this new extension model are that the base and derived classes are decoupled, and it takes less code to extend the capability of the Microsoft Dynamics AX application.

The getClassFromSysAttribute method works by searching through the classes that are derived from the our base class (EF_ExtFrameworkSample6) until it finds a class that has matching attribute

The input value of the attribute class can be anything, an enum, a string, an integer. For this example I used the class name as input.

1) Create the attribute class

As you see I added in the attribute class also a generic static method that can be used by any extension that might use this attribute

class EF_ClassNameAttribute extends SysAttribute
{
    ClassName className;
}
public void new(ClassName _className)
{
    super();
    className = _className;
}
public ClassName parmClassName(ClassName _className = className)
{
    className = _className;

    return className;
}
//this method should be in the factory class, not here.
//I took the liberty to put it here becuse I might want to share it with multiple factory classes.
public static Object getClassFromSysAttribute(ClassName _baseClassName, ClassName _className)
{
    EF_ClassNameAttribute attr;
    Object cl;

    attr = new EF_ClassNameAttribute(_className);
    cl = SysExtensionAppClassFactory::getClassFromSysAttribute(_baseClassName, attr);

    if (!cl)
    {
        throw error(Error::wrongUseOfFunction(_baseClassName));
    }

    return cl;
}



2) Create the base class

the base class does not need to be abstract, but is a good practice

abstract class EF_ExtFrameworkSample6
{
    Name name;
    MethodName functionName;
}
abstract protected void init()
{
}
public void run()
{
    this.init();
    info(strFmt('Hello my name is %1', name));
    info(strFmt('the class that run is %1', functionName));
}


3) Create the Extensions


[EF_ClassNameAttribute(classStr(EF_ExtFrameworkSample6_1))]
class EF_ExtFrameworkSample6_1 extends EF_ExtFrameworkSample6
{
}
protected void init()
{
    name = 'Pippo';
    functionName = funcName();
}
[EF_ClassNameAttribute(classStr(EF_ExtFrameworkSample6_2))]
class EF_ExtFrameworkSample6_2 extends EF_ExtFrameworkSample6
{
}
protected void init()
{
    name = 'Topolino';
    functionName = funcName();
}


4) Create the Factory class


class EF_ExtFrameworkSample6Factory
{
}
protected void new()
{
}
public static EF_ExtFrameworkSample6 newFromClassName(ClassName _className)
{
    return EF_ClassNameAttribute::getClassFromSysAttribute(classStr(EF_ExtFrameworkSample6), _className);
}
public static EF_ExtFrameworkSample6 newFromSample6_1()
{
    return EF_ExtFrameworkSample6Factory::newFromClassName(classStr(EF_ExtFrameworkSample6_1));
}
public static EF_ExtFrameworkSample6 newFromSample6_2()
{
    return EF_ExtFrameworkSample6Factory::newFromClassName(classStr(EF_ExtFrameworkSample6_2));
}
//just for trials...
public static void main(Args _args)
{
    EF_ExtFrameworkSample6 cl;
    cl = EF_ExtFrameworkSample6Factory::newFromSample6_1();
    cl.run();
}

image

One side note, The extension framework cache so every time you do some changes during development clear the cache :

static void JobEF_SysExtensionCache(Args _args)
{
    SysExtensionCache::clearAllScopes();
}

Monday, August 19, 2019

Dynamics Ax SysExtension framework to decouple a display method and avoid if

A simple practical example how the SysExtension framework will rescue us to adhere to SOLID principles and make our code testable:

We have a table that has 4 fields:

clip_image001

1)     GrossValue (Real, extends RealBase)

2)     NetValue (Real, extends RealBase)

3)     Target (Real, extends RealBase)

4)     Type (Enum)

The enum  Type has two elements

clip_image002

Now the requirement is to create a display method, contractValue, that according to the type it does a different calculation:

So you could simply create your method like this:

[SysClientCacheDataMethodAttribute(true)]

public display Total contractValue()

{

    Total value;

 

    if (this.Type == EF_EnumsForSysExtFramworkDemo1::Type1)

        value = this.GrossValue * this.Target;

 

    if (this.Type == EF_EnumsForSysExtFramworkDemo1::Type2)

        value = this.NetValue * this.Target;

 

    return value;

}

 

Can you guess what is the problem with this method?

·        Brilliant!! Yes you are right, too many ‘If’ clauses are there and if we want to introduce another new type, then you need to write another ‘if’. In other words, it violates the Open closed principle (OCP): a class should be open for extension but closed for modification.

·        Second issue this method can’t be properly unit tested!

·        It violates the single responsibility principle

If you work on a small project you can simply ignore these points and claim and that you need a quick simple solution to finish your work quickly and go home early, but if you work on a large enterprise project you as developer have the responsibility to deliver good quality code and Dynamics is by definition an enterprise project. So, let’s see how we can improve this code and make our code SOLID and testable.

So how can you do it?

Use the sys Extension Framework!
there are other ways to achieve it (for example using the strategy pattern https://refactoring.guru/design-patterns/strategy/csharp/example) but I prefer to use ax built in tools as it also caches and is much faster (see this article to know more https://blogs.msdn.microsoft.com/mfp/2014/05/08/x-pedal-to-the-metal/).

Create an attribute class that extends SysAttribute

 

class EF_SysExtFramworkDemo1Attribute extends SysAttribute

{

   EF_EnumsForSysExtFramworkDemo1 types; //the enum for types

}

 

 

public void new(EF_EnumsForSysExtFramworkDemo1 _types)

{

    super();

    types = _types;

}

 

public EF_EnumsForSysExtFramworkDemo1 parmTypes(EF_EnumsForSysExtFramworkDemo1 _types = types)

{

    types = _types;

 

    return types;

}

 

Create an Abstract base class

 

abstract class EF_SysExtFramworkDemo1BaseClass

{

   EF_TableForSysExtFramworkDemo1 tableBuffer;

}

protected void initTableBuffer(EF_TableForSysExtFramworkDemo1 _tableBuffer)

{

    tableBuffer = _tableBuffer;

}

abstract public void getContractValue()

{

}

 

Create the derived classes for the different type options

 

[EF_SysExtFramworkDemo1Attribute(EF_EnumsForSysExtFramworkDemo1::Type1)]

class EF_SysExtFramworkDemo1Type1 extends EF_SysExtFramworkDemo1BaseClass

{

}

public Total getContractValue()

{

    return tableBuffer.GrossValue * tableBuffer.Target;

}

 

[EF_SysExtFramworkDemo1Attribute(EF_EnumsForSysExtFramworkDemo1::Type2)]

class EF_SysExtFramworkDemo1Type2 extends EF_SysExtFramworkDemo1BaseClass

{

}

public Total getContractValue()

{

    return tableBuffer.NetValue * tableBuffer.Target;

}

 

Create a factory class

 

class EF_SysExtFramworkDemo1Factory extends EF_SysExtFramworkDemo1BaseClass

{

}

public Total getContractValue()

{

    return 0;

}

public Total getContractValue()

{

    return 0;//default value

}

public static EF_SysExtFramworkDemo1Factory construct(EF_EnumsForSysExtFramworkDemo1 _type, Common _tableBuffer)

{

   EF_SysExtFramworkDemo1Attribute attr;

   EF_SysExtFramworkDemo1BaseClass baseCl;

    ClassName baseClName = classStr(EF_SysExtFramworkDemo1BaseClass);

 

    attr = new EF_SysExtFramworkDemo1Attribute(_type);

    baseCl = SysExtensionAppClassFactory::getClassFromSysAttribute(baseClName, attr);

 

    if (!baseCl)

    {

        baseCl = new EF_SysExtFramworkDemo1Factory(); //so it will use the default getContractValue in this class

 

        //OR just throw an error

        //throw error(Error::wrongUseOfFunction(funcName()));

    }

 

   baseCl.initTableBuffer(_tableBuffer);

 

    return baseCl;

}

 

Update the display method on the table

 

[SysClientCacheDataMethodAttribute(true)]

public display Total contractValue2()

{

    Total value;

 

   EF_SysExtFramworkDemo1Factory cl;

    cl = EF_SysExtFramworkDemo1Factory::construct(this.Type, this);

 

    value = cl.getContractValue();

 

    return value;

}

 

 

Now we have achieved two important goals

1)     If we add a new type that will require a different calculation, we don’t have to touch anywhere our code (closed for modification). We simply have to create a new derived class (open for extension)

[EF_SysExtFramworkDemo1Attribute(EF_EnumsForSysExtFramworkDemo1::Type3)]

class EF_SysExtFramworkDemo1Type3 extends EF_SysExtFramworkDemo1BaseClass

{

}

public Total getContractValue()

{

    return ….;

}

 

2)     We can now unit test

 

To complete the picture, we can create the Unit test:

class EF_SysExtFramworkDemo1Type1Test extends SysTestCase

{

}

[SysTestCheckInTestAttribute]

public void testType1()

{

    // Arrange

   EF_SysExtFramworkDemo1BaseClass cl;

   EF_TableForSysExtFramworkDemo1 table;

    Total actualTotal, expectedTotal;

 

    // Act

   table.GrossValue = 100;

    table.Target = 2;

   

    expectedTotal = 200;

   

    cl = EF_SysExtFramworkDemo1Factory::construct(EF_EnumsForSysExtFramworkDemo1::Type1, table);

    actualTotal = cl.getContractValue();

 

    // Assert

   this.assertEquals(expectedTotal, actualTotal);

}

[SysTestCheckInTestAttribute]

public void testType2()

{

    // Arrange

   EF_SysExtFramworkDemo1BaseClass cl;

   EF_TableForSysExtFramworkDemo1 table;

    Total actualTotal, expectedTotal;

 

    // Act

    table.NetValue = 100;

    table.Target = 2;

   

    expectedTotal = 200;

   

    cl = EF_SysExtFramworkDemo1Factory::construct(EF_EnumsForSysExtFramworkDemo1::Type2, table);

    actualTotal = cl.getContractValue();

 

    // Assert

   this.assertEquals(expectedTotal, actualTotal);

}

 

To run the test you can just now just right click on the class, AddIns -> Run Tests.

But I usually create also a main method to the test class so that I can just run the class and even add a menu item to run my test

public static void main(Args _args)

{

    ListEnumerator msgs;

    SysTestSuite suite = new SysTestSuite(classstr(EF_SysExtFramworkDemo1Type1Test));

    //use this to run in transaction isolation

    //SysTestSuite suite = new SysTestSuiteTTS(classstr(EF_SysExtFramworkDemo1Type1Test));

 

    SysTestResult result = new SysTestResult();

 

   result.addListener(new SysTestListenerPrint());

   result.addListener(new SysTestListenerDB());

   result.addListener(new SysTestListenerInfolog());

   result.parmCodeCoverageEnabled(true);

 

    suite.run(result);

 

   info(result.getSummary());

 

    msgs = result.getMessages().getEnumerator();

    while (msgs.moveNext())

    {

        error(strFmt('%1', msgs.current()));

    }

}

 

clip_image003

 

One side note, The extension framework cache so every time you do some changes during development cleae the cache :

SysExtensionCache::clearAllScopes();

Project

 

Here is how your project might look like:

clip_image004

 

I know what you think, 6 classes, lot of complexity to replace 4 lines of code!!

Yes, at first glance seems a non-sense, but believe me (actually not me but all the community), writing solid extensible, testable code is the only way to make withstand the changes and the new requirements which always come sooner or later. And the ability to just extend and simply add new classes without touching the existing code is so much powerful that worth all the effort.

Once you get used is not so much a time to write your code properly and you will start to realize that a code that is not backed by unit tests is simply Bad Code.