Tuesday, August 20, 2019

SysOperation framework basic Sync vs Async with or without custom controller

As a first sample we just want to run a method, no parameters

 

Step1, can create start by creating a service class


This is the class where we have the main processing logic is.

The method performs a summation from 1 to 10, but does not really matter what it does, your method will do something much more meaningful. What I just want to simulate is a long process of 5 seconds.

class FCH_SysOpSample2_OpService

{

}

[SysEntryPointAttribute(true)]

private void runSummation()

{

    int i;

    int val;

    for (i = 1 ; i < 10; i++)

    {

        val = val + i;

    }

 

    sleep(5000);//I want simulate a long process...

 

    info(int2str(val));

}

 

Notice the attribute SysEntryPoint which will explicit to the framework that this is the method to execute.

 

Step2, create a menu item Action

 

clip_image002

Notice that

  • Object we set SysOperationServiceController (which is the Operation framework base class for the controller, more in step 3)
  • Parameters we set our service class name and our method separated by a dot.
  • EnumTypeParameters we set SysOperationExecutionMode which is the enum of the operation framework which specifies which execution mode we want to use.

[as a side note: if you are curious how it works you can look in the main method \Classes\SysOperationServiceController\main which is called by the menu item. You will see that it calls this method \Classes\SysOperationServiceController\initializeFromArgs which parses the parameters from the args class]

Opening this menu item will show this dialog

clip_image003

If I click ok it will begin processing, and I can notice that the UI is blocked until it completes processing (we set 5 seconds…) and display the dialog.

If I would select Batch processing the process would be executed as a batch.

 

Step 3, create a controller

 

Now we expand our example by creating a controller so instead of calling from the menu item action the basic class of the controller we extend that same class so that we can control how it should behave.

In this first example we do not need to pass any parameter so we want to hide the dialog (by default a batch always shows a dialog where you can set the parameters and the batch execution).

class FCH_SysOpSample2_Controller extends SysOperationServiceController

{

}

public ClassDescription caption()

{

    return "enrico FCH_SysOpSample2_Controller";//is important to recognize the process in the batch history form

}

void new()

{

    super();

    this.parmClassName(classStr(FCH_SysOpSample2_OpService));

    this.parmMethodName(methodStr(FCH_SysOpSample2_OpService, runSummation));

    this.parmDialogCaption('Basic SysOperation Sample');

    this.parmExecutionMode(SysOperationExecutionMode::ReliableAsynchronous);//I could omit this parm since it is the default

}

public static void main(Args _args)

{

    FCH_SysOpSample2_Controller controller;

    controller = new FCH_SysOpSample2_Controller();

    controller.parmLoadFromSysLastValue(false);

    controller.parmShowDialog(false);

    controller.startOperation();

}

 

What I did here I had to override the new method where I set the service class and method name (what I set in the menu action parameter before) and the execution mode.

I added also a main method to run the class and here I set also that I do not want to see the dialog (in order for hiding the dialog the I also have to set parmLoadFromSysLastValue as false)

I could have also used the run() method instead of startOperation() but is not recommended.

If I run this class I will see that the UI is not blocked. When it finishes it will show the info box with the result.

It does not create a new batch, but it is still available in the batch history

System administration/inquiries/batch job jistory

 

clip_image005

 

One more note. Sometimes as noted in this post http://www.axdeveloperconnection.it/webapp/blog/reliableasynchronous-not-so-reliable-heres-why

The reliable asynchronous is not so reliable… works fine no worries, but the info log might not show up. To know more read that post. Here I just add the code you have to add to the controller class: override the afterOperation method like this:

protected void afterOperation(SysOperationExecutionMode _executionMode, AifAsyncResult _asyncResult)

{

    Batch batch;

    int pollCount;

    BatchHistory batchHistory;

    #SysOperation

 

    if (_executionMode != SysOperationExecutionMode::Synchronous)

    {

        progressWait.endWaiting();

    }

 

    if (_asyncResult.parmHasException())

    {

        error(strFmt("@SYS317009", this.caption(), _asyncResult.parmExceptionType()));

        return;

    }

 

    if (_executionMode == SysOperationExecutionMode::ReliableAsynchronous)

    {

        batch = this.operationReturnValue();

        if (batch)

        {

            if (batch.Info == conNull())//if infolog is not availabe

            {

                while (batch.Info == conNull())//while is not available

                {

                    pollCount++;

                    if (pollCount >= #BatchTableMaxPollCount)

                        return; //we have reached the max, so exit

                    sleep(#BatchTablePollingInterval);

                    select batchHistory where batchHistory.BatchId == batch.RecId;

                    batch.Info = BatchHistory::showLog(batchHistory.RecId);

                }

                infolog.view(batch.Info);

            }

            else

            {

                infolog.view(Batch::showLog(batch.RecId));

            }

        }

    }

}

 

here the full code for the controller class:

class FCH_SysOpSample2_Controller extends SysOperationServiceController
{
}
void new()
{
    super();
    this.parmClassName(classStr(FCH_SysOpSample2_OpService));
    this.parmMethodName(methodStr(FCH_SysOpSample2_OpService, runSummation));
    this.parmDialogCaption('Basic SysOperation Sample');
    this.parmExecutionMode(SysOperationExecutionMode::ReliableAsynchronous);//I could omit this parm since it is the default
}
public ClassDescription caption()
{
    return "enrico FCH_SysOpSample2_Controller";//is important to recognize the process in the batch history form
}
public static void main(Args _args)
{
    FCH_SysOpSample2_Controller controller;
    controller = new FCH_SysOpSample2_Controller();
    controller.parmLoadFromSysLastValue(false);
    controller.parmShowDialog(false);
    controller.startOperation();
}
protected void afterOperation(SysOperationExecutionMode _executionMode, AifAsyncResult _asyncResult)
{
    Batch batch;
    int pollCount;
    BatchHistory batchHistory;
    #SysOperation

    if (_executionMode != SysOperationExecutionMode::Synchronous)
    {
        progressWait.endWaiting();
    }

    if (_asyncResult.parmHasException())
    {
        error(strFmt("@SYS317009", this.caption(), _asyncResult.parmExceptionType()));
        return;
    }

    if (_executionMode == SysOperationExecutionMode::ReliableAsynchronous)
    {
        batch = this.operationReturnValue();
        if (batch)
        {
            if (batch.Info == conNull())//if infolog is not availabe
            {
                while (batch.Info == conNull())//while is not available
                {
                    pollCount++;
                    if (pollCount >= #BatchTableMaxPollCount)
                        return; //we have reached the max, so exit
                    sleep(#BatchTablePollingInterval);
                    select batchHistory where batchHistory.BatchId == batch.RecId;
                    batch.Info = BatchHistory::showLog(batchHistory.RecId);
                }
                infolog.view(batch.Info);
            }
            else
            {
                infolog.view(Batch::showLog(batch.RecId));
            }
        }
    }
}


 

 

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.