пятница, 4 июля 2014 г.

How to transfer objects between server and client using List, Set or Map in Dynamics AX


Hi.
Some time we use such structure as a list of classes. And we can get into situation, when we need to transfer our list using a container (for example between client and server or we need to print Excel report for client ...).
In this post we will try to do this operation using very simple approach.

Starting conditions. We have client site class, and hard processing. We want to transfer hard processing to the server. Let's try do something interesting ...
CMSalesOrderDataContract with some variables

class CMSalesOrderDataContract
{
    SalesId   salesId;
    Qty       maxQty;
    Qty       averageQty;
}

we have parm methods to access these fields

public SalesId parmSalesId(SalesId _salesId = salesId)
{
    salesId = _salesId;
    return salesId;
}

public Qty parmMaxQty(Qty _maxQty = maxQty)
{
    maxQty = _maxQty;
    return maxQty;
}

public Qty parmAverageQty(Qty _averageQty = averageQty)
{
    averageQty = _averageQty;
    return averageQty;
}

and simple init method
public void initFromSalesTable(SalesTable _salesTable)
{
    SalesLine   salesLine;

    this.parmSalesId(_salesTable.SalesId);
    select maxOf(SalesQty) from salesLine
        where salesLine.SalesId == _salesTable.SalesId
        ;
    this.parmMaxQty(salesLine.SalesQty);
    select avg(SalesQty) from salesLine
        where salesLine.SalesId == _salesTable.SalesId
        ;
    this.parmAverageQty(salesLine.SalesQty);
}

and construct method
public static CMSalesOrderDataContract construct()
{
    return new CMSalesOrderDataContract();
}

Than we have 2 classes:
one collects information for us, another prints information in form;

Server class collects and saves information for us.
Declare variables
class CMCollectInformation
{
    List salesOrdersInfo;
}
init list
public void new()
{
    salesOrdersInfo = new List(Types::Class);
}
construct class on Server side.
public server static CMCollectInformation construct()

{
    return new CMCollectInformation();
}

collect information
public void collect()
{
    SalesTable                  salesTable;
    CMSalesOrderDataContract    salesOrderDataContract;
    while select salesTable
    {
        salesOrderDataContract = CMSalesOrderDataContract::construct();
        salesOrderDataContract.initFromSalesTable(salesTable);
        salesOrdersInfo.addEnd(salesOrderDataContract);
    }
}
return information packed in a container
public container getPackedData()
{
    return salesOrdersInfo.pack();
}

And second class allows us to print information on client side
class CMPrintInformation
{

}
initialize class strong on CLIENT side
public client static CMPrintInformation construct()
{
    return new CMPrintInformation();
}

public void printInfo()
{
    CMCollectInformation        collectInformation;
    ListEnumerator              le;
    CMSalesOrderDataContract    salesOrderDataContract;
    List                        soClientInfoList;

    // create and collect information n server side
    collectInformation  = CMCollectInformation::construct();
    collectInformation.collect();
    soClientInfoList = List::create(collectInformation.getPackedData());
    le = soClientInfoList.getEnumerator();

    while (le.moveNext())
    {
        salesOrderDataContract = le.current();
        info(strFmt('SalesId %1, maxQty %2, averageQty %3', salesOrderDataContract.parmSalesId(), salesOrderDataContract.parmMaxQty(), salesOrderDataContract.parmAverageQty()));
    }
}

Finally let's create a job to test our work
static void Job3(Args _args)
{
    CMPrintInformation  printInformation = CMPrintInformation::construct();
    printInformation.printInfo();
}

An error is thrown, because our DataContract class does not contain pack and unpack methods

Let's implment a few changes in our dataContract class 
Class declaration should look like

class CMSalesOrderDataContract implements SysPackable
{
    SalesId   salesId;
    Qty       maxQty;
    Qty       averageQty;
    
    #define.CurrentVersion(1)
    #localmacro.CurrentList
        salesId,
        maxQty,
        averageQty
    #endmacro
    
}

and add pack method
public container pack()
{
    return [#CurrentVersion,#CurrentList];
}

and add unpack method
public boolean unpack(container packedClass)
{
    int version     = runbase::getVersion(packedClass);
    switch (version)
    {
        case #CurrentVersion:
            [version,#CurrentList] = packedClass;
            return true;

        default :
            return false;
    }
    return false;
}

Run the job again and get a new error

We need to implement create method 

public static CMSalesOrderDataContract create(container _packedClass)
{

    CMSalesOrderDataContract    salesOrderDataContract = CMSalesOrderDataContract::construct();

    salesOrderDataContract.unpack(_packedClass);
    return salesOrderDataContract;
}

Try to run our job once again

Excellent result!

Summary:
When we want to transfer List, Set or Map containing objects of a class between server and client we have to implement pack, unpack methods and create method. Pack and unpack methods are standard. Create method must be static and should get container as an input parameter. It should create an instance of the class, unpack variables from passed container and return the instance.



пятница, 6 июня 2014 г.

Useful. Copy purchLines from one purch order to another

Copy purchLines from one purch order to another in Dynamics AX 2009, 2012.

Last time I get many question from new developer How to use ... I want to print simple but useful jobs to help newbie "Do their Things Faster With More Energy".

Standard functionality allow copy purch (sales) lines from one order to another using form \Forms\PurchCopying. If we try to see algorithm we can see construction like below.

AX2009

static void Job63(Args _args)
{
    PurchCopying            purchCopying = PurchCopying::construct(SalesPurchCopy::CopyAllLines);
    TmpFrmVirtual           tmpFrmVirtualLines;
    TmpFrmVirtual           tmpFrmVirtualHeader;
    PurchTable              purchTableFrom  = PurchTable::find('PO-0000597');
    PurchLine               purchLineFrom;
    PurchTable              purchTableTo    = PurchTable::find('PO-0000602');

    void writeTmpFrmVirtual(TmpFrmVirtual   _tmpFrmVirtual,
                            tableId         _tableId,
                            recId           _recId,
                            Num             _id,
                            LineNum         _lineNum = 0,
                            TransDate       _transDate = systemdateget(),
                            Qty             _qty = 0)

    //\Forms\PurchCopying\Methods\writeTmpFrmVirtual
    {
        _tmpFrmVirtual.TableNum     = _tableId;
        _tmpFrmVirtual.RecordNo     = _recId;
        _tmpFrmVirtual.Id           = _id;
        _tmpFrmVirtual.LineNum      = _lineNum;
        _tmpFrmVirtual.TransDate    = _transDate;
        _tmpFrmVirtual.Qty          = _qty;
        _tmpFrmVirtual.write();
    }
    ;
    // if we want to copy with header.
    //writeTmpFrmVirtual(tmpFrmVirtualHeader,
    //                   purchTableFrom.TableId,
    //                   purchTableFrom.RecId,
    //                   purchTableFrom.PurchId);
    while select purchLineFrom
        where purchLineFrom.PurchId == purchTableFrom.PurchId
    {
        tmpFrmVirtualLines.clear();
        tmpFrmVirtualLines.initValue();
        writeTmpFrmVirtual(tmpFrmVirtualLines,
                           purchLineFrom.TableId,
                           purchLineFrom.RecId,
                           purchLineFrom.PurchId,
                           purchLineFrom.LineNum,
                           systemdateget(),
                           purchLineFrom.PurchQty);
    }
    purchCopying.initParameters(purchTableTo, tmpFrmVirtualLines, tmpFrmVirtualHeader);
    purchCopying.copy(); // transaction will created inside method
}

AX2012

static void Job63(Args _args)
{
    PurchCopying            purchCopying = PurchCopying::construct(SalesPurchCopy::CopyAllLines);
    TmpFrmVirtual           tmpFrmVirtualLines;
    TmpFrmVirtual           tmpFrmVirtualHeader;
    PurchTable              purchTableFrom  = PurchTable::find('PO-000055');
    PurchLine               purchLineFrom;
    PurchTable              purchTableTo    = PurchTable::find('PO-000057', true /*not good selectforupdate within transaction, but otherwise standart 2012R2 work with error */);
    List                    tmpFrmVirtualLinesList = new List(Types::Record);
    List                    tmpFrmVirtualHeaderList = new List(Types::Record);   
    PurchCopyingPurchTableContract    contract;
    Qty                     qtyFactor         = 1;
    NoYes                   reverseSign       = NoYes::No;
    NoYes                   recalculateAmount = NoYes::No;
    NoYes                   copyMarkup        = NoYes::No;
    NoYes                   copyPrecisely     = NoYes::No;
    NoYes                   deleteLines       = NoYes::No;

    void writeTmpFrmVirtual(TmpFrmVirtual   _tmpFrmVirtual,
                            tableId         _tableId,
                            recId           _recId,
                            Num             _id,
                            LineNum         _lineNum = 0,
                            TransDate       _transDate = systemdateget(),
                            Qty             _qty = 0)

    //\Forms\PurchCopying\Methods\writeTmpFrmVirtual
    {
        _tmpFrmVirtual.TableNum     = _tableId;
        _tmpFrmVirtual.RecordNo     = _recId;
        _tmpFrmVirtual.Id           = _id;
        _tmpFrmVirtual.LineNum      = _lineNum;
        _tmpFrmVirtual.TransDate    = _transDate;
        _tmpFrmVirtual.Qty          = _qty;
        _tmpFrmVirtual.write();
    }
    ;

    // if we want to copy with header.
    //writeTmpFrmVirtual(tmpFrmVirtualHeader,
    //                   purchTableFrom.TableId,
    //                   purchTableFrom.RecId,
    //                   purchTableFrom.PurchId);
    //tmpFrmVirtualHeaderList.addEnd(tmpFrmVirtualHeader);
    while select purchLineFrom
        where purchLineFrom.PurchId == purchTableFrom.PurchId
    {
        tmpFrmVirtualLines.clear();
        tmpFrmVirtualLines.initValue();
        writeTmpFrmVirtual(tmpFrmVirtualLines,
                           purchLineFrom.TableId,
                           purchLineFrom.RecId,
                           purchLineFrom.PurchId,
                           purchLineFrom.LineNumber,
                           systemdateget(),
                           purchLineFrom.PurchQty);
        tmpFrmVirtualLinesList.addEnd(tmpFrmVirtualLines);
    }

    contract = PurchCopyingPurchTableContract::construct();   
    contract.parmCallingTable(purchTableTo);
    contract.parmPackedTmpFrmVirtualLines(tmpFrmVirtualLinesList.pack());
    contract.parmPackedTmpFrmVirtualHeader(tmpFrmVirtualHeaderList.pack());
    contract.parmQtyFactor(qtyFactor);
    contract.parmReverseSign(reverseSign);
    contract.parmRecalculateAmount(recalculateAmount);
    contract.parmCopyMarkup(copyMarkup);
    contract.parmCopyPrecisely(copyPrecisely);
    contract.parmDeleteLines(deleteLines);
    purchCopying.initParameters(contract);
    purchCopying.copy(); // transaction will created inside method
}

понедельник, 26 мая 2014 г.

Part2. Dynamics ax 2012. Split construct method by models.

Split construct method by models. Part2.

Last time we tried to resolve the problem with separate construct method by model in one layer using new future in ax delegate and event handler.
At this article we will try to resolve the same problem using another approach.
It was first time introduced in Unit testing (source Classes\SysTestCase\testMethods + Classes\SysTest\invoke - try to find methods in object by specific mask), and now in AX 2012 we can find very similar method using name conversational in financial dimension (\Classes\DimensionEnabledType\getSystemDefinedDimensionsServer - try to find Views by name consist of specific mask). Lets try to use it.
From previous topic we have next conditions
      TestBaseClass - in model A01
      TestBaseClass01_Model01 in model A01
      TestBaseClass02_Model02 in model A02
We need to write construct method, and this method must work after we move model A01 to another application.

in TestBaseClass (model A01) we create new static method constructFromCode()
public static TestBaseClass constructFromCode(SalesTable _salesTable)
{
       #define.codeConstructor('codeConstructor')
       TestBaseClass       ret;
       SysDictClass        sysDictClass = new SysDictClass(classNum(TestBaseClass))       ;
       DictMethod          method = null;
       Set                 methods;
       SetEnumerator       se;
       Set                 orderedSet = new Set(Types::String);
       ExecutePermission   executePermission;
       ;

       // source Classes\SysTestCase\testMethods
       methods = sysDictClass.methods(false, true, false);
       se = methods.getEnumerator();
       while (se.moveNext())
       {
              method = se.current();
              if (method
               && method.accessSpecifier()    == AccessSpecifier::public
               && method.returnType()         != Types::void
               && method.parameterCnt()       == 1)
              {
                     if (strStartsWith(method.name(), #codeConstructor))
                     {
                            orderedSet.add(method.name());
                     }
              }         // if methods
       } // while

       // part 2 - split init call
       // source \Classes\SysTest\invoke
       executePermission = new ExecutePermission();
       executePermission.assert();
       se = orderedSet.getEnumerator();
       while (se.moveNext())
       {
              // part 2.1 call methods
              ret = sysDictClass.callStatic(se.current(), _salesTable);
              if (ret)
                     break;
       }
       CodeAccessPermission::revertAssert();

       // init default value
       if (!ret)
       {
              ret = new TestBaseClass();
       }
       ret.parmSalesTable(_salesTable);
       return ret;
}
next we need to create 2 additional methods in TestBaseClass
codeConstructorClass01_M01 in A01 model
public static TestBaseClass codeConstructorClass01_M01(SalesTable _salesTable)
{

       TestBaseClass01_Model01 testBaseClass01_Model01 = TestBaseClass01_Model01::construct();

       if (testBaseClass01_Model01.findDetermination(_salesTable))
           return testBaseClass01_Model01;
       return null;
}
and codeConstructorClass02_M02 in A02 model
public static TestBaseClass codeConstructorClass02_M03(SalesTable _salesTable)
{
       TestBaseClass02_Model02 testBaseClass02_Model02 = TestBaseClass02_Model02::construct();

       if (testBaseClass02_Model02.findDetermination(_salesTable))
           return testBaseClass02_Model02;
       return null;
}
let's create and run a job to check our implementation
static void TestBaseClass2(Args _args)
{
        SalesTable                      salesTable;         // cursor
        TestBaseClass                   baseClass;
        setPrefix("SalesInfo2");
        while select salesTable
        {
                baseClass = TestBaseClass::constructFromCode(salesTable);
                baseClass.showInfo();
        }
        info("done");
}
and results are


Results


Summary.
We can use various solutions to make construct method independent from models and layers.

Part1. Dynamics ax 2012. Split construct method by models.

Split construct method by models. Part1.

Using construct method is a common technique in AX, for example SalesFormLetter::construct(...)


Corresponding child class of SalesFormLetter class is constructed based on DocumentStatus parameter.
This is good. However let’s try to implement one simple task.

Step 1.

We need to print message BackOrder is #SalesId #CustomerName for all sales orders.
Implementation.
Create a new class
class TestBaseClass
{
   SalesTable   salesTable;
}
public SalesTable parmSalesTable(SalesTable _salesTable = salesTable)
    return salesTable;
} public void showInfo()
{
info(strFmt("Backorder is %1 %2", this.parmSalesTable().SalesId, this.parmSalesTable().customerName()));
}
{


public static TestBaseClass construct()
{
   return new TestBaseClass();
}
   
and job for testing this class
static void TestBaseClass1(Args _args)
{
  SalesTable   salesTable; // cursor
 TestBaseClass   baseClass; setPrefix("SalesInfo1");
 while select salesTable
         baseClass = TestBaseClass::construct(salesTable.SalesStatus);
         baseClass.parmSalesTable(salesTable);
         baseClass.showInfo();
     }
     info("done");
}
Result is

Step 2.

Consultant has changed the requirement to print Invoice if SalesStatus== Invoice. - ok - let's implement the change.
Create a new class
class TestBaseClass01_Model01 extends TestBaseClass
{

}
overwrite the method
public void showInfo()
{
    info(strFmt("Invoiced is %1 %2", this.parmSalesTable().SalesId, this.parmSalesTable().customerName()));
}

and change construct method of TestBaseClass
public static TestBaseClass construct(SalesStatus _salesStatus)
{

    TestBaseClass   testBaseClass;
    switch (_salesStatus)
    {
        case SalesStatus::Invoiced:
            testBaseClass = new TestBaseClass01_Model01();
            break;

        default :           
            testBaseClass = new TestBaseClass();

    return testBaseClass;
}

Update job and run it

OK - we have done this!
All the changes were done in one layer and one model (for example layer cus, model A01).

Step 3.

Our consultant wants to create another modification in cus layer, but in model A02. The goal of the modification is to print Delivered for sales order with SalesStatus==Delivered. Why do we need to implement it in different model (we ask our consultant)?
Because we have many clients. Some clients want to get additional information, some - don't. Base logic is in one model and additional logic is in another model which allows us to have only one application and to develop without any problem in the future.
Ok, We have a new task!!!
Excellent. Let's do it!

*Note - if you create an object in wrong model - don't worry, you can always change current model of the object (method, event handler ...) - right click on the object and select - Move to model ...

Create a new model and select it as current. In new model create a new class
class TestBaseClass02_Model02 extends TestBaseClass
{

}

overload method
public void showInfo()
{
    info(strFmt("Delivered is %1 %2", this.parmSalesTable().SalesId, this.parmSalesTable().customerName()));
}

than slightly modify method TestBaseClass::construct()

public static TestBaseClass construct(SalesStatus _salesStatus)
{
    TestBaseClass   testBaseClass;

    switch (_salesStatus) 
    {
        case SalesStatus::Invoiced:
            testBaseClass = new TestBaseClass01_Model01();
            break;

        case SalesStatus::Delivered:
            testBaseClass = new TestBaseClass02_Model02();
            break;

        default :
            testBaseClass = new TestBaseClass();
    }
    return testBaseClass;
}
and run our job without any modification


Everything is good and we can summarize our results.
1. We implemented the requirements - good.
2. We created correct models by AX standard - good.
3. We have one model A01 for one type of customers - good.
4. We have one model A02 - this model allows us to extend logic for other customers. - good.
Everything is almost good!

Let's see our solution split by models

TestBaseClass - in model A01
TestBaseClass01_Model01 in model A01
TestBaseClass02_Model02 in model A02

If we try to install model A01 to another application - we will get compile error, because system could not find class TestBaseClass02_Model02 - this is not good.
If we move TestBaseClass::construct to A02 model and try to install A01 to another application - we will lose construct method - this is not good.

How we can re-factoring our small solution to resolve this problem?
In AX 2012 developers have interesting mechanism - delegates. Let's try to use it to resolve the conflict.
Ok, We have a new Interesting task!

Step 4.

4.1 Change TestBaseClass::construct

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

4.2. create a new method TestBaseClass. findDetermination() (model A01)
public boolean findDetermination(SalesTable _salesTable)
{
    boolean ret;

    if (_salesTable.SalesStatus == SalesStatus::Backorder)
    {
        ret = true;
        this.parmSalesTable(_salesTable);
    }
    return ret;
}

4.3. overwrite method \Classes\TestBaseClass01_Model01\findDetermination (model A01)
public boolean findDetermination(SalesTable _salesTable)
{
    boolean ret;

    if (_salesTable.SalesStatus == SalesStatus::Invoiced)
    {
        ret = true;
        this.parmSalesTable(_salesTable);
    }
    return ret;
}

4.4. overwrite method \Classes\TestBaseClass02_Model02\findDetermination (model A02)
public boolean findDetermination(SalesTable _salesTable)
{
    boolean ret;

    if (_salesTable.SalesStatus == SalesStatus::Delivered)
    {
        ret = true;
        this.parmSalesTable(_salesTable);
    }
    return ret;
}

4.5. Let's create a new class - this class helps us to resolve the problems (model 01)
// Class fabric. This class can generate right class updater

class TestBaseClassFabricaConstructor
{
    SalesTable  salesTable;             // salesTable for updating
    List        potentialConstructors;  // save all potential updaters
}


public void new()
{
    potentialConstructors = new List(Types::Class)
}



public SalesTable parmSalesTable(SalesTable _salesTable = salesTable)
{
    salesTable = _salesTable;
    return salesTable;
}



// add updater to some list to save
public void subscribe(TestBaseClass _subscriber)
{
    potentialConstructors.addEnd(_subscriber);
}





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


and create a new blank but very important method
delegate void constructorCollect(TestBaseClassFabricaConstructor _fabricaCollector)
{

}

plus another method to call delegate
// call all subscribers and get subscribers list. Subscriber add itselt manualy to our collections

public void collect(TestBaseClassFabricaConstructor _fabricaCollector = this)
{
    this.constructorCollect(_fabricaCollector);
}

It look's good, but not finished. We will return to this class a little bit later.

4.6. create a new method \Classes\TestBaseClass\subscribe (model A01)
public static void subscribe(TestBaseClassFabricaConstructor _fabricaConstructor)
{
    TestBaseClass   baseClass = TestBaseClass::construct();
    _fabricaConstructor.subscribe(baseClass);
}

4.7. Create a new method \Classes\TestBaseClass01_Model01\subscribe (model A01)
public static void subscribe(TestBaseClassFabricaConstructor _fabricaConstructor)
{
    TestBaseClass01_Model01   baseClass = TestBaseClass01_Model01::construct();
    _fabricaConstructor.subscribe(baseClass);
}

4.8. Create anew method \Classes\TestBaseClass02_Model02\subscribe (model A02)
public static void subscribe(TestBaseClassFabricaConstructor _fabricaConstructor)
{
    TestBaseClass02_Model02   baseClass = TestBaseClass02_Model02::construct();
   _fabricaConstructor.subscribe(baseClass);
}

4.9. Return to class TestBaseClassFabricaConstructor.
method constructorCollect -  right click "New Event Handler Subscription"


and do like shown bellow 3 times - for all three classes

Each event handler can be placed in own model.
TestBaseClass and TestBaseClass01_Model01 is placed in model A01, TestBaseClass02_Model02 is placed in A02 model.
4.10 - create the last method in TestBaseClassFabricaConstructor.softConstruct
// determine what is class want be updater for salesTable
public TestBaseClass softConstruct(SalesTable _salesTable = this.parmSalesTable())
{
    ListEnumerator  le = potentialConstructors.getEnumerator();
    TestBaseClass   ret;

    while (le.moveNext())
    {
        ret = le.current();

        if (ret.findDetermination(_salesTable)) // check if updater want update SalesTable - welcome
        {
            break;
        }
    }
    return ret;
}

Very good. Now we need to run it!

Create a new job

// TODO - Example for everyone. How me can split method with multimodel declarations
static void TestBaseClassFabricaConstructorJob(Args _args)
{
    TestBaseClassFabricaConstructor fabricaConstructor; // constructor for our class updater
    SalesTable                      salesTable;         // cursor
    TestBaseClass                   baseClass;          // abstract updater

    setPrefix("SalesInfo");
    while select salesTable
    {
        fabricaConstructor = TestBaseClassFabricaConstructor::construct();  // init
        fabricaConstructor.parmSalesTable(salesTable);                      // init
        fabricaConstructor.collect();                                       // call method collect. This method call all subscribers and subscribers add itself to update candidate list
        baseClass = fabricaConstructor.softConstruct();                     // loop by all updaters and find the best updater
        if (baseClass)                                                      // if updater find - show info about it
        {
            baseClass.showInfo();
        }
    }
    info("done");
    // If we need to create new updater
    // 1. Create new class and extends it from base class (TestBaseBaseClass_Model01)
    // 2. Create static method subscribe
    // 3. Create subscription to \Classes\TestBaseClassFabricaConstructor\constructorCollect
    // 4. Create method findDetermination
    // 5. Create method showInfo
    //
    //
}

and results


Well done!

Let's see what will happen if we decide  to publish model A01 for clients - it will work - because we move model with out one event handler.

Next time we will try to resolve our task with another approach.
Have a nice day!