18 December 2019

From date and toDate query ranges in Report and form X++ code in AX 2012

Below logic can be used for filtering using from date and to date.

dateRange.value(SysQuery::range(FromDate.dateValue(), ToDate.dateValue()));

Form level in the execute query can write the below logic.

QueryBuildRange dateRange = SysQuery::findOrCreateRange(this.queryBuildDataSource(), fieldNum(MyTable, MyDateField));

26 November 2019

SysOperation frame work in D365 F&O example with batch job

class TRGBatchService extends SysOperationServiceBase { /// <summary> /// /// </summary> /// //[SysEntryPoint(false)] private void processCode() { Info("Test"); } }

class TRGBatchController extends sysOperationServiceController
{
  public static void main(Args args)
    {
        TRGBatchController controller = new TRGBatchController(classStr(TRGBatchService),methodStr(TRGBatchService,processCode),SysOperationExecutionMode::Synchronous);
        controller.startOperation();
    }

    /// <summary>
    ///
    /// </summary>
    /// <returns></returns>
    public ClassDescription caption()
    {
        ClassDescription ret;
    
        ret ="Operation batch job";
    
        return ret;
    }


}

14 November 2019

Sequence of methods in the FORM level in AX / Form opening sequences in AX 2012 D365


Sequence of methods in the FORM level in AX / Form opening sequences in AX 2012 D365

Sequence of Methods calls while opening the Form
Form --- init ()
Form --- Datasource --- init ()
Form --- run ()
Form --- Datasource --- execute Query ()
Form --- Datasource --- active ()


Sequence of Method calls while saving the record in the Form
Form --- Datasource --- ValidateWrite ()
Table --- ValidateWrite ()
Form --- Datasource --- write ()
Table --- insert ()

Sequence of Methods calls while creating the record in the Form
Form --- Datasource --- create ()
Form --- Datasource --- initValue ()
Table --- initValue ()
Form --- Datasource --- active ()

Sequence of Methods calls while closing the Form
Form --- canClose ()
Form --- close ()


Sequence of Methods calls while modifying the fields in the Form
Table --- validateField ()
Table --- modifiedField ()

Sequence of Method calls while deleting the record in the Form
Form --- Datasource --- validatedelete ()
Table --- validatedelete ()
Table --- delete ()
Form --- Datasource --- active ()



17 October 2019

BOM Explode X++ logic in AX 2012 D365 FO

class BOMExplode
{
    date                        _date = mkDate(22,09,2019);
    void itemExplode(ItemId _ItemId, int _level = 0, BOMQty _bomQty = 1)
    {
        BOM                         bomTable;
        BOMVersion                  bomVersion;
        boolean                     enable;
        InventTestVariableId        cvQualityGroupId;
        Level                       level = _level;
       
        while select bomVersion
            where bomVersion.ItemId == _itemid
                && bomVersion.Active
                && bomVersion.FromDate <= _date//DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone())
                && (!bomVersion.ToDate || bomVersion.ToDate >= _date)//DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone()))
        {
            if (bomVersion.RecId)
            {
                While select bomTable
                    where bomTable.BOMId == bomVersion.BOMId
                {
                    Info(strFmt("Parent Item: %1 Part Number: %2 Quantity: %3, Level: %4 ",bomVersion.ItemId,bomTable.ItemId, bomTable.bomQty,level));
                    if (this.hasChild(bomTable.ItemId))
                    {
                        this.itemExplode(bomTable.ItemId, level + 1, bomTable.BOMQty);
                    }
                }
            }
        }
    }

    boolean hasChild(ItemId _itemId)
    {
        BOMVersion  bomVersion;
        boolean     ret = false;

        select firstonly bomVersion
            where bomVersion.ItemId == _itemid
                && bomVersion.Active
                && bomVersion.FromDate <= _date//DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone())
        && (!bomVersion.ToDate || bomVersion.ToDate >= _date);//DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone()));

        if (bomVersion.RecId)
        {
            ret = true ;
        }

        return ret;
    }

    public static void Main(Args args)
    {
        BOMExplode bomExplode = new BOMExplode();

        bomExplode.itemExplode("A20HBA06B7");

    }

}

04 September 2019

How to get AX applciation preiod start date and end date in D365 FO or ax7

PeriodStart             periodStartDate;
PeriodEnd               periodEndDate;

periodStartDate = FiscalCalendarYear::findYearByCalendarDate(Ledger::fiscalCalendar(), DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone())).StartDate;
        periodEndDate   = FiscalCalendarYear::findYearByCalendarDate(Ledger::fiscalCalendar(), DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone())).EndDate;

09 August 2019

How to explode BOM through X++ BOM Explode and subBOM X++ BOM versions and BOM

class BOMDesign
{
TmpBOMExplode tmpBOM;
}
private int InsertParentItem(ItemId _itemId, int _level)
{
InventTable inventTable;
;

//Gets the parent information and then insert on TmpBOMExplode Table with level 0.
select firstOnly inventTable where inventTable.ItemId == _itemId;

tmpBOM.ItemId = _itemId;
tmpBOM.ItemName = inventTable.itemName();
tmpBOM.Level = _level;
tmpBOM.BOMQty = 1 ;
tmpBOM.insert();

return _level+ 1 ;
}

boolean hasChild(ItemId _itemId)
{
BOMVersion bomVersion;
;

//Check if the item is also a BOM item.
select firstonly bomVersion
where bomVersion.ItemId == _itemid
&& bomVersion.Active
&& bomVersion.FromDate <= systemdateget () && (!bomVersion.ToDate || bomVersion.ToDate >= systemdateget ());

if (bomVersion.RecId)
return true ;

return false ;
}


void itemExplode(ItemId _ItemId, int _level = 0, BOMQty _bomQty = 1)
{
BOM bomTable;
InventTable inventTable;
BOMVersion bomVersion;
InventDim   inventDim;
;

//Insert parent Item
if (_level == 0)
_level = this.InsertParentItem(_ItemId, _level);

//Verifies if the Item exists in BOMVersion Table.
//The item must be active and not expired.
while select firstonly bomVersion
where bomVersion.ItemId == _itemid
&& bomVersion.Active
&& bomVersion.FromDate <= systemdateget() && (!bomVersion.ToDate || bomVersion.ToDate >= systemdateget());
// optoin comparing with Dimensions//
/*
exists join inventDim
            where inventDim.inventDimId == bomVersion.InventDimId &&
                  (inventDim.InventColorId == inventColorId || inventColorId == "") &&
                  (inventDim.InventSizeId == inventSizeId || inventSizeId == "");
*/
{
if (bomVersion.RecId)
{
//Every item on BOMVersion has a BOMId which is used to show
//which products belong to a BOM Item.
While select bomTable
where bomTable.BOMId == bomVersion.BOMId
join inventTable
where bomTable.ItemId == inventTable.ItemId
{
//Insert the items that compose the BOM Item with level 1.
tmpBOM.ItemId = bomTable.ItemId;
tmpBOM.ItemName = inventTable.itemName();
tmpBOM.RefItemId = bomVersion.ItemId;
tmpBOM.BOMQty = bomTable.BOMQty / bomTable.BOMQtySerie * _bomQty;
tmpBOM.Level = _level;
tmpBOM.insert();

//This method is used to check if the BOM Item is composed by
//another BOM Item, case true it will call the method recursively
// with level 2.
if (this.hasChild(bomTable.ItemId))
this.itemExplode(bomTable.ItemId, _level+ 1, tmpBOM.BOMQty);
}
}
}
public static void main(Args args)
{
BOMDesign bomDesign = new BOMDesign();
;

bomDesign.itemExplode( 'D0001', 0 );
}
}

For more info please click here for reference

26 July 2019

Display inventory dimensions dynamically in D365 / Add display inventory dimensions in form

Display inventory dimensions dynamically in D365

Please click here for link

and use the below code to 

InventDimCtrl_Frm_EditDimensions    inventDimFormSetup;

 /// <summary>
    /// Inventory dimension group method.
    /// </summary>
    /// <returns>
    /// A <c>InventDimCtrl_Frm_EditDimensions</c> object.
    /// </returns>
    InventDimCtrl_Frm_EditDimensions inventDimSetupObject()
    {
        return inventDimFormSetup;
    }

 /// <summary>
    /// Initializes the range and inventory dimensions to be display while opening the form
    /// </summary>
    public void init()
    {
        super();
        // Add a filter to the query. Specify the field to use in the filter.
       
        //This method will be used to show default fields at the form setup
        element.updateDesign(InventDimFormDesignUpdate::Init);
    }
/// <summary>
    /// Update design mode as per inventory dimension
    /// </summary>
    /// <param name="_mode">
    /// Inventory dimension design mode.
    /// </param>
    void updateDesign(InventDimFormDesignUpdate _mode)
    {
        switch (_mode)
        {
            // Form Init
            case InventDimFormDesignUpdate::Init    :
                if (!inventDimFormSetup)
                {
                    inventDimFormSetup  = InventDimCtrl_Frm_EditDimensions::newFromForm(element);
                    inventDimFormSetup.parmSkipOnHandLookUp( true);
                }

            // Datasource Active
            case InventDimFormDesignUpdate::Active  :
                inventDimFormSetup.formActiveSetup(InventDimGroupSetup::newItemId(DataSourceTable.ItemId)); //InventDimDisplay is the datasource name.
                inventDimFormSetup.formSetControls( true);
                break;

            // Datasource Field change
            case InventDimFormDesignUpdate::FieldChange :
                inventDimFormSetup.formActiveSetup(InventDimGroupSetup::newItemId(DataSourceTable.ItemId)); //InventDimDisplay is the datasource name.
                InventDim.clearNotSelectedDim(inventDimFormSetup.parmDimParmEnabled()); // InventDim is referring to datasource name
                inventDimFormSetup.formSetControls( true);
                break;

            default :
                throw error(strFmt ("@SYS54195", funcName()));

        }
    }
Add the below code in the DS active method.
int  active()
        {
            int ret;

            ret = super();

            element.updateDesign(InventDimFormDesignUpdate::Active);

            return ret;

        }


24 July 2019

How to populate form data source with temp table. D365 FO and AX7

Create new form, add temptable as DataSource.

DS-> Init()

public void init() // init method of form data source. 
{
   CustGroupTmp tmpTable; 
   super();
   tmpTable.CustGroupId= "Test1";
   tmpTable.insert();
   tmpTable.CustGroupId= "Test2";
   tmpTable.insert(); 
   CustGroupTmp.linkPhysicalTableInstance(tmpTable); // CustGroupTmp is the form datasource name this cursor

//or 
CustGroupTmp.setTmpData(tmpTable);
TableType should be: InMemory
}

work in class inserting temptable records.

// to use the same logic in the class, pass the formDatasource-CustGroupTmp to class
if (_args.record().TableId == tableNum(CustGroupTmp))
{
 FormDataSource formDS = _args.caller().datasource(formDataSourceStr(CustGroupTmpForm, CustGroupTmp)) as FormDataSource;
 CustGroupTmp = formDS.cursor();
 formDS.executeQuery();
}

// FormDataSource fds
// CustGroupTmp custGroupTempBuffer = fds.cursor()
// custGroupTempBuffer.linkPhysicalTableInstance(tmpTable);

18 July 2019

X++ code to write data to Notepad or CSV file in D365FO or AX7

TextStreamIo                        streamIO;
FileName                            fileNameWithExt;
    FileName                            fileName;

 public void initFile()
    {
        #define.validationFileSuffix('-data-validation-errors.txt')
        InteropPermission perm = new InteropPermission(InteropKind::ClrInterop);
       
        perm.assert();
        streamIO = TextStreamIO::constructForWrite(#utf8format);
        streamIO.outRecordDelimiter(#delimiterCRLF);

        fileName += #validationFileSuffix;
    }

boolean validationFailed(FreeTxt _errorTxt)
    {
        boolean ret;

        if(!streamIO)
        {
            this.initFile();
        }
           
        streamIO.write(_errorTxt);

        return ret;
    }


 public void downloadFile()
    {
        if (streamIo != null)
        {
            File::SendFileToUser(streamIo.getStream(), fileName);
        }

        CodeAccessPermission::revertAssert();
    }

10 July 2019

How to calculate Available physical of an item given the inventLocationId that shown on the on hand form


How to get available physical for an Item and for Warehouse in AX 2012 / D365 FO 
InventDimParm       invDimParm;
   InventDim           invDim;
   Qty                 availPhys;

   invDim.InventLocationId = 'INVENTLOCATIONID';
   invDim = InventDim::findOrCreate(invDim);
   invDimParm.initFromInventDim(InventDim::find(invDim.inventDimId));
   availPhys = InventSum::findSum("ITEMID",invDim,invDimParm).availPhysical();
   info(strfmt("availPhys:%1 ",availPhys));
========
InventDim           inventDim;
   InventDimParm       inventDimParm;
   InventOnhand        inventOnhand;
  
   inventDim.InventLocationId = 'Yourwarehouse';
   inventDimParm.initFromInventDim(inventDim);
   inventOnhand = InventOnhand::newParameters('Youritem', inventDim, inventDimParm);
   info(strfmt("Available Physical: %1", inventOnhand.availPhysical()));

08 July 2019

Credit limit check on Sales Quotation based on the customer credit limit in D365 FO of AX7


class TRG_SalesQuotationTableWriteEventHandler
{
    /// <summary>
    ///
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormDataSourceEventHandler(formDataSourceStr(SalesQuotationTable, SalesQuotationLine), FormDataSourceEventType::Written)]
    public static void SalesQuotationLine_OnWriting(FormDataSource sender, FormDataSourceEventArgs e)
    {
        SalesQuotationTable salesQuotationTable;
        SalesQuotationLine  salesQuotationLine,salesQuotationLineLoc;
        CustTransOpenSum    custTransOpenSum;

        salesQuotationLineLoc = sender.cursor();
        salesQuotationTable = salesQuotationTable::find(salesQuotationLineLoc.QuotationId);

        if(smmParametersTable::find().CheckCreditLimitOnQuotation == NoYes::Yes)
        {
            Amount creditLimit  = CustTable::find(salesQuotationLineLoc.CustAccount).CreditMax;
            select sum(LineAmount) from salesQuotationLine
                group by QuotationId
                where salesQuotationLine.QuotationId == salesQuotationTable.QuotationId;
           
            select AccountNum,AmountMST from custTransOpenSum
                where custTransOpenSum.AccountNum == salesQuotationTable.CustAccount;

            Amount newBalance = custTransOpenSum.AmountMST + salesQuotationLine.LineAmount;
            Amount creditExcess = newBalance - creditLimit;
            if(newBalance  > creditLimit)
            {
                warning(strFmt("Quotation:%1 Creit limit exedded Opening balance %2 Current order:%3 New balance:%4 Credit limit:%5 Credit excess:%6 ",
                    salesQuotationLineLoc.QuotationId,
                    custTransOpenSum.AmountMST,
                    salesQuotationLine.LineAmount,
                    newBalance,
                    creditLimit,
                    creditExcess));
            }
        }
    }

}

25 June 2019

Admin provisioning tool can't stop DynamicsAXBatch service

Admin provisioning tool can't stop DynamicsAXBatch service 
System.ComponentModel.Win32Exception: The service cannot accept control messages at this time in D365

  • Click the Start menu
  • Click Run or in the search bar type services.msc
  • Press Enter
  • Look for the service and check the Properties and identify its service name. Service name would be DynamicsAxBatch
  • Once found, open a command prompt. Type: sc queryex [servicename].
  • Press Enter
  • Identify the PID
  • In the same command prompt type: taskkill /f /pid [pid number]
  • Press Enter
  • Now start the service and and register yourself in the Admin Provisioning Tool

21 June 2019

How to access form data source using Form control in the event handler in D365 or AX7

How to access form record using Form control in the event handler in D365 or AX7

[FormControlEventHandler(formControlStr(InventJournalTransfer, Functions), FormControlEventType::Clicked)]

public static void Functions_OnClicked(FormControl sender, FormControlEventArgs e)
    {
        InventJournalTrans      inventJournalTrans;
        InventJournalTable      journalTable;

        FormRun form = sender.formRun();
        FormDataSource journalTable_ds  =       form.dataSource(
        formDataSourceStr(InventJournalTransfer,InventJournalTable)) as FormDataSource;
        journalTable                   = journalTable_ds.cursor();

    }

24 May 2019

How to import Azure bacpac file to Local SQL DB / On premises SQL DB in D365 environment

cd C:\Program Files (x86)\Microsoft SQL Server\140\DAC\bin

SqlPackage.exe /a:import /sf:C:\Users\Adminc4221d5a84\Documents\Backup\AXDBUATCopy07Oct.bacpac /tsn:localhost /tdn:AXDB_UATCOPY07Oct18 /p:CommandTimeout=1200

--or
-- To import to on-premises need to import data using this below link.https://bitwizards.com/thought-leadership/blog/2013/january-2013/restore-sql-azure-database-to-sql-server-2012


CREATE USER axdeployuser FROM LOGIN axdeployuser
EXEC sp_addrolemember 'db_owner', 'axdeployuser'

CREATE USER axdbadmin FROM LOGIN axdbadmin
EXEC sp_addrolemember 'db_owner', 'axdbadmin'

CREATE USER axmrruntimeuser FROM LOGIN axmrruntimeuser
EXEC sp_addrolemember 'db_datareader', 'axmrruntimeuser'
EXEC sp_addrolemember 'db_datawriter', 'axmrruntimeuser'

CREATE USER axretaildatasyncuser FROM LOGIN axretaildatasyncuser
EXEC sp_addrolemember 'DataSyncUsersRole', 'axretaildatasyncuser'

CREATE USER axretailruntimeuser FROM LOGIN axretailruntimeuser
EXEC sp_addrolemember 'UsersRole', 'axretailruntimeuser'
EXEC sp_addrolemember 'ReportUsersRole', 'axretailruntimeuser'

CREATE USER [NT AUTHORITY\NETWORK SERVICE] FROM LOGIN [NT AUTHORITY\NETWORK SERVICE]
EXEC sp_addrolemember 'db_owner', 'NT AUTHORITY\NETWORK SERVICE'

UPDATE T1
SET T1.storageproviderid = 0
    , T1.accessinformation = ''
    , T1.modifiedby = 'Admin'
    , T1.modifieddatetime = getdate()
FROM docuvalue T1
WHERE T1.storageproviderid = 1

ALTER DATABASE [AXDB_UATCOPY07Oct18] SET CHANGE_TRACKING = ON (CHANGE_RETENTION = 6 DAYS, AUTO_CLEANUP = ON)

-- stop the Microsoft dynamics 2 services, Management reporter service, and World wide web service.


ALTER DATABASE [AxDB] MODIFY NAME = [AxDB_Original07Oct18]

ALTER DATABASE [AXDB_UATCOPY07Oct18] MODIFY NAME = [AxDB]

-- start the Microsoft dynamics 2 services, Management reporter service, and World wide web service.
--Do full DB sync
--If it is on premises add a user on admin provision tool to access AX application

.\DeployAllReportsToSsrs.ps1 -LogFilePath C:\logreport.txt -PackageInstallLocation J:\AosService\PackagesLocalDirectory

05 May 2019

D365 FO Report extension example using event handler : Report extension

Report extension in two ways an event handler. Below is the example for the Customer base data report.

Step1. Duplicate the report and do the required changes.
Step2. Create an extension for the Report output MenuItem and output object to new report what we create in the step1.

Or
If the output menuItem assigned with controller class.

Step1. Duplicate the report and do the required changes.
Step2. Create an EventHandler or COC for the Controller classgetReportName() method.
Step3. In the event handler method, assign the new report to return in this method.

 public static str getReportName(Args _args)
    {
        str reportName;

        if (_args.menuItemName() == menuitemOutputStr(CustBasedata))
        {
            reportName = ssrsReportStr(CustBasedata, Report);
        }
        else if (_args.menuItemName() == menuitemOutputStr(CustListReport))
        {
            reportName = ssrsReportStr(CustListReport, Report);
        }
        else if (_args.menuItemName() == menuitemOutputStr(smmSalesCustItemStatistics))
        {
            reportName = ssrsReportStr(smmSalesCustItemStatistics, Report);
        }

        return reportName;
    }


class smmReportController_EventHandler
{
    /// <summary>
    ///
    /// </summary>
    /// <param name="args"></param>
    [PostHandlerFor(classStr(smmReportsController), staticMethodStr(smmReportsController, getReportName))]
    public static void smmReportsController_Post_getReportName(XppPrePostArgs args)
    {
        Str reportName;
        smmReportsController  reportsController = args.getThis();
        Args argsLoc = args.getArg(identifierStr(_args));
        //Args argsLoc = args.getArgNum(1);
        if (argsLoc.menuItemName() == menuitemOutputStr(CustBasedata))
        {
            reportName = ssrsReportStr(CustBasedataTRG, Report);
        }     
        args.setReturnValue(reportName);
    }

}

23 April 2019

How to override form data source field lookup method in D365FO , Form Extension control Lookup method in D365FO

Here find the GitHub Example
public class InventTestItemQualityGroup
{
    [FormDataSourceEventHandler(formDataSourceStr(InventTestItemQualityGroup, InventTestItemQualityGroup), FormDataSourceEventType::Initialized)]
    public static void InventTestItemQualityGroup_OnInitialized(FormDataSource sender, FormDataSourceEventArgs e)
    {
        var overrides = InventTestItemQualityGroup::construct();
     
        sender.object(fieldNum(InventTestItemQualityGroup, HHDFinishedGoods)).registerOverrideMethod(methodStr(FormDataObject, lookup),
            methodStr(InventTestItemQualityGroup,ItemId_OnLookup), overrides);
    }

    public void ItemId_OnLookup(FormStringControl _callingControl)
    {
        SysTableLookup sysTableLookup = SysTableLookup::newParameters(tableNum(InventTable), _callingControl);
        Query                 query = new Query();
        QueryBuildDataSource qbds,qbds2;
        QueryBuildLink            queryBuildLink;

        qbds= query.addDataSource(tableNum(InventTable));
        qbds.addRange(fieldNum(InventTable, ItemId));
        qbds2 = qbds.addDataSource(tablenum(InventItemGroupItem));   

        qbds2.joinMode(JoinMode::InnerJoin);//
        qbds2.addLink(fieldnum(InventTable, ItemId),fieldnum(InventItemGroupItem, ItemId));//
        qbds2.addLink(fieldNum(InventTable, DataAreaId),fieldnum(InventItemGroupItem, ItemDataAreaId));//
        qbds2.addRange( fieldNum( InventItemGroupItem, ItemGroupId)).value(queryValue(InventParameters::find().HHDItemGroup));

        sysTableLookup.parmQuery(query);

        sysTableLookup.addLookupfield(fieldNum(InventTable, ItemId));
        sysTableLookup.addLookupfield(fieldNum(InventTable, NameAlias));     
        sysTableLookup.addLookupfield(fieldNum( InventItemGroupItem, ItemGroupId));

        sysTableLookup.performFormLookup();
    }

    public static InventTestItemQualityGroup construct()
    {
        return new InventTestItemQualityGroup();
    }

}

Data entities initialize the value in D365

public void mapEntityToDataSource(DataEntityRuntimeContext _entityCtx, DataEntityDataSourceRuntimeContext _dataSourceCtx)
    {
InventJournalTrans journalTrans = _dataSourceCtx.getBuffer();
journalTrans.field = "Value";

22 April 2019

Data Entities sequence methods in D365

Exporting Data in DataEntities calling sequence:

1. initValue
2. validateField
3. validateWrite
4. update
4.1. doUpdate
4.1.1. persistEntity
4.1.1.1. doPersistEntity
4.1.1.1.1. initializeDataSources
4.1.1.1.1.1. initializeEntityDataSource
Note: initializeDataSource is called once for each DataSource in Entity.
4.1.1.1.2. mapEntityToDataSources
Note: initializeDataSource is called once for each DataSource in Entity.
4.1.1.1.3. saveDataSources
4.1.1.1.3.1. updateEntityDataSource
4.1.1.1.4. mapEntityToDataSource (maybe for another record)
4.1.1.1.5. saveDataSources
4.1.1.1.5.1. updateEntityDataSource for update operation and (insertEntityDataSource for insert)
4.1.1.1.5.1.1. mapDataSourceToEntity
4.1.1.1.5.1.2. doSaveDataSource
4.1.1.1.5.1.2.1. updateDataSource
4.1.1.1.5.1.2.1.1. preupInsertDataSource
4.1.1.1.5.1.2.1.1.1. validateWrite of table
Plus:
postLoad

09 April 2019

Introduction to the SysOperation framework in AX 7 / D365 - SysOperation Frame work example


Introduction to the SysOperation framework in AX 7 / D365 - SysOperation Frame work example

Click here for another example

The SysOperation Framework, initially called Business Operation Framework, seem to be the new substitute of the RunBase framework. As such, it allows you to perform operations that require parameters from the user, it allows you to set the operations to be executed in batch, or in a new asynchronous way, or in a synchronous manner. The great thing is that it simplifies the pack / unpack of variables that wasin the RunBase framework, taking advantage of the Attributes feature, introduced with AX 2012.


So to get started, we must understand that the SysOperation framework works in a way that's close to the Model-View-Controller (MVC) pattern.
The key objects here are:


· Data Contract:

The data contract is the model class in which we define which attributes we need for our operation, commonly set as parameters by the user in a dialog. It's nothing more than a model class with a few attributes in it. We can define a SysOperation Data Contract class simply by adding the DataContractAttribute attribute to its declaraion. Additionally, if we want a set of methods to be available to us, we can also extend the SysOperationDataContractBase base class. With this class, we can define how our basic dialog will look like to the user. We can define labels, groups, sizes and types of the parameters.
· UI Builder:

The UI builder class is actually an optional class for the SysOperation framework, which kind of acts as the view part of the pattern. You should only use it if you want to add some extra behavior to the dialog that AX constructs dynamically for you. If you are perfectly happy with the dialog AX shows you when you run your operation, you shouldn't worry about this class. To create a UI Builder, you should extend the SysOperationAutomaticUIBuilder class. It will provide you a set of methods to work with the dialog's design, but we usually add extra behavior or customize a lookup inside the postBuild method.
· Controller:

The controller class has greater responsibility than the others. As the name suggests, it is the class that orchestrates the whole operation. The controller class also holds information about the operation, such as if it should show a progress form, if it should show the dialog, and its execution mode - asynchronous or not. To create a controller class you should extend the SysOperationServiceController, which will give you all of the methods you need.
· Service:

There are some who put the business logic on controller classes so that they also perform the operation itself. I'm particularly not a big fan of that, it's too much responsibility for a single class! The programmers who created the SysOperation framework probably think the same, and they have made a way to separate the operation. You can create a service class! The only thing you have to do is extend the SysOperationServiceBase class and you're good to go. This class is the one that should contain all the business logic. When constructing your controller, you will indicate which class holds the operation that the controller will trigger, I'll demonstrate it later.

So this was a brief explanation in my own words of how the SysOperation currently works. For more information, you can also download the official Microsoft whitepaper here.
Now on to the code.

SysOperating it

To define our Model, or our DataContract, all we have to do is create a class, with each of the values created as the class' fields, and parm methods, which will be our "properties". Each field that we want to pack and unpack during the execution of the operation has to have its parm method decorated with the DataMemberAttribute attribute. If we don't add that attribute, the value for that field won't be packed to the server, and will not be initialized when you try to access it on the service class. Additionally, parm methods without the attribute will not have its field displayed on the default dialog.

So for example, consider the following model class:


[DataContractAttribute]
class SysOperationDemoDataContract
{
Name name;
BirthDate birthDate;

MonthsOfYear monthThatWontBeSerialized;
}

[DataMemberAttribute]
public BirthDate parmBirthDate(BirthDate _birthDate = birthDate)
{
birthDate = _birthDate;

return birthDate;
}

[DataMemberAttribute]
public Name parmName(Name _name = name)
{
name = _name;

return name;

}

public MonthsOfYear parmMonthThatWontBeSerialized(MonthsOfYear _monthThatWontBeSerialized = monthThatWontBeSerialized)

{
monthThatWontBeSerialized = _monthThatWontBeSerialized;

return monthThatWontBeSerialized;
}



So after easily defining our model, which is a class that will store all of the values that we will need from the user - or not - for our operation, we can define our service.

To define our service class and have it called by the SysOperation framework, we must define a method that receives our data contract as a parameter. So for this demo, I have defined the following class:


class SysOperationDemoService extends SysOperationServiceBase
{

}


public str performDemo(SysOperationDemoDataContract _contract)

{
str info = strFmt('%1 was born in %2', _contract.parmName(), _contract.parmBirthDate());

info(info);

return info;
}

It doesn't do much. It takes what the user has typed in the dialog and displays it on the Infolog. In a real scenario, this class could either contain the business logic itself, or just interact with other classes that contain it. I personally rather have the business logic in other classes, because by pattern, this class should always have the "Service" suffix. I'll talk more about naming conventions for the SysOperation later.

So we have our Model, we have our service. What about our controller?

The controller class has a few key methods like the RunBase framework, that you should be very familiar with, which are:


· main
· construct
· new
· validate
· run


Their names are self explanatory, and if you are a little familiar with the RunBase framework you'll have no problem getting over them. I put the new method in that list because the new method of the base SysOperationServiceController class receives two strings as parameters, which are the name of the class and the method of the service to be executed. There is a neat method called initializeFromArgs which sets these values on the controller after it's constructed, given a correctly intialized Args object. This method allows you to use it with menu items, I'll blog about it later. Anyway, for this example we'll override the new method on our controller.

I've also put the construct method on the list. As a general good practice, your controller should have a public static method called construct, which will do all the dirty constructing (duuh) work. Additionally, you can override the new method and set it as a protected method, so that anyone who wants to use your controller will have to call the construct method.


So here's how I'll define our sample controller:


class SysOperationDemoController extends SysOperationServiceController

{

}



protected void new()

{

// This tells the controller what method it should execute as the service. In this case, we'll run SysOperationDemoService.performDemo()

super(classStr(SysOperationDemoService), methodStr(SysOperationDemoService, performDemo), SysOperationExecutionMode::Synchronous);

}
public void new()
    {
        super();
 
        this.parmClassName(classStr(SysOperationServiceClass));
        this.parmMethodName(methodStr(SysOperationServiceClass, processOperation));
 
        this.parmDialogCaption("Batch Operation Dialog Title");
    }



public static SysOperationDemoController construct()

{

SysOperationDemoController controller;



controller = new SysOperationDemoController();



controller.parmShowDialog(true); // Actually the default value

controller.parmShowProgressForm(false);



return controller;

}

public static void main(Args _args)

{

SysOperationDemoController controller;

controller = SysOperationDemoController::construct();

controller.startOperation();

}

protected boolean validate()

{

SysOperationDemoDataContract contract;

boolean ret = true;



contract = this.getDataContractObject();



if(contract.parmBirthDate() > DateTimeUtil::date(DateTimeUtil::addYears(DateTimeUtil::utcNow(),-18)))

{

// Failing the validate will not close the dialog, and the user will have another chance of input

}



return ret;

}

public void run()

{

info('Run method has been called');



super();

}

protected ClassDescription defaultCaption()

{

// This will be the dialog's caption

return 'SysOperation demo';

}

So as you can see we define which method will be executed as the service on the new method.
The construct method does the dirty work, setting some properties on the controller. The validate then checks if the user is at least 18 years old. If the validate fails, the user has another chance of setting the correct values on the dialog. Also, as a tip, I've set the dialog's caption by overriding the defaultCaption method. As for the run method, I've only overwritten it so that you can see the execution flow, which is:

>> main
>> construct
>> validate
>> run

Draw a sequence diagram in your head. :)


With the three classes that I have described here, you can actually run a simple SysOperation framework demo. You can do this by simply opening the controller class and pressing F5.


Here's some tips that can save you some time:


· You should generate incremental IL for every change on any of the classes from the SysOperation framework. It won't take long, and unfortunately, it's necessary
· When you start getting odd behaviors, specially when you change something on your DataContract and it doesn't reflect on your dialog, you should clean the usage cache. You can do this by clicking on Tools > Options > Usage data > Reset
· You can debug SysOperation services if your Controller class' execution mode is either Synchronous or Asynchronous, when you've unchecked the option to execute business operations in CIL. You can get more info on executing business operations in CIL here.



As I've mentioned, the UI Builder class allows us to completely customize the dialog which is constructed for us. Since I'll probably turn this into a series of posts, I'll demonstrate this over the next posts.


Even though it's fairly easy and simple to understand, I must confess that I think it requires too much code infrastructure to perform some simple tasks. It feels strange having to write at least 3 classes to perform a simple delete operation that requires some extra logic or validation, for example. Still, I'd rather have to do so instead of having to control the dirty pack / unpack pattern of the RunBase framework.

Ledger Voucher creation Framework and x++ code to create ledger voucher

 Please click her for MS reference file Below is the out of the box example reference and code. SalesInvoiceJournalPostSubBill_Extension->...