Saturday, November 7, 2009

Comparison Framework Sample : BOM Version comparer

Hi all :)

If we take some time to explore the classes structure existing inside Dynamics Ax we can find many useful frameworks.

One of this frameworks is the comparison framework.
I'm sure many of you have used sometimes the comparison code tool to discover what has changed in code between two layers.

Well, Dynamics Ax brings us the opportunity to use this comparison framework to compare everything we want.



Let's do it

I will create a BOM Version comparison tool.
I mean ... I want a simple tool that allows me to compare two BOM versions of the same BOM.

There are two "tutorial" classes in the AOT that shows us a little comparison tool sample.

This two classes are "Tutorial_CompareContextProvider" and "Tutorial_Comparable".

The first thing we will do is to duplicate the Tutorial_CompareContextProvider class and we create the MyBOMCompareContextProvider class (Remember to modify the main and construct methods to use the new created class).

This class is the one who manages the user interface part of the comparison framework.

Well, we want to use this tool in this way :
  1. The user selects an item on the inventTable form and press "BOM" button and "Lines" option.
  2. Here we want the user will be able to press a button and select wich of the BOM versions associated with the same Item will be compared.
We need then our class will be called in an Item context ...

We change the main method of our "MyBOMCompareContextProvider" class :

public static void main(Args args)
{
InventTable InventTable;
BOMVersion BOMVersion;
ItemId ItemId;

MyBomCompareContextProvider BomComparer;
;

if (args.dataset() == tablenum(InventTable))
{
InventTable = args.record();
ItemId = InventTable.ItemId;
}
else
if (args.dataset() == tablenum(BOMVersion))
{
BOMVersion = args.record();
ItemID = BOMVersion.ItemId;
}
else
throw error('Parameters needed');


BOMComparer = MyBomCompareContextProvider::construct(ItemID);

SysCompare::startCompareOfContextProvider(BomComparer);
}
Here we're to get the master Item for we must to compare the different available BOM Versions.


As we can see on the code above ... we must to change the construct method too to acquire the master Item :


public static MyBomCompareContextProvider construct( ItemId MasterItem )
{
return new MyBomCompareContextProvider( MasterItem );
}

And consequently ... we must to change the new method too :

void new( ItemId _ItemId)
{
;
ItemId = _ItemId;
}

This means we must add ItemId variable on the ClassDeclaration of MyBOMCompareContextProvider.

Now we must tune some parameters :
We need to show the tree pane to allow the user to navigate through several BOM lines.
public boolean showTreePane()
{
     return true;
}

Now the Case-sensitive parameter :

public boolean parmCaseSensitive( boolean _casesensitive )
{
return false;
}
And all the other parameters (ShowDifferencesOnly, ShowLineNumbers, SuppressWhiteSpaces, SupportDeleteWhenIdentical...) set to false.

What we want to compare ?

Now we must to create a class to manage the elements we want to compare ... It's called the ComparableClass.

If we think about the BOM structure ... it's like a tree structure and this means we need a recursive comparable structure.
OK, but analyzing our case ... we will need two different comparable structures : One for the BOM Versions (BOMVersion Table) and another for the lines (BOM Table)
Both comparable classes will apply on BOM lines, one for Sub-BOM lines and the other for simple lines.



We will duplicate the Tutorial_Comparable class and create the new "MyBOMVersionComparable" class.
Here we add a new method called "parmBOMVersion" in this way :
public BOMVersion parmBOMVersion(BOMVersion _BOMVersion = BOMVersion)
{
    BOM Bom;
    ;
    BOMVersion.data(_BOMVersion);

  
    return BOMVersion;
}


And we create a new static method :
public static MyBOMVersionComparable newBOMVersion(BOMVersion _BOMVersion)
{
    MyBOMVersionComparable BOMVerComp = MyBOMVersionComparable::construct();
;
    BOMVerComp.parmBOMVersion(_BOMVersion);
    return BOMVerComp;
}

Now we modify the ComparableSelected(), Construct(), ComparableName() following same criteria we found on tutorial class.
And this important change on the Name() method :
public str name()
{
  
    return strfmt('%1 BOM Version',BOMVersion.ItemId);
}

We must know our comparison tool will navigate recursively the BOM tree and, when sub-boom appears, we must try to unify the same line "header" on both comparable lines and let the comparison tool will follow the tree comparing those sub-lines.


Now we modify the ComparableTextList method just changing all references to CustTable by BOMVersion (This method is the one who tells to the comparison engine what must to be compared. In our case it's correct to compare all the fields on the BOMVersion Table)

We will go back now to MyBOMCompareContextProvider on method "ComparableList. In this method we enumerate every single object that could be compared (this just fills the combo list that allows to the user to choose wich objects he wants to compare)


public List comparableList(SysCompareType _type)
{
List list = new List(Types::Class);
BOMVersion BOMVersion;


while select BOMVersion where BOMVersion.ItemId == ItemId
{
list.addEnd(MyBOMVersionComparable::newBOMVersion(BOMVersion));
}
return list;
}
Obviously we are looking for every BOMVersion associated with our MasterItem.

Testing our first draft (for impatient souls :-P )


We will create an Action MenuItem associated with our "MyBOMCompareContextProvider" class and with a label like ... "Compare Versions".

And now we add our new menuItem on the BOMConsistOf form (inside the buttongroup "VersionButtonGroup") assigning the BOMVersion Datasource to its datasource property.



We can run our first comparison tool (alpha version) ...




Ok, but ... now we must to compare the lines and sub-boms too

Investigating a little with this classes ... we found a curious method called GetEnumerator().
By it's name ... it seems to be the typical method that is called by someone to navigate trought it's children nodes.

But ... wait !, it returns a SysComparableEnumerator.
mmm and what is a SysComparableEnumerator ? ... it's an interface with the methods : current() and movenext().
Well, I created a new class called MyComparableEnumerator to try to use an standard ListEnumerator as a SysComparableEnumerator :
class MyComparableEnumerator extends listEnumerator implements SysComparableEnumerator
{
}


And now we can use a normal list and it's listenumerator as we want.

We must create the second comparable class wich it will manage the BOM lines. We called it "MyBOMComparable" and it's very similar to "MyBOMVersionComparable" but with some changes regarding the parmBOM() method, and a newBOM static method (you can download the demo project related to this article) and this important changes on ComparableTextList() method :

public List comparableTextList( SysComparable _top,
                                SysCompareContextProvider _context,
                                SysComparable _matchingDummy = null)
{
    str text;
    DictTable dictTable = new DictTable(BOM.TableId);
    DictFieldGroup dictFieldGroup;
    DictField dictField;
    DictField extDictField;
    fieldId   fieldId, extFieldId;
    int i, j, k;
    List list = new List(Types::Record);

    for (i=1; i<=dictTable.fieldGroupCnt(); i++)
    {
        dictFieldGroup = new DictFieldGroup(dictTable.id(), dictTable.fieldGroup(i));
        text = '';
        for (j=1; j<=dictFieldGroup.numberOfFields(); j++)
        {
            fieldId = dictFieldGroup.field(j);

            dictField = new DictField(dictTable.id(), fieldId);
            if (dictField && dictField.id() != fieldnum(BOM, BOMId) && dictField.id() != fieldnum(BOM, linenum) )
            {
                for (k=1; k<=dictField.arraySize(); k++)
                {
                    extFieldId   = Global::fieldId2Ext(fieldId, k);
                    extDictField = new DictField(dictTable.id(), extFieldId);

                    switch (extDictField.baseType())
                    {
                        case Types::Container:
                            break;

                        case Types::String:
                        case Types::VarString:
                            text += strfmt(" %1%2: %3\n", extDictField.label(), strrep(' ', 40-strlen(extDictField.label())), strReplace(BOM.(extFieldId), '\n', ','));
                            break;

                        default:
                            text += strfmt(" %1%2: %3\n", extDictField.label(), strrep(' ', 40-strlen(extDictField.label())), BOM.(extFieldId));
                            break;
                    }
                }
            }
        }
        list.addEnd(SysComparableTmpText::newText(substr(text,1,strlen(text)-1), dictFieldGroup.label(), 0, false, true));
    }
    return list;
}

We've hardcoded some exceptions on this comparison regarding some datafields we already know it will be different between comparable objects (like the bomId or the linenum) but we don't want the system marks as different those objects if only this two fields are differents (because this fields will always be different)


Now we return back to MyBOMVersionComparable...
We want to inform the comparison tool about all the children BOM lines associated with one BOMVersion.
We add a BOMList (type list) variable on the ClassDeclaration and we will fill-in the list for example on the parmBOMVersion method (maybe it's not the best place to do this, but ... this is just a little sample ;) )
public BOMVersion parmBOMVersion(BOMVersion _BOMVersion = BOMVersion)
{
    BOM Bom;
    ;
    BOMVersion.data(_BOMVersion);

    BOMList = new List(Types::Class);
    While Select BOM where BOM.BOMId == BOMVersion.BOMId
    {
     BOMList.addEnd(MyBOMComparable::newBOM(BOM));
    }
return BOMVersion;
}


And now we can implement the GetEnumerator() method :
public SysComparableEnumerator getEnumerator()
{
    MyComparableEnumerator LEnum;

    LEnum = BOMList.getEnumerator();
    return LEnum;
}

We just make this little typecasting trick to return a SysComparableEnumerator from a simple ListEnumerator.

And obviously ... we must take care about the sub-BOM lines.

We return back to MyBOMComparable class and we will :
Add a list to the classdeclaration and we're gonna fill this list on the parmBOM method :

public BOM parmBOM(BOM _BOM = BOM)
{
  BOMVersion BOMVersion;
  InventTable InventTable;
    ;


    BOM.data(_BOM);

    BOMList = new List(Types::Class);
    InventTable = InventTable::find(BOM.ItemId);
    if (InventTable.ItemType == ItemType::BOM)
    {
        if (BOM.ItemBOMId)
        {
            // if BOMVersion designed ... we use it
            BOMVersion = BOMVersion::find(BOM.ItemId, BOM.ItemBOMId, false, systemdateget(), systemdateget(), BOM.BOMQty);
        }
        else
        {
            // else ... we will try to get an apropiated BOMVersion
            BOMVersion = BomVersion::selectBomVersion(BOM.ItemId, systemdateget(), BOM.bomQty, BOM.inventDim());
        }

        if (BOMVersion)
            BOMList.addEnd(MyBOMVersionComparable::newBOMVersion(BOMVersion));
    }

    return BOM;
}


And the same typecasting trick on the getEnumerator() method ...

public SysComparableEnumerator getEnumerator()
{
  MyComparableEnumerator LEnum;

  LEnum = BOMList.getEnumerator();

  if (BOMList.elements())
   return LEnum;
  else
    return null;
}


Finally ...

Let's try it :



Well, it was hard to explain, but here you can download the sample project (xpo) and play, investigate and try it yourself.

NOTE: I removed from the downloadable project the BOMConsistOf form because it's a standard form and it's not a good idea to import existing forms from third parties without being sure what you are doing. BTW you only must to take the menuItem you will find inside the project  and to add to your BOMConsistOf form by yourself (assigning the datasource property to BOMVersion)

Greetz,

Monday, September 21, 2009

Welcome to Dynamics Ax Stuff

Welcome to Dynamics Ax stuff, my name is Manel Querol and I'm the webmaster of www.trucosax.com (the world's biggest Dynamics Ax community in Spanish language).

On July'2009 I was awarded with the Microsoft MVP award on Dynamics Ax and this boosted me to create my first Dynamics Ax blog in english language.

I would make a request to the readers to try to be patience with my english skills, I will try my best to express myself in a comprehensive way.


Currently I'm working as a freelance and I'm very very busy, but I will try to get enough time to frequently write here.

Best Regards,

Manel Querol.