Thursday, May 24, 2018

RunBase to SysOperation

A wonderful article written on SysOperation and RunBase by 

From RunBase to SysOperation: Business Operation Framework

There's been a lot of talk about the Business Operation Framework (or SysOperation), how it is the next iteration and future replacement of the RunBase and RunBaseBatch framework. I say "future replacement" since there is plenty of RunBase to go around in the standard application. But we'd all be fools not to take advantage of this new framework in AX. There is a bit of information to be found about this new framework online already. Rather than just a straightforward walkthrough, I will make the comparison between RunBase and SysOperation and you will see making the transition is not that difficult.

The SysOperation framework is basically an implementation of the MVC pattern. The isolation of the parameters (model), the dialog (view) and the code that runs (controller) is the essence of the pattern. If you think about RunBase and these three elements, you will realize how they are intertwined within the RunBase framework:

Model:
• Member variables
• Pack/unpack (aka serialization)

View:
• Dialog()
• GetFromDialog()
• PutToDialog()

Controller:
• Prompt()
• Run()

The framework leverages the services framework for this.
 A service in AX has a data contract, this is basically an AX class with special attributes. The class specifies accessor methods ("parm" methods in AX-speak), again decorated with attributes, which let you set and get member variables. This specifies the data that can go into your process (aka "operation"), and can also specify a return value for it.

The operation itself is just a method that takes a data contract, and returns a data contract. This basically classifies it as a custom service. And indeed, if you use this framework, your operation can in fact be used as a service over AIF. 

You can see the SOA emerge, a hint of where AX is going. Anyway, distractions aside, we now have a model, we have a process, now we need a controller and a view… Easily enough, the view is automatically created based on your data contract (although you can create your own). The controller is simply an inherited class that doesn't even need to do much.
So, what I decided to do is show you a RunBaseBatch class, and then help you convert the class into a SysOperation class. This class lets you input a title and two integer numbers. The run() method will use the title as an infolog prefix, then output the numbers, and the sum of the two numbers. Exciting stuff indeed. So let's dissect this class.

The inputs of this class are the title and the two numbers. My ClassDeclaration of this class looks as follows:



Also note the DialogFields in here (the "View"), and the macros #CurrentList and #CurrentVersion which are used in the pack() and unpack() methods, basically the "serialization" of the input data.

To convert this into a BOF pattern, we will create a Data Contract with our three arguments.



For each member variable (since in AX they are always private), we will create an accessor method. Again here we need to decorate with attributes.





So, we have now created our input Data Contract. Next, we want to create the operation, which is basically the run() method of the RunBase.

Compared to RunBase, the operation will not have the member variables for our inputs, but rather it will take the data contract as an argument.

Below shows the original run() method, and the new operation method. We'll also make the operation class RunOn = Server. Other than that, it's a regular class with a method, no special inheritance, interface implementation, or anything.





Ok, so we now have the input Data Contract, we have the operation that takes the data contract and runs the functionality.

 How about the dialog? Well, that will get auto-created by the controller. So, how about that controller? Another class!



Ok, here's where it gets interesting. Technically, we wouldn't even need our own controller class.

The base controller class which we're extending here (SysOperationServiceController) has some extra logic in its base method, that reads parameters from the Args argument. This will allow you to just create your own menu item (see MSDN article on BOF usage), point it to the class.

 The parameters field needs to be set to your class and method name. In our case it would be DAXMusingsService.CoolOperation (notice the period in between the class and method), the enumtype to SysOperationExecutionMode and then pick your enum value, default is Synchronous (more about this later) - see an example here. So why am I telling you all this when we were creating our own class?
Well, the SysOperationServiceController's constructor (the "New" method in AX) takes the service class name, operation method name and the SysOperationExecutionMode enum as arguments. Since we specifically created our controller class to run the specific operation, we can "hardcode" the class and method name (using best practices of course: pre-compiler methods classStr and methodStr). To accomplish this, let's just create a static construct method. We'll still leave the option of passing in an execution mode so you can do some testing on your own later.



Ok, so we created a handy static construct method. Are we there yet? Yup, pretty much, all we need is a static Main method to execute our controller class.



One more thing left to do. Since services, including BOF services, run X++ in the CLR, we need to generate CIL. Rather than doing a full compile (which you should have done when you installed AX!), we can just generate incremental CIL.



Alright, ready to rock and roll. Run the class!




Cool, that worked! So, no thanks to the length of this article so far, that wasn't bad at all, was it?

Of course, the AX-dialog aficionado in you is already complaining about the auto-generated dialog . For one, in this example the two integers with the same data type have the same label. Secondly, RunBase(Batch) classes need to deal with queries, so how does that work?

Source :  https://daxmusings.codecrib.com/2011/08/from-runbase-to-sysoperation-business.html


We walked through a full example of taking a basic RunBase class and turning into a Business Operation Framework MVC pattern. The example turned out fairly straightforward once you get the concepts down. The two things we were missing though, was the ability to add a query to the Operation, as well as manipulating the auto-generated dialog of the BOF, which is based on the Data Contract and its data member data types. This article builds further on that code, so if you haven't read it yet, please check the previous articlefirst. 

Let's start with the query. Since all inputs that are being passed down to your operation are on the data contract, we can safely assume the query will need to go onto the data contract as well. Now, the data contract has accessor methods for every member you wish to expose, and those members are base AX types (string, int, real, etc). So how will we pass a query? Well, traditionally queries are "packed" (aka serialized) into a container. Containers however are not part of the CLR and so we need a more basic type to serialized our query to. So, BOF basically serializes (encodes) the query to a string. Let's look at the easy way to do first, which is basically all automatic. 

Assuming we have a query in the AOT called "CustBaseData" (this query is available in standard AX for your convenience). All we need to do add that query to our Data Contract is add a variable in the classDeclaration for the encoded query string, and create a data member on the data contract class of type string, and add a special attribute for the query (on top of the datamember attribute we have to add anyway): 




The AifQueryTypeAttribute specifies the name of the argument variable that the encoded query will go into, and you specify what query in the AOT it is based on (CustBaseData in this case). This is already enough to make the query show up on your dialog, as it would with RunBase (a dialog with select button, etc). 
Now of course the question is, how do we retrieve (decode) this encoded query string in our operation "CoolOperation"? Well, basically we're not encoding the query itself necessarily, we're encoding and decoding a container (=packed query). 
For this, AX has two methods available: 

SysOperationHelper::base64Encode() 
SysOperationHelper::base64Decode() 

So, to retrieve the packed query, we just need to decode the parmQuery() string of the data contract to a container, then unpack that container to a query... So, let's add some code to our previous CoolOperation method: 



Here we decode the parmQuery() string to container and immediately pass that to the new Query() method to unpack that there. Further down, we create a new QueryRun object base on that query and just iterate the result set and output the customer account number. Before we can run this, we need to run the Incremental CIL Generation! Then, go ahead and try it! To limit the amount of output, I basically clicked the "Select" button and selected the first customer account as a filter. 




Ok, so that worked beautifully! Now you know how to add a query to your Business Operation. So how about customizing the dialog? In a RunBase class, we can add groups to the dialog, and order dialog controls by changing the order of adding of fields to the dialog. This can also be achieved with BOF, by adding some attributes to the data contract. 
So, the original dialog showed the two integers and then the title. Let's add a group for the numbers, and a group for the title. And let's sort it so the title group is comes before the numbers. All this information is added as metadata on the contract. First, you "declare" the group in the classDeclaration of the data contract: 



The attribute takes a name for the group (control), a label (I used a hard coded string here, but you should use a label such as @SYS9999), and a third parameter specifying the order. Now, for each data member, we can say what group it belongs to, and give it a sorting number within the group: 


LastNumber 
Title 

The order in which you add the attributes doesn't matter. The query doesn't take a group since it's on the side with the filter fields and the select button (ok, I had to try this to see what would happen... nothing. Adding a group attribute on the query doesn't do anything. You read it here first!). 
Anyway, now we have groups, but our two integer fields are still called "Integer" and "Integer". So how do we set label and helptext like we used to do on RunBase dialog fields? Well, more attributes! (Again, please use labels in real life code!) 


Last Number 

Here's what the dialog now looks like: 



Admittedly, I haven't figured out how to get rid of the "Parameters" top-level group. I'll be debugging this some time to figure out if there's an easy way to get rid of it. So anyway, this is all great. But is there *ANY* way to build a dialog from scratch, like, the old-fashioned way using dialog.addField() or something? 
Well yes ladies and gentlemen, there is. You can create your own "UIBuilder" class by extending the SysOperationUIBuilder class. By default, the SysOperationServiceController class uses the SysOperationAutomaticUIBuilder class, which examines your data contract(s), the attributes used (for groups etc), and builds the dialog from that automatically. But, you can create your own builder. To make the BOF use your UI builder, you guessed it, we can attach the builder to your data contract using... an attribute: 

SysOperationContractProcessingAttribute(classStr(YOURUIBuilderClassName)) 

Unfortunately, again, the post is running a little long, so I'll owe you an example of this some time. Feel free to test out the UI builder class, if you search the AOT for the SysOperationContractProcessingAttribute string in the classes, you will find some examples of this in standard AX. Happy coding!

Monday, May 21, 2018

How to write a generate method to map the default dimension

[DMFTargetTransformationAttribute(true),DMFTargetTransformationDescAttribute("@DMF1365"),
DMFTargetTransformationSequenceAttribute(24)
,DMFTargetTransFieldListAttribute([fieldStr(DMFLedgerJournalEntity,CICDepartments),fieldStr(DMFLedgerJournalEntity,CICProfitCenter),fieldStr

(DMFLedgerJournalEntity,CICDivisions)])
]
public container CICgenerateDimension(boolean _stagingToTarget = true)
{
    container                       res,varContainer;
    Counter                         varCounter=0;
    RecId                           retValueOfDefDimension;


    if (_stagingToTarget)
    {
        varContainer = [0];

        //assigning one dimension
        if(entity.CICDepartments)
        {
            varContainer += ['Department',entity.CICDepartments];
            varCounter++;//increase on ssignmnet of each dimension
        }

        //assigning 2nd dimension
        if(entity.CICProfitCenter)
        {
            varContainer += ['ProfitCenter',entity.CICProfitCenter];
            varCounter++;//increase on assignmnet of each dimension
        }

        if(entity.CICDivisions)
        {
           //assigning 3rd dimension
            varContainer += ['Divisions',entity.CICDivisions];
            varCounter++;//increase on assignmnet of each dimension
        }
        //so on.. you can pass any number of dimensions

        //prepare container
        varContainer = conPoke(varContainer,1,varCounter);

        //pass continer and get back value generated
        retValueOfDefDimension = AxdDimensionUtil::getDimensionAttributeValueSetId(varContainer);

        //Return the default dimension
        if(retValueOfDefDimension)
            res=[retValueOfDefDimension];         

    }
    return res;
}


-----------------------------------------------------------------------------------------------------------------
//assign the return value to the target table field in getReturnFields method


        case methodStr(DMFLedgerBalanceEntityClass, CICgenerateDimension) :
        con += [fieldstrToTargetXML(fieldStr(LedgerJournalTrans, DefaultDimension))];
        break;

How to generate the default dimension with the combination of dimension values using X++

Method 1 :-
static void DefaultDimension(Args _args)
{
DimensionAttributeValueSetStorage valueSetStorage = new DimensionAttributeValueSetStorage();
DimensionDefault result;

int i;
DimensionAttribute dimensionAttribute;
DimensionAttributeValue dimensionAttributeValue;
container conAttr = ['Department','ProfitCenter' ,'Divisions'];
container conValue = ['CE', 'HO', 'GL'];
str dimValue;

for (i = 1; i <= conLen(conAttr); i++)
{
dimensionAttribute = dimensionAttribute::findByName(conPeek(conAttr,i));

if (dimensionAttribute.RecId == 0)
{
continue;
}

dimValue = conPeek(conValue,i);

if (dimValue != "")
{
// The last parameter is "true". A dimensionAttributeValue record will be created if not found.
dimensionAttributeValue =
dimensionAttributeValue::findByDimensionAttributeAndValue(dimensionAttribute,dimValue,false,true);

// Add the dimensionAttibuteValue to the default dimension
valueSetStorage.addItem(dimensionAttributeValue);
info("%1",dimensionAttributeValue);
}
}

result = valueSetStorage.save();
info("%1",result);
}
--------------------------------------------------------------------------------------------------------------------------

Method 2

 static void DefaultDimension(Args _args)
 {

    container   varContainer;
    Counter     varCounter=0;
    RecId       retValueOfDefDimension;
    ;
    
    varContainer = [0];
    
    //assigning one dimension
    varContainer += ["Department","025"];
    varCounter++;//increase on assignmnet of each dimension
    
    //assigning 2nd dimension
    varContainer += ["CostCenter","010"];
    varCounter++;//increase on assignmnet of each dimension
    
    //so on.. you can pass any number of dimensions
    
    //prepare container
    varContainer = conPoke(varContainer,1,varCounter);
    
    //pass continer and get back value generated
    retValueOfDefDimension = AxdDimensionUtil::getDimensionAttributeValueSetId(varContainer);
    
    //display just for demo puprose. in real practice simply assign it to table field(s)
    if(retValueOfDefDimension)
        info(strFmt('You can verify record with RecId %1 in Table DimensionAttributeValueSetItem by filtering on field DimensionAttributeValueSet.',retValueOfDefDimension));    
}

How to enable the dimension fields based on the Item selected on the form.

[Form] public class KMTShipFromWarehouses extends FormRun {     InventDimCtrl_Frm_EditDimensions        inventDimFormSetup;     /// ...