Monday, May 31, 2010

throw "mischief"

Exception handling in AX is actually very simple. You have a bundle predefined exception codes (info, warning, error, CLRError, and so on) and that it is. As much more I was astonished, when a coworker shows me the follow code, saying the exception would be thrown, but the error message was never displayed:

throw strfmt("Error in %1", funcname());


The right X++ code was then:

throw error(strfmt("Error in %1", funcname()));


While the typical argument for throw is an enumeration of type Exception (), actually the throw instruction accepts any in AX existing type.

After some tests I get the follow insights:


  • The Kernel obviously tries to convert the given argument into an integer and then to procced the conversion:


    F.ex. this X++ code

    throw "3";
    


    raises an exception with the type error. Ditto

    throw "3.7";
    

  • More complex types like objects and tables (except a not initialized CLR-Object) raises exception codes above value 255. Tough only exceptions between 0 and 255 can be handled in a different way, an exception above 255 can be catched with a general catch block.
  • try
    {
        throw 230;
    }
    catch (230)
    {
        info("This works fine.")
    }
    
    
    try
    {
        throw 256;
    }
    catch (256)
    {
        // this line will never reached
    }
    catch
    {
        info("That's the way it is!")
    }
    

Of course you may think this statement gives the ability to use self defined exception codes. I advice against this idea; in further versions of AX, the self defined codes could be used by the Kernel itself and brings unexpected side effects like continue database transactions (in fact, some exception codes include this feature already).

Friday, May 28, 2010

throw "dummfug"

Die Ausnahmebehandlung in AX ist ja im Prinzip sehr einfach gehalten. Es gibt eine vordefinierte Anzahl verschiedener Ausnahme-Codes (Info, Warning, Error, CLRError etc.) und damit hat es sich. Recht erstaunt war ich, als eine Arbeitskollegin mir folgenden Code zeigte, mit der Bemerkung dass wohl eine Ausnahme ausgelöst, aber die Feldermeldung dazu niemals angezeigt würde:

throw strfmt("Fehler in %1", funcname());


Der korrekte X++ Code lautete schliesslich:

throw error(strfmt("Fehler in %1", funcname()));


Während typischerweise ein Element der Enumeration vom Typ Exception (also im Prinzip eine Ganzzahl) an throw übergeben wird, akzeptiert diese Anweisung aber in Wirklichkeit jeden in AX vorhandenen Typ.

Nach ein paar Tests lässt sich folgende Aussagen machen:


  • Der Kernel versucht offensichtlich das Argument in eine Ganzzahl umzuwandeln und dann zu interpretieren:


    Der Code

    throw "3";
    


    löst beispielsweise eine Ausnahme vom Typ Error aus. Ebenso

    throw "3.7";
    

  • Komplexere Typen wie Objekte und Tabellen (mit Ausnahme von nicht initialisierten CLR-Objekten) lösen einen Ausnahmecode grösser 255 aus. Obwohl nur Ausnahmen zwischen 0 und 255 differenziert behandelt werden können, kann eine Ausnahme mit einem höheren Wert als 255 durch einen allgemeinen Catch-Block trotzdem abgefangen werden.
  • try
    {
        throw 230;
    }
    catch (230)
    {
        info("This works fine.")
    }
    
    
    try
    {
        throw 256;
    }
    catch (256)
    {
        // this line will never reached
    }
    catch
    {
        info("That's the way it is!")
    }
    

Nun könnte diese Feststellung natürlich dazu verleiten, selbst definierte Ausnahmecodes einzuführen und zu verwenden. Davon würde ich allerdings abraten; in kommenden Versionen könnten diese durch den Kernel definiert werden und spezielles Verhalten an den Tag legen, wie z.B. das weiterführen von Datenbranktransaktionen, was zur Zeit bei einigen Ausnahmen ja bereits der Fall ist.

Wednesday, May 26, 2010

ForUpdate record after record insert

Every time, if you save a record into database using method insert() or doInsert(), you're record handle is selected forUpdate. That means, that you can do data manipulation with update() or doUpdate() without reread the current record.

But to do this you can run into trouble. If you have not a database transaction outside the insertation and manipulation, other users can modify your record as well. And as soon you want to do your update() or doUpdate() you get an exception:

Cannot edit a record in Table (Tablename).
An update conflict occurred due to another user process deleting the record or changing one or more fields in the record.

Therefore avoid such code:

Table table;
;
table.KeyField = 'a';
table.insert();

table.AdditionalField = 'halli galli';
table.update();

Do it that way instead:

Table table;
;
ttsbegin;
table.KeyField = 'a';
table.insert();

table.AdditionalField = 'halli galli';
table.update();
ttscommit;

Or even this way:

Table table;
;
table.KeyField = 'a';
table.insert();

// time consuming other code here

ttsbegin;
table.selectForUpdate(true);
table.reread();
table.AdditionalField = 'halli galli';
table.update();
ttscommit;

Depending on requirements you may need a database transaction over all code or not.

ForUpdate-Datensatz nach dem Einfügen eines Datensatzes

Jedes mal, wenn ein Datensatz mit der insert()- oder der doInsert()-Methode auf der Datenbank erzeugt wird, erhält man den neu erzeugten Datensatz mit dem Prädikat forupdate. Das heisst im Prinzip, dass ich den Datensatz beliebig mit update() oder doUpdate() ändern kann ohne ich vorher von der Datenbank neu lesen zu müssen.

Allerdings gibt es oft Probleme, wenn das ganze nicht in einer Datenbanktransaktion gehalten wird: U.u. wird der Datensatz nämlich in der Zwischenzeit verändert und das führt zu einer Ausnahme sobald update() oder doUpdate() die Änderung in die Datenbank schreiben soll:

Ein Datensatz in Debitoren (Tabellenname) kann nicht bearbeitet werden.
Aktualisierungskonflikt, weil ein anderer Benutzerprozess den Datensatz gelöscht oder mindestens ein Feld im Datensatz geändert hat.

Deshalb sollte man solchen Code vermeiden:

Table table;
;
table.KeyField = 'a';
table.insert();

table.AdditionalField = 'halli galli';
table.update();

Stattdessen sollte man ihn entweder so schreiben:

Table table;
;
ttsbegin;
table.KeyField = 'a';
table.insert();

table.AdditionalField = 'halli galli';
table.update();
ttscommit;

Oder so:

Table table;
;
table.KeyField = 'a';
table.insert();

// time consuming other code here

ttsbegin;
table.selectForUpdate(true);
table.reread();
table.AdditionalField = 'halli galli';
table.update();
ttscommit;

Je nach Anforderung ob der Datensatz in der Zwischenzeit verändert werden darf braucht es eine alles umfassende Datenbanktransaktion oder eben nicht.

Sunday, May 2, 2010

Decode CLR Exception message

The ability to use .NET code inside X++ source code is a neat feature. If the native code brings limitations, you may can solve it with .NET code. Unfortunately the .NET framework exception messages in AX are very superficially.

The following static method can be used to decode a .NET exception:

static void cRLExtendException(System.Exception _exception)
{
    System.Exception exception = _exception;
    SysInfoLogStr infoLogStr;
    ;
    if (exception)
    {
        infoLogStr = exception.get_Message();
        if (exception.Equals(exception.GetBaseException()))
        {
            // the most inner exception has reached, now we can write the infolog message and throw the exception
            error(infoLogStr);
            throw Exception::CLRError;
        }
        else
        {
            // the current exception is not the most inner exception, so we just set a infolog prefix
            setprefix(infoLogStr);
            MyClass::cRLExtendException(exception.get_InnerException());
        }

    }
    /* else
    {
        well, there was no CLR exception, so just left out
    }
    */
}

And that's how to use it:

System.String[] files;
;

try
{
    files = System.IO.Directory::GetFiles('c:\\halliGalli');
}
catch
{
    MyClass::cRLExtendException(CLRInterop::getLastException());
}
Now your infolog window will look like this:


In this case the auxiliary method is hosted on a class named 'MyClass'. Where the method should be implemented on your environment has to be decided by you or your team.