ProfileDynamics AX GeekBlogLists Tools Help
December 14

Finding files with WinAPI

 Axapta’s WinAPI class has a bunch of static methods to handle files. The code example below shows how to utilize some of these methods to find files.

The two methods used to fetch all files matching the search criteria are findFirstFile() and findNextFile(). Don’t forget to clean up after yourself with findClose().

 

The code also uses three different find methods:

  • fileExists(_name) returns true, if _name is an existing file
  • folderExists(_name) returns true, if _name is an existing file or folder
  • pathExists(_name) returns true, if _name is an existing folder

The example uses the infolog for output. As with any infolog beware of performance and limitation to 10.000 lines.

 

static void FindFile(Args _args)

{

    #File

 

    FileName fullFileName(FileName _path, FileName _fileName)

    {

        FileName    pathName;

        FileName    fileName;

        FileName    fileExtension;

        ;

        [pathName,fileName,fileExtension] = fileNameSplit(_fileName);

 

        return _path + '\\' + fileName + fileExtension;

    }

 

    void findFiles(FileName _path,

                   FileName _fileName,

                   boolean _inclSubDir = true,

                   FileName _prefix = fullFileName(_path,_fileName))

    {

        FileName    fileName;

        int         hdl;

        ;

 

        setprefix(_prefix);

 

        if (WinAPI::folderExists(_path))

        {

            [hdl,fileName] = WinApi::findFirstFile(fullFileName(_path,_fileName));

 

            while (fileName)

            {

                if (WinAPI::fileExists(fullFileName(_path,fileName)))

                    info(fileName);

 

                fileName = WinApi::findNextFile(hdl);

            }

 

            WinApi::findClose(hdl);

 

            if (_inclSubDir)

            {

                [hdl, fileName] = WinAPI::findFirstFile(_path+'\\'+#AllFiles);

 

                while (fileName)

                {

                    if (strlwr(fileName) != strlwr(_fileName) &&

                        strlwr(fileName) != strlwr('.')       &&

                        strlwr(fileName) != strlwr('..')      &&

                        WinAPI::pathExists(fullFileName(_path,fileName)))

                        findFiles(fullFileName(_path,fileName), _fileName, _inclSubDir, fileName);

 

                    fileName = WinApi::findNextFile(hdl);

                }

 

                WinApi::findClose(hdl);

            }

        }

 

    }

 

    findFiles('c:\\Program Files','*.doc');

}

December 10

Using query()

As the name implies QueryRun is the executor of a query linked to it. To construct a query you want QueryRun to execute, you need build classes:

  • Query
  •  QueryBuildDataSource
  • QueryBuildRange
  • QueryBuildFieldList
  • QueryBuildLink
  • QueryBuildDynaLink

 

Today’s example will use the first four to demonstrate a query that sums the credit limit field in CustTable grouped by Country and Currency. Additionally a count field indicates how many records are represented in the sum. Ranges are implemented for AccountNum and Country, but the user is allowed to add additional range criteria in the dialog. To demonstrate the status() property I have locked it, so the user can not change it.

 

QueryRun

QueryRun executes the query. If needed the familiar query dialog can be opened before the query is run. This is done using the prompt() method. SysQueryRun extends QueryRun and has a total of 7 prompt*() methods, with different default behavior. For example promptAllowAddRange() would allow the user to add new where-conditions.

 

Query

A query represents the select statement. This is where all the strings come together.

 

QueryBuildDataSource

Using QueryBuildDataSources you add all the tables you want joined (just one in this example). This is also where you define how the resultset is to be sorted. The orderMode() method lets you define

  • OrderBy
  • GroupBy

 

QueryBuildRange

Ranges represent where conditions. Multiple ranges are connected with AND conditions. Unfortunately there is no easy way to change that.

 

QueryBuildFieldList

Represents the selected fields of the query. By default all fields are selected. In regular queries you probably wouldn’t use them that often, but when grouping this is the way to define which fields are calculated. SelectionField is an enum with the following values

  •  Avg
  • Count
  • Database
  • Max
  • Min
  • Sum   

 

The result of Test_Query on my test system looks something like this:

CA CAD     1120,00 (3 records)

CA USD      500,00 (1 records)

DE EUR        0,00 (1 records)

DK CAD     5000,00 (1 records)

DK EUR      300,00 (6 records)

DK GBP      560,00 (2 records)

ES EUR        0,00 (1 records)

IE EUR        0,00 (1 records)

NL EUR       20,00 (2 records)

NO EUR     3000,00 (1 records)

 

 

static void Test_Query(Args _args)

{

    CustTable            custTable;

    Query                query      = new Query();

    QueryRun             qr         = new queryRun(query);

    QueryBuildDataSource qbds       = qr.query().addDataSource(tableNum(CustTable));

    QueryBuildRange      qbrAccN    = qbds.addRange(fieldNum(CustTable,AccountNum));

    QueryBuildRange      qbrCountry = qbds.addRange(fieldNum(CustTable,Country));

    QueryBuildFieldList  qbfl       = qbds.fields();

    ;

 

    qbrAccN.value('4000..4050');

    qbrAccN.status(RangeStatus::Locked);

    qbrCountry.value('CA..NO');

 

    qbfl.addField(fieldNum(CustTable,CreditMax),SelectionField::Sum);

    qbfl.addField(fieldnum(CustTable,RecId),SelectionField::Count);

 

    qbds.addSortField(fieldnum(CustTable,Country));

    qbds.addSortField(fieldNum(CustTable,Currency));

    qbds.orderMode(OrderMode::GroupBy);

 

    if (qr.prompt())

    {

 

 

        while (qr.next())

        {

            custTable = qr.get(tableNum(CustTable));

 

            print strfmt("%1 %2 %3 (%4 records)",custTable.Country,custTable.Currency,

                         num2str(custTable.CreditMax,10,2,0,0),custTable.RecId);

        }

 

    }

 

    pause;

}

December 06

Using lists & listIterators - 8 queens example

I am not using lists a lot, because I find it too restrictive not to be able to move back in the list. Also, I find it annoying that end() moves beyond the last element, not to the last element. Nevertheless, there are some uses for lists and I want to give an example how it can be used.

 

The example is a puzzle: How do you place eight queens on a chessboard, so they can not beat each other?

The algorithm below is a brute force approach to the problem: Find a place for the next queen, place it. If it was the 8th queen, save solution. If the queen could not be placed, undo last move, try next possible field and so forth. The problem can be solved nicely using iteration or recursion. I like recursion.

 

I am using two lists. The first one is for the current solution. All placed queens are placed in this list. It basically is a simple stack. All elements are added to the start of the list. To undo a move, the first element is deleted.

The second list is a list of lists, a list of all solutions that worked out.

To access the contents you need an iterator similar to sets and maps. You can see those in the last block, when solutions are output via the infolog.

  

static void eightQueens(Args _args)

{

    int             board[64];

    int             found;

    str             solution;

    list            listAllSolution = new list(types::Class);

    list            listOneSolution = new list(types::String);

    listIterator    li_OneSolution  = new listIterator(listOneSolution);

    listIterator    li_AllSolution  = new listIterator(listAllSolution);

 

    int board(int _row, int _col, int _upd = 0)

    {

        if (_row < 1 || _row > 8 || _col < 1 || _col > 8)

            return -1;

        else

            board[(_row-1)*8+_col] += _upd;

 

        return board[(_row-1)*8+_col];

    }

 

    void updateBoard(int _row, int _col, int _upd)

    {

        int i;

        ;

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

        {

            board(i,_col,_upd);

            board(_row,i,_upd);

            board(i,_col-_row+i,_upd);

            board(i,_col+_row-i,_upd);

        }

    }

 

    void placeQueen(int _row = 1, int _col = 1)

    {

        int i;

        ;

 

        while (_col <= 8)

        {

            while (_col <= 8 && board(_row, _col))

                _col++;

 

            if (_col <= 8)

            {

                updateBoard(_row,_col,1);

                listOneSolution.addStart(strfmt("%1%2 ",num2char(64+_row),int2str(_col)));

 

                if (_row == 8)

                {

                    listAllSolution.addEnd(List::merge(listOneSolution,new list(Types::String)));

                }

                else

                    placeQueen(_row+1,1);

 

                updateBoard(_row,_col,-1);

                li_OneSolution.begin();

                li_OneSolution.delete();

            }

 

            _col++;

        }

    }

 

    ;

 

    placeQueen();

 

    li_AllSolution.begin();

    while (li_AllSolution.more())

    {

        found++;

        solution = "";

        li_OneSolution = new listIterator(li_AllSolution.value());

        li_OneSolution.begin();

        while (li_OneSolution.more())

        {

            solution = li_OneSolution.value() + " " + solution;

            li_OneSolution.next();

        }

        info(strfmt("%1: %2",found,solution));

        li_AllSolution.next();

    }

 

}

December 04

RecId & Tablebrowser

A table with recId columns (other than its own recId) will not show these in the table browser. Take reqTransCov for example. It has two additional recId columns, linking it to reqTrans: ReceiptRecId and IssueRecId. You will not see those in the table browser. That is because adding them to a grid will always fail. Axapta will pretend it did add them, but in fact it did not. Try it out.

Most of the time it will make sense not to display them as they are internal information and not of interest to the user. However, it might sometimes be helpful to see them, if just for debugging purpose. You can use the display-method workaround to do so:

  1. Add a new display method to the datasource (or table).
  2. Add an intEdit field and link it to the display method.

 

This display method is a (datasource) method to display reqTransCov.receiptRecid:

 

display recid ReceiptRecid(common _record)

{

    return _record.(fieldNum(ReqTransCov, ReceiptRecId));

}   

 

The more general approach would of course be to implement this capability into \Classes\SysTableBrowser and Forms\SysTableBrowser. Maybe I’ll pick that up in a later post…

December 02

Preview Steen's x++ and MorphX book

Steen Andreasen’s book is expected to be released in January. He has uploaded a free preview demo chapter online here. The chapter deals with what most developers dread – reports. Check it out, lots of code samples, illustrations and in depth explanations. Nice.

November 28

Interesting fact about set(Types::Record)

Sets can store data of type record. By definition sets only store unique data, right? So, if you use this construct you should be aware that recId is not taken into account when determining whether a record is already in the set or not.

Now, in some tables it can be perfectly normal to have records whose only difference is the recId. If you need all instances of these records in your memory storage (set) for further processing, you are better of using a different construct like a recordSortedList, recordInsertList or maybe a map.

Take inventTrans for example: run the job below on your test system. If you do get a match of set elements and inventTrans records try this:

Add a sales order line, any item, qty = 2. Go to the inventory transaction, functions/split and make two transactions out of one. Now run the job again.

 

The result in my test company:

214 records in inventTrans

213 records in record set

 

static void SetDuplicates(Args _args)

{

    inventTrans inventTrans;

    set         recordSet = new set(types::Record);

    int         recordCnt = 0;

    ;

 

    while select inventTrans

    {

        recordSet.add(inventTrans);

        recordCnt++;

    }

 

    print strfmt("%1 records in inventTrans",recordCnt);

    print strfmt("%1 records in record set ",recordSet.elements());

    pause;

} 

November 27

Axapta & precision

Brandon reported his frustration about un-Real precision yesterday. And he has a point of course. However, I think the real issue here is not how many digits there are after the decimal sign, it is how many decimals there are overall. Axapta handles a precision of up to 16 digits. This can become an issue when dealing with very large numbers and high precision at the same time (inventory closing for example).

The job below illustrates the point. It job takes two numbers a and b, adds them up and prints the results. For each inner loop another digit is appended to the end.

The first outer iteration takes a starting value of 9, the next 99 and the final iteration starts at 999.

The point is that the calculation will “bum out” at step 15 for starting value 9, at step 14 for 99 and at 13. for 999. In other words it fails as soon as the total number of digits reaches 16.

 

static void realPrecision(Args _args)

{

    int     i,j;

    real    a,b;

 

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

    {

        a = exp10(i) - 1;

        b = exp10(i) - 1;

        for (j=1;j<=16;j++)

        {

             a += (9 / exp10(j));

             b += (9 / exp10(j));

             print strfmt("(%1) a     = %2", num2str(j,2,0,0,0),num2str(a,25,16,1,1));

             print strfmt("(%1) b     = %2", num2str(j,2,0,0,0),num2str(b,25,16,1,1));

             print strfmt("(%1) a + b = %2", num2str(j,2,0,0,0),num2str(a+b,25,16,1,1));

            

             if (j mod 4 == 0)

                pause;

        }

    }

 

}

   

November 26

Manipulating bits

Axapta and x++ implements all the operators you need to manipulate data on a bit level. Not that you’d need it a lot these days, but it can be helpful to implement a set of flags for example. The standard application also makes occasional use of it, too (see InventDimFixedClass).

 

The bit operators are

<<        shift left (multiply with 2)

>>        shift right (divide by 2)

&          binary and

|           binary or

^           binary xor

~          binary inversion

 

Take a look at the job for a few examples on how they are used.

 

static void Bits(Args _args)

{

    int     mask;

 

    int setBit(int _bitMask,int _bit)

    {

        return _bitMask | (1 << _bit);

    }

 

    int killBit(int _bitMask,int _bit)

    {

        return _bitMask & ~(1 << _bit);

    }

 

    boolean bit(int _bitMask, int _bit)

    {

        return setBit(0,_bit) & _bitMask;

    }

 

    int flipBit(int _bitMask, int _bit)

    {

        return bit(_bitMask,_bit) ? killBit(_bitMask, _bit) : setBit(_bitMask, _bit);

    }

 

    int invertMask(int _bitMask)

    {

        return ~_bitmask;

    }

 

    int xorMask(int _bitMask1, int _bitMask2)

    {

        return _bitMask1 ^ _bitMask2;

    }

 

    str bitString(int _bitMask)

    {

        str bS;

        int i;

 

        for (i=31;i>=0;i--)

            bS += bit(_bitMask,i) ?  "1" : "0";

 

        return bS;

    }

    ;

 

    mask = setBit(mask,4);

    print bitString(mask);

 

    mask = flipBit(mask,7);

    print bitString(mask);

 

    mask = killBit(mask,7);

    print bitString(mask);

 

    mask = invertMask(mask);

    print bitString(mask);

 

    mask = xorMask(mask,255);

    print bitString(mask);

 

    pause;

} 

November 24

#InventDimJoin

If you’ve developed anything in the supply chain area, you’ve most probably come across InventDimJoin. InvenDimJoin is a macro and it’s mostly used to narrow down search results for inventory transactions (inventTrans) or inventory postings (inventTransPost), but can be used on any table having a inventDimId field.  

 

The macro accepts four to five parameters

·         InventDimId

·         InventDim

·         InventDimCriteria

·         InventDimParm

·         Index hint (optional)

 

You must use it as a join with a select-statement (hence the name) and it returns no contents from inventDim. It is not an exist join, but doesn’t return any useful fields, so it probably should be an exist-join. Anyway, let’s take the parameters one by one:

 

InventDimId

This is the unique key to the inventory dimension table. This is where you supply the inventDimId field from the table you are joining.

 

InventDim

Any InventDim table buffer. This is the table buffer used in the joined select. The contents of the buffer has no influence on the select result set and resulting records will only have the tableId field filled (but that’s a constant anyway and already filled by just defining the buffer, so really you get nothing).

 

InventDimCriteria

This is also a inventDim buffer, but the contents of this one matters. Using this buffer, you define which dimensions you are looking for exactly (warehouse ‘Main’, location ‘IN-1’, batch ‘241105’ etc.).

 

InventDimParm

InventDimParm is a temporary table. A record basically consists of nothing but a number of flags to indicate which inventory dimensions are important and which ones can be disregarded. There is a *Flag field for every inventory dimension field. InventDimParm has a variety of methods to initialize these flags. An example would be initPhysicalInvent(). It clears all flags (=No) and sets only those flags to yes whose corresponding inventory dimension is a physical dimension (for the dimension group id you pass along as a parameter).

Consequently you provide InventDimJoin with a InventDimParm buffer. For all flags with value ‘No’ the contents of the corresponding inventDimParm field does not matter.

 

Index hint

This is an optional parameter to help you optimize performance.

 

Here's an example:

 

static void InventDimJoinTest(Args _args)

{

    SalesLine       salesLine;

    InventDim       inventDim;

    InventDim       inventDimCriteria;

    InventDimParm   inventDimParm;

    ;

 

    InventDimCriteria.InventLocationId = 'Main';

    InventDimCriteria.wMSLocationId    = 'IN-1';

    InventDimCriteria.configId         = 'Red';

 

    inventDimParm.clear();

    inventDimParm.InventLocationIdFlag = NoYes::Yes;

    inventDimParm.wmsLocationIdFlag    = NoYes::No;

    inventDimParm.ConfigIdFlag         = NoYes::Yes;

 

    while select salesLine

        #InventDimJoin(salesLine.inventDimId, InventDim, inventDimCriteria, InventDimParm, dimIdx)

    {

        print strfmt("%1 %2 %3 %4 %5",salesLine.InventDimId, salesLine.SalesId, salesLine.ItemId,

                     inventDim.InventLocationId, salesLine.inventDim().InventLocationId);

    }

 

    pause;

}

 

The job finds all salesLines for the ‘Main’ warehouse and configuration ‘Red’. The location doesn’t matter, since it’s inventDimParm flag is turned off (additional dimensions like batch, serial etc.wouldn’t make a difference either, clear() takes care of that).

Note that %4 prints the inventDim.inventLocationId. I just put that in there to make the point that it will always be blank.

dimIdx is the optional parameter. It’s an index defined on the InventDim table. You will notice in the output the result is sorted by inventDimId.

 

 

November 23

Executing external x++ code

runbuf() executes x++ code passed to it. The code must be defined as a function and can return a value. Parameters passed to runbuf() will be forwarded, but default parameters won’t work. To show how it works, I am going to use this function to execute code read from an external file. Not very useful and you probably wouldn’t want to allow it, it’s just to show that it can be done (easily).

 

static void ExecuteCodeFromFile(Args _args)

{

    #File

    AsciiIo     asciiIo = new AsciiIo("c:\\temp\\findCustomer.xpp",#io_read);

    XppCompiler xppCompiler = new XppCompiler();

    Source      source;

    str         line;

    CustTable   custTable;

    ;

 

    if (asciiIo)

    {

        asciiIo.inFieldDelimiter(#delimiterEnter);

        [line] = asciiIo.read();

 

        while (asciiIo.status() == IO_Status::Ok)

        {

            source += #delimiterEnter;

            source += line;

            [line]  = asciiIo.read();

        }

 

        if (!xppCompiler.compile(source))

            error (xppCompiler.errorText());

 

        custTable = runbuf(source,'4000');

        print CustTable.Name;

    }

    else

        print "Could not open file";

 

    pause;

}

 

The external file c:\temp\findCustomer.xpp:

 

CustTable findCustomer(CustAccount _accountNum)

{

    return CustTable::find(_accountNum);

}

 

First the file c:\temp\findCustomer.xpp is read into source. Source is then compiled and if that goes okay it is executed. As you can see ‘4000’ is passed as a parameter simply by adding it to the runbuf() call. You can also see runbuf returns the function’s return value.

 

I had trouble getting code compiled that I had written using notepad. As it turns out, the compiler does not accept the tab character. So if you are going to try this out, watch out for that.

Productivity tip: classes EditorScripts & xppSource

Did you ever notice that little service icon in the Axapta source editor? When you open it, it reveals some tools like commenting a source-block or inserting a template. That can for example be a while() loop or an if() block.

It is actually surprisingly easy to hook your own little scripts up to this menu.

All you need is a method in Classes\EditorScripts. comments_insertHeader() is one of the standard methods, but you can add your own methods. Use the underscore to define the menu path.

Having defined a method you need to add some task. The example I will show here is adding a block to iterate a map. I will add the template to \Classes\xppSource and call it from my EditorScripts method. And that's it. You are ready to use your new tool.

 

This is the template we will build:

 

    miMap = new mapIterator(map);

    miMap.begin();

    while (miMap.more())

    {

        miMap.next()

    }

 

..and this is how we do it:

 

\Classes\EditorScripts

void template_flow_iterateMap(Editor editor)

{

    xppSource xppSource = new xppSource(editor.columnNo());

    ;

    editor.insertLines(xppSource.iterateMap());

}

 

\Classes\xppSource

Source iterateMap(Source _mapName = 'map')

{

    str miMapName = "mi"+StrUpr(substr(_mapName,1,1))+substr(_mapName,2,strlen(_mapName));

    ;

 

    source += strfmt("%1 = new mapIterator(%2);",miMapName,_mapName);

    source += '\n';

    source += this.indent();

    source += strfmt("%1.begin();",miMapName);

    source += '\n';

    source += this.indent();

    this.while(strfmt("%1.more()",miMapName),strfmt("%1.next()",miMapName));

 

    return source;

}

 

Now obvisouly there’s some room for improvement here. We could search the code for defined maps / mapIterator and use those etc. This is just to show how it works in general.

 

November 21

Useful methods for sets / record2set example

I find myself working with sets and maps a lot. They are a fast and easy way to buffer information without going back to the database all the time.

The set object has a few nice methods to compare sets.

 

Union

Pass two sets to this method. The method returns a new set that includes all items from either set.

 

Difference

Pass two sets to this method. The return set includes all items from the first set that are not found in the second set.

 

Intersection

Pass two sets to this method. The method return set includes all items found in both sets.

 

The following example illustrates the three methods. record2Set creates a set out of any table record (for simplicity arrays are excluded here). The format is fieldname#value The method is applied to two customer records. Check out the results you are getting: 

  • Union proves not very useful in this example, it has all the fields, and two entries if values are different.
  • Difference is interesting. These are the differences in the two records. Note that the values are from whichever set you supplied first.
  • Intersection. All identical fields. Differences are dropped altogether.

 static void JobSetExamples(Args _args)

{

    custTable   custTable_1;

    custTable   custTable_2;

    Set         setCustTable_1;

    Set         setCustTable_2;

    Set         compare;

 

    set record2set(common _record)

    {

        DictTable       dictTable;

        DictField       dictField;

        FieldId         fieldId;

        Set             recordSet;

        ;

 

        dictTable = new DictTable(_record.tableId);

        recordSet = new Set(Types::String);

 

        for (fieldId = dictTable.fieldNext(0);fieldId;fieldId = dictTable.fieldNext(fieldId))

        {

            dictField = dictTable.fieldObject(fieldId);

            if (!dictField.isSystem() && dictField.arraySize() == 1)

                recordSet.add(strfmt("%1#%2",dictField.name(),_record.(fieldId)));

        }

 

        return recordSet;

    }

    ;

 

    custTable_1            = CustTable::find('4000');

    custTable_2            = CustTable::find('4000');

    custTable_1.AccountNum = "1234";

    custTable_1.Name       = "Test";

    custTable_2.CreditMax  = 333.33;

 

    setCustTable_1 = record2set(custTable_1);

    setCustTable_2 = record2set(custTable_2);

 

    compare = Set::difference(setCustTable_1,setCustTable_2);

    print compare.toString();

 

    compare = Set::difference(setCustTable_2,setCustTable_1);

    print compare.toString();

 

    compare = Set::intersection(setCustTable_1,setCustTable_2);

    print compare.toString();

 

    compare = Set::union(setCustTable_1,setCustTable_2);

    print compare.toString();

 

    pause;

}

November 20

Storing objects in a container

Axapta containers have a severe limitation: They don’t accept objects. Or do they? Well, not directly, but there is a way to do it: pack the contents into a container. Containers can store containers, so that will work.

 

Let’s take an easy example first:

 

static void Container1(Args _args)

{

   container   con, conSet;

    Set        set       = new Set(Types::String);

    int          intValue  = 42;

    real        realValue = 1.61803;

    ;

 

    set.add("John");

    set.add("Paul");

    set.add("George");

    set.add("Ringo");

    print set.toString();

 

    con = [set.pack(),intValue,realValue];

 

    // do some processing, call methods using the container etc.

 

    [conSet,intValue,realValue] = con;

    set = Set::create(conSet);

    set.add("Yoko Ono");

 

    print set.toString();

    pause;

}

 

Now this was easy, because the set is a basic data type string and sets have a pack method. What if it was a real object? Well, we would have to implement the SysPackable interface. The interface only two methods: pack() and unpack(). For the next example I am going to create a new class TestPackable.

I am going to get the class wizard to do most of the work for me.

 

1. Go to Tools / Development Tools / Wizards / Class Wizard.

2. Choose a name for the class, for example TestPackable, select class template “Normal”, no inheritance. Click Next.

3. From the list of interfaces select SysPackable. Click Next.

4. Select “Create all interface methods”, “Create all abstract methods”. Click Next.

5. Click Finish.

 

That’s it. Now all we have to do is adding some functionality.

 

void new(int _intValue, real _realValue, Set _set)

{

    intValue  = _intValue;

    realValue = _realValue;

    set       = _set;

}

 

str toString()

{

    return strfmt("%1 %2 %3",intValue,realValue,set ? set.toString() : "");

}

 

public boolean unpack(container _packedClass)

{

    container   setContainer;

    int         version = runbase::getVersion(_packedClass);

 

    switch (version)

    {

        case #CurrentVersion:

            [version,#CurrentList] = _packedClass;

            set = Set::create(setContainer);

            return true;

        default :

            return false;

    }

 

    return false;

}

 

public container pack()

{

    container   setContainer = set.pack();

    ;

 

    return [#CurrentVersion,#CurrentList];

}

 

public class TestPackable implements SysPackable

{

    #define.CurrentVersion(1)

    #localmacro.CurrentList

        intValue,

        realValue,

        setContainer

    #endmacro

 

    int     intValue;

    real    realValue;

    Set     set;

}

 

I am using the macro #CurrentList to define which instance variables to put in the container (the foundation was already generated by the wizard). I have also added a simple toString() method, so we can check the results are as expected. The following job is an example of how pack/unpack can be used:

 

static void Container2(Args _args)

{

    container       con;

    Set             set1       = new Set(Types::String);

    Set             set2       = new Set(Types::String);

    int             intValue  = 42;

    real            realValue = 1.61803;

    TestPackable    testPackable1, testPackable2;

    ;

 

    set1.add("John");

    set1.add("Paul");

    set1.add("George");

    set1.add("Ringo");

    set2.add("Bjorn");

    set2.add("Benny");

    set2.add("Frida");

    set2.add("Agnetha");

 

    testPackable1 = new TestPackable(intValue,realValue,set1);

    print testPackable1.toString();

 

    testPackable2 = new TestPackable(intValue,realValue,set2);

    print testPackable2.toString();

 

    con = [testPackable1.pack(),testPackable2.pack()];

 

    // do some processing, call methods using the container etc.

 

    testPackable1 = new TestPackable(0,0,null);

    testPackable2 = new TestPackable(0,0,null);

 

    testPackable1.unpack(conpeek(con,1));

    testPackable2.unpack(conpeek(con,2));

 

    print testPackable1.toString();

    print testPackable2.toString();

 

    pause;

}

 

The job creates two instances of TestPackable, puts them both into a container, retrieves them again and displays the contents for verification.

SysPackable is implemented by some standard classes as well, Classes\Dialog is one of them.

November 19

cross-references & find

One of the cool things in Axapta is that you can read and modify the actual source code (provided you have the source code license, of course). When working with a standard object you often need to know where and how it is used. This is where two essential tools come in: cross-references and the find-dialog.

 

Cross-References 

Cross references answer questions like "Where is this object used?" and "What objects is it using?". You access it by right clicking any object in the AOT, Add-Ins/Cross-Reference.

The key is to keep the references updated. The initial references are built during the installation (installation check list / update cross-reference). It is up to you to keep it updated when you later change or add objects to the AOT. You can do that periodically by right clicking the top AOT node, Add-Ins / Cross-reference / Update or you can have the system update the reference whenever you compile some code. To do that go to Menu Tools / Options / Button Compiler and activate the Cross-reference checkbox.

 

Find Dialog 

The find dialog is more powerful than you might think. The search text is actually a regular expression. The syntax for regular expressions in Axapta can be found under System Documentation / Functions / match. It is not quite as powerful as Unix users might be used to, but it’ll do.

The find dialog allows you to search not only methods, but “All  nodes”.

Properties for example can be found easily, if you know they are stored as text as Property blank(s)#Value.

 

Find all temp. tables: Go to Data Dictionary\Tables find “All nodes” containing text

 

Temporary *#Yes

 

Ever wondered why searching for static method calls or enum types didn’t return any results? Colons are treated as special characters in regular expressions, so to find them you have to escape them with a backslash

 

            InventDirection\:\:Receipt

 

finds all occurrences of InventDirection::Receipt in the selected scope.

 

The dialog even let’s you define your own filter code. Your code is actually compiled to a method called FilterMethod(). The method has four parameters:

 

  • str _treeNodeName
  • str treeNodeSource
  • XRefPath _path
  • ClassRunMode _runMode

 

You can define any filter you want, your source simply needs to return a boolean to indicate found / not found. For example to find all insert() methods that do not call super():

Go to Data Dictionary / Tables find methods named insert, then copy this into the source field:

 

TextBuffer tb = new textbuffer();

tb.setText(_treeNodeSource);

return !tb.find("super()");

 

The method is also useful to match or-conditions.   

 

TextBuffer tb = new textbuffer();

tb.setText(_treeNodeSource);

return tb.find("insert()") || tb.find("delete()");

 

Execute this find on the classes node to return all methods that call insert() or delete().

November 18

Microsoft Shuffles the Deck Chairs at MBS

Microsoft Shuffles the Deck Chairs at MBS. Mary Jo Foley at Microsoft Watch reports, Microsoft Business Solutions SVP Doug Burgum will move into a new chairman role next spring, where he will be charged with more evangelism and long-range thinking about Microsoft's applications business. Microsoft is hoping to have a new MBS senior vice president in place by Spring 2006.

November 17

Recursion in Axapta

Axapta can handle recursive method calls and you can see the standard application making use of it in some places, for example in \Classes\ReqTransFormExplosion\treeBuildNode. But just deep can you go before it bugs out?

 

Let’s take a classic example of recursive programming: Towers of Hanoi.

 

The Tower of Hanoi puzzle was invented by the French mathematician Edouard Lucas in 1883. We are given three pegs and a tower of n disks, initially stacked in increasing size on one of three pegs. The objective is to transfer the entire tower to one of the other pegs, moving only one disk at a time and never a larger one onto a smaller.

 

The code to solve this puzzle is minimal, very intuitive – and recursive.

 

static void TowersOfHanoi(Args _args)

{

    void move(int _n,str _from, str _to, str _transfer)

    {

        if (_n > 1)

        {

            move(_n-1,_from,_transfer,_to);

            move(1,_from,_to,_transfer);

            move(_n-1,_transfer,_to,_from);

        }

        else

            print strfmt("%1 -> %2",_from, _to);

    }

    ;

 

    move(3,"Peg #1","Peg #3","Peg #2");

    pause;

}

 

The parameters instruct the function to move 3 discs from peg #1 to peg #3 and use peg #2 as a buffer.

The solution is to move n-1 discs to the buffer, the last (biggest) disc to the destination and then move the rest from the buffer to the destination. How do you move the rest from the buffer to the destination? Well, you move n-1 discs… you get the point.

 

Back to original question: How far can you go before it crashes? The answer is 400. Try it out. Change _n from 3 to 400 and run the job. No problem. It will take a long time to finish, so unless you really need to know how to move 400 discs, I suggest you interrupt with Ctrl+Break :-).

Now change _n to 401. It crashes almost instantly.

Each call to move adds one level of recursion (if _n > 1), so that’s 399 + 1 for the initial call = 400.

 

Btw: Don’t use info() for the output. You would run into a whole other limitation with the infolog.

November 16

Deleting all objects from a custom layer

An entire layer can safely be deleted by removing these two files:

 

ax<layer>.aod

aod stands for “application object data”. This file holds all objects for the layer it is named after. For example, axusr.aod for the usr-layer, axcus.aod for the cus-layer etc.

 

axapd.aoi

aoi stands for “application object index”. It tells Axapta where to find each individual object. The file will be rebuild the next time you log on to Axapta.

 

Note: Deleting these will not delete any labels. Labels are stored in files using this naming convention:

 

ax<layer><language>.ald

Application label data

 

ax<layer><language>.alc

application label comments

 

ax<layer><language>.ali

application label index

 

If you delete entire layers frequently and are prefer to automate repetitive tasks, you can easily create a batch file to do the job for you.

 

move /Y "F:\Program Files\Axapta30_SP3\Application\Appl\Standard\axusr.aod" "F:\Program Files\Axapta30_SP3\Application\Appl\Standard\axusr.aod_bak"

move /Y "F:\Program Files\Axapta30_SP3\Application\Appl\Standard\axapd.aoi" "F:\Program Files\Axapta30_SP3\Application\Appl\Standard\axapd.aoi_bak"

 

This example has the added benefit of moving the file to *_bak instead of deleting it outright. If for some reason I need the old version back I can restore it by simply moving them back to their original names.

 

It goes without saying that no user should be logged into Axapta when you delete or move these files. 

November 15

Working with multiple configurations? Try -regconfig

When working with multiple configurations, using the configuration utility to select the current config before logging on can get tiresome.

You might use different configurations if you have more than one Axapta installation or each configuration has a different default company, for example.

 

There’s an easy way around it: For each of your configurations create a shortcut to the appropriate ax32.exe, and then change the shortcut properties (target) to include the -regconfig parameter.

 

"F:\Program Files\Axapta30_SP3\Client\Bin\ax32.exe" "-regconfig=Axapta 3.0 SP3 Fat Client"

 

That's what the target for one of my SP3 installations looks like.

Now all you have to do to get into the right installation is double-clicking the corresponding shortcut. No more AxConfig :-).

 

 

November 14

SQLSYSTEMVARIABLES

When restoring an Axapta database from a different server, you might have experienced the following error message:

 

[Microsoft][ODBC Database Server Driver][SQL Server] Invalid object name ‘SQLSYSTEMVARIABLES’.

 

The problem is your database user (default bmssa) can not access the SQLSYSTEMVARIABLES table. If the original and new SQL server use the same user id, the following query analyzer command should fix it:

 

exec sp_change_users_login 'update_one','bmssa','bmssa'

 

If that doesn’t help or user ids are different, try changing the database and object owners.

           

            exec sp_changedbowner 'bmssa'

 

changes the owner of the database. I have used this script to change the owner of tables.

 

DECLARE @oldOwner sysname, @newOwner sysname, @sql varchar(1000)

 

SELECT

  @oldOwner = 'AX30'

  , @newOwner = 'bmssa'

  , @sql = '

  IF EXISTS (SELECT NULL FROM INFORMATION_SCHEMA.TABLES

  WHERE

      QUOTENAME(TABLE_SCHEMA)+''.''+QUOTENAME(TABLE_NAME) = ''?''

      AND TABLE_SCHEMA = ''' + @oldOwner + '''

  )

  EXECUTE sp_changeobjectowner ''?'', ''' + @newOwner + ''''

 

EXECUTE sp_MSforeachtable @sql

 

 

Hi Axapta Fans!

Welcome to my blog! I will post links, code snippets, tips and tricks and Axapta news, ... whatever I feel worth sharing with the AX community.

Please feel free to add your comments and ideas. Thanks!

Check out the linked websites and blogs. There's a lot of great information out there.