Monday, December 20, 2010

WinAPI::getTempFilename() im .NET Business Connector auf einer x64 Maschine

Ein Aufruf auf die externe Methode GetTempFileNameW auf Kernel32 verursacht eine Ausnahme, wenn sie im .NET Business Connector verwendet wird und dieser auf einem x64-System ausgeführt wird.

Das Problem betrifft zumindest AX 2009 (vielleicht AX 4.0 und AX 3.0 ebenfalls).

Als Workaround kann man folgenden Code verwenden:

fileName = System.IO.Path::GetTempFileName(); // returns a filename in the temporary directory

oder

fileName = CLRInterop::getAnytypeForObject(System.IO.Path::GetTempPath() )+ curuserid() + int642str(CLRInterop::getAnyTypeForObject(netTime.get_Ticks());


Egal wie man es löst, natürlich sollte man eine temporäre Datei nach Verwendung auch immer wieder löschen.

Friday, December 10, 2010

RLS in display methods on reports (AX 4.0)

X++ select statements do ignore Record Level Security (RLS) by default. With the property  recordLevelSecurity the behaviour to use Record Level Security can be modified. The property can also be used to retrieve the current behaviour.

But there is a difference to select statements inside display methods. Every select statement has activated Record Level Security if the command is in a scope inside a call stack with a display method root. But the current status is not really available trough the property recordLevelsSecurity. A call to this propery will return false, make you thinking Record Level Security is off - but it is not.

display showEmplName()
{
    EmplTable emplTable;
    EmplName name;
    ;
    info(strfmt("%1", emplTable.recordLevelSecurity())); // this will return false, but actually it's true!

    select firstonly Name from emplTable 
        where emplTable.EmplId == reportTable.emplId; // the record will use RLS (yeah, it's magic)

name = emplTable.Name;

    return name;
}

Even here the Record Level Security is on, without explicit change to recordLevelSecurity property:
display showEmplName()
{
    EmplName name;
    ;
    name = EmplTable::find(reportTable.EmplId); // the select inside the find method will use RLS even you haven't activate this

    return name;
}


If you want to ensure, that Record Level Security is really off, you need to touch the property explicit.
display showEmplName()
{
    EmplTable emplTable;
    EmplName name;
    ;
    emplTable.recordLevelSecurity(false); // this will really free from RLS   

    select firstonly Name from emplTable 
        where emplTable.EmplId == reportTable.emplId;

    name = emplTable.Name;

    return name;
}


This Problem appears in AX 4.0. In AX 2009 this phenomen does not exists; Record Level Security stays off until you change it explicit.

RLS in Displaymethoden auf Berichten (AX 4.0)

X++ Select-Statements sind per Standard so eingestellt, dass sie die Sicherheit auf Datensatzebene (RLS) ignorieren. Über die Eigenschaft recordLevelSecurity kann aber dieses Feature ein- oder ausgeschaltet werden. Die Eigenschaft kann aber auch dazu verwendet werden, den aktuellen Status des Features zu ermitteln.

Select-Anweisungen innerhalb von Display-Methoden verhalten sich jedoch ein wenig anders. Und zwar ist für jede Select-Anweisung die Sicherheit auf Datensatzebene aktiviert, welche aus dem Aufrufstapel einer Display-Methode stammt. Allerdings ist über die Abfrage der Eigenschaft recordLevelSecurity nicht ermittelbar ob das Feature nun aktiviert ist oder nicht. Ein Aufruf von recordLevelSecurity gibt in einer solchen Situation stets false zurück was vermeintlicherweise zu der Annahme führen könnte, dass die Sicherheit auf Datensatzebene nicht aktiv ist obwohl sie das ist.

display showEmplName()
{
    EmplTable emplTable;
    EmplName name;
    ;
    info(strfmt("%1", emplTable.recordLevelSecurity())); // this will return false, but actually it's true!

    select firstonly Name from emplTable 
        where emplTable.EmplId == reportTable.emplId; // the record will use RLS (yeah, it's magic)

name = emplTable.Name;

    return name;
}

Auch so ist die Sicherheit auf Datensatzebene aktiv - ohne dass eine explizite Änderung der Eigschaft recordLevelSecurity erfolgt:
display showEmplName()
{
    EmplName name;
    ;
    name = EmplTable::find(reportTable.EmplId); // the select inside the find method will use RLS even you haven't activate this

    return name;
}


Will man nun sicherstellen, dass die Sicherheit auf Datensatzebene wirklich deaktiviert ist, muss man explizit die Eigenschaft nocheinmal setzen.

display showEmplName()
{
    EmplTable emplTable;
    EmplName name;
    ;
    emplTable.recordLevelSecurity(false); // this will really free from RLS   

    select firstonly Name from emplTable 
        where emplTable.EmplId == reportTable.emplId;

    name = emplTable.Name;

    return name;
}


Dieses Problem betrifft AX 4.0. In AX 2009 tritt das Phänomen nicht mehr auf; die Sicherheit auf Datensatzebene bleibt deaktiviert bis sie explizit aktiviert wird.

Benutzergruppe lässt sich nicht löschen nach Migration von AX 3.0 nach AX 2009

Manchmal kann es vorkommen, dass sich von AX 3.0 migrierte Benutzergruppen nicht löschen lassen. Folgender Effekt tritt ein: Man löscht die Benutzergruppe und die Zeile verschwindet, öffnet man das Formular allerdings erneut, wird die Benutzergruppe wieder angezeigt.

Das passiert, wenn der Konfigurationsschlüssel für Reporting Services aktiviert ist, aber keine Reporting Services Server angegeben und eingerichtet wurden.

Mit einer kleinen Anpassung in AOT/Forms/SysUserGroupInfo/Data Sources/User Group Info/delete kann dem Problem abgeholfen werden.

public void delete()
{
    userGroupId groupID;
    SysSRSTablePermissions permissionsTable;
    AifEndpointUser aifEndpointUser;
    SysSecurityFormTable    sysSecurityFormTable;
    ;

    groupID = userGroupInfo.Id;

    ttsbegin;

    delete_from aifEndpointUser
        where aifEndpointUser.UserId == groupID
        && aifEndpointUser.AxaptaUserType == AifAxaptaUserType::UserGroup;

    delete_from sysSecurityFormTable
        where sysSecurityFormTable.UserGroupId == groupID;

    super();

    if (isConfigurationkeyEnabled(configurationKeyName2Id('ReportingServices')))
    {
        //Now that the group is deleted, we can synchronize all the secure views that
        //include RLS criteria for this group. We need to keep the record for this
        //group in SRSTablePermissions for now because they are used to determine which
        //tables have permissions assigned to them for the specified group.
        //Once all views have been synchronized, then we can delete those records.

        if (SRSSecureViewManagerProxy::syncWithGroup(groupID) != 0)
        {
            // modificaton begin
            if ((select * from SRSServers).RecId)
            {
                ttsabort;
                return;
            }
            /*else
            {
                just fall trough, there is no need to abort, since there is no Reporting Services Servers located
            }*/
            // modification end
        }

        //Delete all records for the group from the SRSTablePermissions table.
        delete_from permissionsTable where permissionsTable.GroupId == groupID;
    }

    ttscommit;
}

Nun können auch alte migrierte Benutzergruppen gelöscht werden.

Friday, October 22, 2010

Unable to delete User Group after upgrade from AX 3.0 to AX 2009

Sometimes you are not able to delete user groups which are migrated from AX 3.0. The effect is as follow: you try to delete and the row removes, but re-opening the form will show the user group again.

This happens if the configuration key for Reporting Services is enabled, but there is no Reporting Services Server refered and configured in AX.

With a little modification in AOT/Forms/SysUserGroupInfo/Data Sources/User Group Info/delete you can workaround the problem.

public void delete()
{
    userGroupId groupID;
    SysSRSTablePermissions permissionsTable;
    AifEndpointUser aifEndpointUser;
    SysSecurityFormTable    sysSecurityFormTable;
    ;

    groupID = userGroupInfo.Id;

    ttsbegin;

    delete_from aifEndpointUser
        where aifEndpointUser.UserId == groupID
        && aifEndpointUser.AxaptaUserType == AifAxaptaUserType::UserGroup;

    delete_from sysSecurityFormTable
        where sysSecurityFormTable.UserGroupId == groupID;

    super();

    if (isConfigurationkeyEnabled(configurationKeyName2Id('ReportingServices')))
    {
        //Now that the group is deleted, we can synchronize all the secure views that
        //include RLS criteria for this group. We need to keep the record for this
        //group in SRSTablePermissions for now because they are used to determine which
        //tables have permissions assigned to them for the specified group.
        //Once all views have been synchronized, then we can delete those records.

        if (SRSSecureViewManagerProxy::syncWithGroup(groupID) != 0)
        {
            // modificaton begin
            if ((select * from SRSServers).RecId)
            {
                ttsabort;
                return;
            }
            /*else
            {
                just fall trough, there is no need to abort, since there is no Reporting Services Servers located
            }*/
            // modification end
        }

        //Delete all records for the group from the SRSTablePermissions table.
        delete_from permissionsTable where permissionsTable.GroupId == groupID;
    }

    ttscommit;
}

Now your able to delete migrated user groups.

Monday, October 4, 2010

Icons for Dynamics AX files

File used by Dynamics AX are often diplayed with the client executable icon or even with no related icon.

I've created some symbols which can be used as file icons for Dynamics AX. Additionally I've created a small application to let assign the files with the icons.



The application can be downloaded here:
http://www.luegisdorf.ch/AX/AXIcons.exe

On Vista/7 you have to run the application with administrator rights, otherwise the application will not work properly. And the executable file should be permanent placed in a local path, where it doesn't move again, because the icons are embedded in the EXE file (a good place could be C:\Programm files\Microsoft Dynamics AX\Common).

Icons für Dynamics AX Dateien

Die für Dynamics AX eingesetzten Dateien haben meistens das Standard-Icon des Clients oder gar kein Icon zugewiesen.

Ich habe ein paar Symbole gestaltet die sich als Datei-Icons für Dynamics AX verwenden lassen. Zusätzlich habe ich ein Programm erstellt, mit welchem die Icons zugewiesen werden können.



Das Programm findet man hier:
http://www.luegisdorf.ch/AX/AXIcons.exe

Man muss es allerdings unter Vista/7 als Administrator ausführen damit es richtig funktioniert. Und das Programm sollte permanent an einen lokalen Platz gelegt werden, an dem es nicht mehr wegverschoben wird, weil die Icons sich direkt in der EXE-Datei befinden (z.B: C:\Program files\Microsoft Dynamics AX\Common).

Tuesday, September 21, 2010

FirstWeekOfYear without getLocaleInfo

Sometimes it happens, that the function WinAPI::getLocaleInfo() raises an exception, when calling the Windows function GetLocaleInfoW in Kernel32.

While the function works for the GUI client, you get this exception in Enterpriseportal:
Dynamics Object Adapter Call failed.

Funktion 'GetLocaleInfoW' in DLL-Bibliothek 'KERNEL32' hat eine Ausnahmebedingung ausgelöst.

Microsoft.Dynamics.BusinessConnectorNet.XppException
at Microsoft.Dynamics.BusinessConnectorNet.AxaptaObject.Call(String methodName, Object[] paramList)
at Microsoft.Dynamics.Framework.BusinessConnector.Session.DynamicsObjectAdapter.Call(String methodName, Object param1)

The impact for the user was, that he just get a plain lookup popup window instead a calendar popup to select a date for a data field. *

I was unable to find the excact reason for that bug. But I assume it's the combination of x64 architecture, the .NET Business Connector and the use of a Unicode Windows Kernel function.

To bypass the error we've modified the method Global::firstWeekOfYear(), which was the caller to WinAPI::getLocaleInfo(), as follow:

Original:
static int firstWeekOfYear()
{
    #WinAPI
    return str2int(WinAPI::getLocaleInfo(#LOCALE_USER_DEFAULT, #LOCALE_FIRSTWEEKOFYEAR));
}
Modified:
static int firstWeekOfYear()
{
    /* #WinAPI
    return str2int(WinAPI::getLocaleInfo(#LOCALE_USER_DEFAULT, #LOCALE_FIRSTWEEKOFYEAR));
    */

    System.Globalization.DateTimeFormatInfo info = System.Globalization.DateTimeFormatInfo::get_CurrentInfo();
    System.Globalization.CalendarWeekRule rule = info.get_CalendarWeekRule();
    ;
    return(CLRInterop::getAnyTypeForObject(rule));
}

With this modification the Windows function GetLocaleInfoW in Kernel32 will not be direct used.

* Webframework based on Webforms (until AX 2009)

FirstWeekOfYear ohne getLocaleInfo

Manchmal kommt es vor, dass die Funktion WinAPI::getLocaleInfo() beim Aufruf der Windowsfunktion GetLocaleInfoW in Kernel32 einen Fehler verursacht.

Während die Funktion über den GUI-Client einwandfrei funktioniert, erhält man beim Aufruf über das Enterpriseportal eine Ausnahme:

Dynamics Object Adapter Call failed.

Funktion 'GetLocaleInfoW' in DLL-Bibliothek 'KERNEL32' hat eine Ausnahmebedingung ausgelöst.

Microsoft.Dynamics.BusinessConnectorNet.XppException
at Microsoft.Dynamics.BusinessConnectorNet.AxaptaObject.Call(String methodName, Object[] paramList)
at Microsoft.Dynamics.Framework.BusinessConnector.Session.DynamicsObjectAdapter.Call(String methodName, Object param1)

Für den Benutzer bedeutete dies, dass er in Lookupfenstern zum Auswählen eines Datums schlichtweg einfach nur eine weisse Seite sah, statt des erwarteten Kalenders.*

Leider konnten wir die Ursache des Fehlers nicht herausfinden. Ich vermute allerdings, dass die Kombination von x64-Architektur, dem .NET-Business-Connector und dem Aufruf einer Unicode-Windows-Kernelfunktion etwas damit zu tun hat.

Als Abhilfe haben wir die betreffende Methode Global::firstWeekOfYear(), welche in unserem Fall die Aufrufende Methode war, wie folgt angepasst:

Original:
static int firstWeekOfYear()
{
    #WinAPI
    return str2int(WinAPI::getLocaleInfo(#LOCALE_USER_DEFAULT, #LOCALE_FIRSTWEEKOFYEAR));
}
Modifiziert:
static int firstWeekOfYear()
{
    /* #WinAPI
    return str2int(WinAPI::getLocaleInfo(#LOCALE_USER_DEFAULT, #LOCALE_FIRSTWEEKOFYEAR));
    */

    System.Globalization.DateTimeFormatInfo info = System.Globalization.DateTimeFormatInfo::get_CurrentInfo();
    System.Globalization.CalendarWeekRule rule = info.get_CalendarWeekRule();
    ;
    return(CLRInterop::getAnyTypeForObject(rule));
}

Auf diese Weise wird die Windowsfunktion GetLocaleInfoW in Kernel32 erst gar nicht direkt verwendet.

* Webframework basierend auf Webforms (bis AX 2009)

Friday, August 6, 2010

Update_recordset und Arrays von gejointen Tabellen

Die Datenbankmodifikationsansweisung update_recordset bietet hübsche Möglichkeiten zum schnellen Ändern von Daten.

Leider hat sich hier im Kernel ein kleiner Fehler eingeschlichen, wenn man als Wertzuweisung ein Array-Element einer gejointen Tabelle verwendet. Folgendes update_recordset wird teilweise ignoriert, konkret ab der Zuweisung des Array-Element custTableRead.Dimension[2] wird diese sowie jede weitere Feldwertzuweisung ausgelassen (also auch die Zuweisung von NameAlias mit dem Wert "any Alias" wird schlichtweg nicht durchgeführt):

    CustTable    custTableUpdate;
    CustTable    custTableRead;
    ;

    ttsbegin;

    update_recordset custTableUpdate setting Street = custTableRead.Street, Name = custTableRead.Dimension[2], NameAlias = "any Alias"
        where custTableUpdate.AccountNum == "00000001"
        join custTableRead
            where custTableRead.AccountNum == "00000002";

    info(strfmt("%1", custTableUpdate.RowCount()));

    select firstonly custTableUpdate where custTableUpdate.AccountNum == "00000001";

    info(custTableUpdate.Street); // works fine so far
    info(custTableUpdate.Name); // you didn't get what you expect
    info(custTableUpdate.NameAlias); // you didn't get what you expect

    ttscommit;





Allerdings funktioniert alles prima, wenn kein Array-Feld im Spiel ist:

update_recordset custTableUpdate setting Name = custTableRead.Name, NameAlias = "any Alias"
        where custTableUpdate.AccountNum == "00000001"
        join custTableRead
            where custTableRead.AccountNum == "00000002";


Es funktioniert auch wenn das Array-Feld von der gleichen Tabelle stammt wie die zu aktualisierende Tabelle.

update_recordset custTableUpdate setting Name = custTableUpdate.Dimension[2], Street = custTableRead.Street
        where custTableUpdate.AccountNum == "00000001"
        join custTableRead
            where custTableRead.AccountNum == "00000002";

Damit eine Zuweisung von custTableRead.Dimension[2] in custTableUpdate.Name korrekt funktioniert muss man leider den altmodischen Weg gehen: Die Datensätze erst auswählen, dann die Zuweisung vornehmen und danach aktualisieren. Dies kostet zwar mehr Zeit, funktioniert dafür aber korrekt (was meiner Meinung nach auch mehr Priorität geniesst :).

ttsbegin;

    select firstonly Dimension from custTableRead
        where custTableRead.AccountNum == "00000002"
        join forupdate custTableUpdate
            where custTableUpdate.AccountNum == "00000001";
        
    custTableUpdate.Name = custTableRead.Dimension[2];
    custTableUpdate.update();
        
    ttscommit;

Dieses Verhalten bezieht sich nur auf AX 2009 und (falls nicht korrigiert) spätere Versionen. Die Möglichkeit zur Verwendung von Joins in update_recordset steht nämlich erst ab AX 2009 zur Verfügung.

Saturday, July 31, 2010

Mandatory-Eigenschaft auf Formdatasource-Feldern mit deaktiviertem Konfigurationsschlüssel

Es gibt einen Designfehler mit der Mandatory-Eigenschaft des Formdatasource-Feldes. Wenn man es auf true setzt, wird auch auf den Feldinhalt geprüft, selbst wenn das Feld mit per Konfiguration deaktiviert ist.

Als Beispiel: Die Tabelle TableA hat zwei Felder: FieldA und FieldBFieldB hat als Konfigurationsschlüssel-Eigenschaft den Wert ConfigKeyA. Man erstellt nun ein Formular mit einer Datasource der Tabelle TableA und setzt die Mandatory-Eigenschaft von FieldB auf true. Dann deaktiviert man den Konfigurationsschlüssel ConfigKeyA im Menü Administration/Einstellungen/System/Konfiguration. Zuletzt startet man den Client neu, öffnet das erstellte Formular und versucht einen neuen Datensatz zu speichern. Der Speichervorgang wird fehlschlagen mit der Meldung, dass FieldB ausgefüllt werden muss.

Als Vorsicht beim Manipulieren der Mandatory-Eigenschaft auf  Formdatasource-Feldern.

Dies betrifft AX 2009, aber gut möglich dass das Fehlverhalten auch in früheren Versionen auftritt.

Mandatory falg on form data source field with disabled configuration key

There's a design bug with the form data source field property called mandatory. If you set it to true, the condition if the field is filled in will be checked even the field is deactivated by configuration.

As an example: Your table TableA has two fields: FieldA and FieldB. FieldB has configuration key property value ConfigKeyA. You create a form with a data source from table TableA. You set the data source field FieldB's property mandatory to true. Now you go to administration/settings/system/configuration and deactivate configuration key ConfigKeyA. At least restart client, open the created form and try to save a new record. It will fail with the message that field FieldB has to be filled in.

So be careful if manipulating the mandatory property on form data source fields.

This applies to AX 2009, but I have not tested if it applies to former Versions too.

Debugging von Tablemap-Code im Enterprise Portal

In AX 2009 (oder auch schon in AX 4.0, kann mich gerade nicht erinnern) ist es nicht möglich Ereigniscode von Steuerelementen auf Formularen zu debuggen.

Eine ähnliche Verhaltensweise besteht beim X++-Debuggen im Enterprise Portal von von Tablemap-Methoden. Der Debugger reagiert nicht auch wenn ein Breakpoint vorhanden ist. Die einzige Möglichkeit eine Reaktion des Debugger zu erzwingen ist die Verwendung der Anweisung 'breakpoint', die natürlich nach dem Debuggen wieder entfernt werden sollte.

Vielleicht betrifft dieser Effekt das Debuggen von X++-Tablemap-Code ausgeführt durch den .NET-Business-Connector ganz allgemein, und nicht nur das Enterprise Portal.

Wednesday, July 28, 2010

Debugging tablemap code trough Enterprise Portal

As a known fact (or bug), it's not possible to catch breakpoints in control events on forms since AX 2009 (or even AX 4.0 can't remember exactly).

A similar behaviour exists, if you try to debug X++ code of table maps methods in Enterprise Portal. The debugger won't react even if you have set a breakpoint. The only one method to force the degugger is to use the command 'breakpoint' in your code, which of course should be removed when finishing debugging.

May be this affects X++ table map code debugging executed by .NET Business Connector in general, not especially the Enterprise Portal.

Monday, July 19, 2010

Select Group By und Join Order By

Vorsicht ist geboten bei Select-Statements, welche Group By und Order By mischen. Zwar funktioniert die Abfrage auf der Datenbank korrekt, aber mit der Einbusse von Daten. Daten für Order By-Tabellen sind dann nämlich nicht verfügbar.

Unten stehendes Beispiel bringt die gewünschten Fälligkeitsdaten der Tabelle CustTransOpen, aber die Belege der Tabelle CustTrans fehlen:

CustTable custTable;
CustTrans custTrans;
CustTransOpen custTransOpen;
;

while select CustGroup from custtable group by CustGroup // group by
    join Voucher from custTrans // order by
         where custTrans.AccountNum == custtable.AccountNum
         join DueDate from custTransOpen group by DueDate // group by
              where custTransOpen.RefRecId == custTrans.RecId
{
    info(custTable.CustGroup); // works
    info(custTrans.Voucher); // works not
    info(Date2StrUsr(custTransOpen.DueDate)); // works
}

Wenn man der Tabelle CustTrans ein 'group by' hinzufügt, erhält man dann auch Daten:

CustTable custTable;
CustTrans custTrans;
CustTransOpen custTransOpen;
;

while select CustGroup from custtable group by CustGroup // group by
    join Voucher, RecId from custTrans group by Voucher, RecId // group by
         where custTrans.AccountNum == custtable.AccountNum
         join DueDate from custTransOpen group by DueDate // group by
              where custTransOpen.RefRecId == custTrans.RecId
{
    info(custTable.CustGroup); // works
    info(custTrans.Voucher); // works now
    info(Date2StrUsr(custTransOpen.DueDate)); works
}

Dieselbe Problematik besteht auch mit Datenabfragen die mit dem QueryRun-Objekt verarbeitet werden.

Es bleibt offen, ob die Ursache für dieses Problem bei AX oder an der SQL-Engine liegt; feststeht, dass man mit dem Mischen von Group By und Order By Bugs erzeugt, die man leider nicht so schnell herausfindet.

Thursday, July 15, 2010

Bitmanipulation with integer64

Take care on bit shifting if the result should be a 64 bit integer. Fragments like the follow code example does not take the obviously desired effect:
int64 i64 = 1 << 32; // move the very right positive bit just 32 ranks to the left
The result on that will be 0. The problem comes up with the return value from the operation 1 << 32, it's an 32 bit integer, even the result will be copied in a 64 bit integer (the bit shfiting operation is executed encapsulated before copy to variable i64). The base operand sets the type of result, in our code it's a constant number (1). And because 1 is a 32 bit integer, the results represents a 32 bit integer too. But why it ends with zero result? Its the reaction for the overflow when trying to access a bit area which is not available on a 32 bit integer (a 32 bit integer has just its 32 bits - not more, not less). Other programming languages reacts in the same situation with exceptions type overflow (f.ex. VB6) or moves the bits in circle (f.ex. C#), but AX quits that problem situation just with a type specified null value.

To ensure the code works fine, do it that way:
int64 base = 1;
int64 result = base << 32; // move the very right positive bit just 32 ranks to the left

Bitmanipulation mit Integer64

Aufgepasst bei Bitverschiebungen wenn das Ergebnis ein Integer 64 Bit Länge sein soll. Manipulationen wie etwas der nachstehend abgebildete Code bringen leider nicht das offensichtlich erwünschte Ergebnis:
int64 i64 = 1 << 32; // move the very right positive bit just 32 ranks to the left
Das Resultat draus ist nämlich 0. Das Problem liegt darin, dass der Rückgabewert der Operation 1 << 32 ein Integer 32 Bit Länge ist, auch wenn das ganze sogleich einem Integer 64 Bit Länge zugewiesen wird (die Bitverschiebung wird gekapselt ausgeführt bevor die Zuweisung an die Variable i64 erfolgt). Aber warum ist das Resultat 0? Der Basisoperand der Bitverschiebung bestimmt den Rückgabewert, in unserem Beispiel also eine konstante eins (1). Und weil eine 1 ein Integer 32 Bit Länge darstellt, ist auch das Ergebnis ein simpler Integer 32 Bit Länge. Das Resultat von 0 resultiert aus der Reaktion auf den Überlauf beim Zugriff auf ein Bit-Bereich den es auf einem Integers 32 Bit Länge nicht gibt (ein Integer 32 Bit Länge hat halt eben nur 32 Bits). Andere Programmiersprachen reagieren hier mit einer Ausnahme vom Typ Überlauf (z.B. VB6) oder schieben die Bits im Kreis herum (z.B. C#), AX beendet die Problemsituation schlicht mit einem typenspezifischen Nullwert.

Damit der Code also richtig funktioniert muss er so geschrieben werden:
int64 base = 1;
int64 result = base << 32; // move the very right positive bit just 32 ranks to the left

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.

Monday, April 26, 2010

CLR Fehlermeldungen aufschlüsseln

Die Einbindung von .NET-Code im X++ Sourcecode ist eine hübsche Sache. Was sich mit Native-Code nicht bewerkstelligen lässt, kann unter Umständen mit .NET-Code gelöst werden. Leider sind die Ausnahmen deren Ursprung dem .NET-Framework zuzuschreiben sind meist sehr oberflächlich gehalten.
Folgende statischer Methode automatisiert das Aufschlüsseln der .NET-Ausnahme:

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
    }
    */
}

Und so wird die neue Hilfsmethode verwendet:

System.String[] files;
;

try
{
    files = System.IO.Directory::GetFiles('c:\\halliGalli');
}
catch
{
    MyClass::cRLExtendException(CLRInterop::getLastException());
}

So sieht dann die Ausgabe des Infolog-Fensters aus:


In den Codebeispielen wurde die Hilfsmethode auf einer Klasse mit dem Namen 'MyClass' angelegt. Auf welcher Klasse schulssendlich die Methode implementiert wird, sollte aber der Entwickler, beziehungsweise das Entwicklerteam entscheiden.

Monday, April 12, 2010

Communicate with the RunBase Dialog

Sometimes you need to communicate with the RunBase object's dialog. Here are some information what you have to know to make it run well.

The Caller
Say, your RunBase dialog calls a custom form which contains special selection settings for the RunBase object (which f.ex. cannot be integrated in the RunBase dialog due layout specifications). So you've placed a menu item button to your dialog. That way the user can call the custom form with the special selection settings:

DialogRunbase ret;
;
ret = super(dialog, forceOnClient);
ret.addMenuItemButton(MenuItemType::Display, 
    menuItemDisplayStr(MyRunBaseSelectionForm));

But once the custom form is open, how to geht a handle to the RunBase object? To grab the RunBase object you need to dig the callers:

FormRun         formRunCaller;
DialogRunBase   dialogRunBase;
MyRunBaseClass  caller;
;
formRunCaller = element.args().caller(); // returns the dialog's form

// returns a DialogRunBase object
dialogRunBase = formRunCaller.args().caller();


// finally returns the RunBase object
caller = dialogRunBase.runBaseBatch(); 

Once you have the handle to the RunBase object you can acceed all public parameter methods and even the RunBase's QueryRun object.

Notify the caller
If the user decide to close your custom form with confirmation, so you need to notify the RunBase object about the new data. The best way is to place this code in the method closeOK() on your custom form:

public void closeOk()
{
    QueryRun qr = caller.queryRun();
    ;
    qr.whatEverYouHaveToManipulate(...);
    caller.parmThisAndThat(...);
    super();
}

Update the dialog values
As the user shall see the setting changes, you need to update the dialog controls. That for you can use the method dialogUpdate():

public void closeOk()
{
    QueryRun qr = caller.queryRun();
    ;
    qr.whatEverYouHaveToManipulate(...);
    caller.parmThisAndThat(...);
    caller.dialogUpdate(); // updates at least query range fields
    super();
}

If you want a little tutorial, you can download this sample project, demonstrates just what I tried to explain. Import the XPO file and open the class MyRunBaseClass, then click on the button "Pick customer".
Keep in mind, it's only a tutorial, the code is neither BP nor fool proofed!

Hope this can improve your coding skills :)

Mit dem Dialog eines RunBase-Objektes kommunizieren

Manchmal ist es nötig mit dem Dialog eines RunBase-Objekts zu kommunizieren. Nachstehend ein paar nützliche Informationen damit es auch gelingt.

An den Aufrufer gelangen
Angenommen der RunBase-Dialog ruft ein weiteres Formular auf, welches über spezielle Selektionseinstellungen für das RunBase-Objekt verfügt (z.B. wenn die Darstellung dieser speziellen Einstellungen im RunBase-Dialog nicht möglich ist). So wird dem Dialog also ein Menu-Item-Button hinzugefügt, damit der Benutzer das Formular mit den speziellen Einstellungsmöglichkeiten aufrufen kann:

DialogRunbase ret;
    ;
    ret = super(dialog, forceOnClient);
    ret.addMenuItemButton(MenuItemType::Display, menuItemDisplayStr(MyRunBaseSelectionForm));

Wie erhält nun aber das Formular Zugriff auf das RunBase-Objekt? Dafür muss durch die Aufrufer 'gegraben' werden:

FormRun         formRunCaller;
    DialogRunBase   dialogRunBase;
    MyRunBaseClass  caller;
    ;
    formRunCaller = element.args().caller(); // returns the dialog's form
    dialogRunBase = formRunCaller.args().caller(); // returns a DialogRunBase object
    caller = dialogRunBase.runBaseBatch(); // finally returns the RunBase object

Wenn man über das Handle des RunBase-Objekts verfügt, hat man Zugriff auf alle öffentlichen Parametermethoden und auch auf das QueryRun-Objekt des RunBase-Objekts.

Den Aufrufer benachrichtigen
Wenn der Benutzer nun das Formular mit den speziellen Selektionskriterien mit OK schliesst, muss das RunBase-Objekt mit den allenfalls vorgenommenen Einstellungen aktualisiert werden. Am besten wird dies in der closeOK()-Methode auf dem Formular implementiert:

public void closeOk()
{
    QueryRun qr = caller.queryRun();
    ;
    qr.whatEverYouHaveToManipulate(...);
    caller.parmThisAndThat(...);
    super();
}

Dialogfelder aktualisieren
Damit der Benutzer die gemachten Änderungen auf dem RunBase-Dialog sieht, müss zusätzlich die Dialogfelder aktualisiert werden, dies wird mit der Methode dialogUpdate() sichergestellt:

public void closeOk()
{
    QueryRun qr = caller.queryRun();
    ;
    qr.whatEverYouHaveToManipulate(...);
    caller.parmThisAndThat(...);
    caller.dialogUpdate(); // updates at least query range fields
    super();
}

Ein kleines sample steht als XPO-Datei zum Download bereit und demonstriert, was ich hier zu erklären versucht war. Nach dem Importieren der XPO-Datei einfach die Klasse MyRunBaseClass öffnen und auf die "Pick customer"-Schaltfläche drücken.
Dieser Beispielcode wurde weder auf Optimale Verfahren geprüft, noch ist er gegen Fehler gefeit!

Hoffe das dieser Beitrag den Entwicklerhorizont ein klein wenig erweitert :)

Monday, March 22, 2010

Die firstonly-Falle

Dies ist eine Übersetzung des englischsprachingen Beitrags the firstonly trap.

Die Verwendung des Firstonly-Qualifizierer in einem X++ Select Statement kann die Ausführungsgeschwindigkeit erheblich steigern. Also warum den Qualifizierer nicht auch für update_recordset und delete_from anwenden? Das war meine Idee und so begann ich folgenden Code umzuschreiben:

ttsbegin;
select firstonly forupdate myTable where myTable.NotUniqueField == 'anyValue';
myTable.delete();
ttscommit;

nach

delete_from firstonly myTable where myTable.NotUniqueField == 'anyValue';

Leider entsprach das Resultat nicht den Erwartungen. Alle Datensätze die mit den Abfragekriterien übereinstimmten wurden gelöscht. Zuerst dache ich, delete_from firstonly leide an der gleichen Ungenauigkeit wie select firstonly und deshalb sei nicht sichergestellt, dass immer nur ein Datensatz verarbeitet würde. Aber in Wirklichkeit sieht es eher so aus, dass der Firstonly-Qualifizierer weder für update_recordset noch für delete_from implementiert wurde (auch wenn der X++ Kompiler es akzeptiert).

Andrerseits, T-SQL bietet die Möglichkeit das TOP-Schlüsselwort in Select-, Update- und auch Delete-Anweisungen zu verwenden - und warum sollte der Kompiler schlucken was keinen Sinn macht? Ich könnte mir vorstellen, dass diese Funktionalität in späteren Versionen zur Verfügung steht. Ich werden den Firstonly-Qualifizierer weiterhin gebrauchen, aber nur wenn die Where-Kriterien einen eindeutigen Datensatz referenzieren.

delete_from firstonly myTable where myTable.UniqueField == 'keyValue';
// and
update_recordset firstonly myTable setting AnyField = 'anyValue' where myTable.UniqueField == 'keyValue';

Vielleicht wird Code von heute auch in späteren AX-Versionen verwendet, wenn update_recordset und delete_from den Firstonly-Qualifizierer unterstützen. Ausserdem finde ich, dass es das Lesen des Codes ein wenig erleichtert.

Also tappt nicht auch in diese Falle ;)

The firstonly trap

Using the firstonly qualifier on a X++ Select Statement can increase speed performance. So why don't use it for update_recordset and delete_from too? This was my Idea and I was going on to rewrite this code:

ttsbegin;
select firstonly forupdate myTable where myTable.NotUniqueField == 'anyValue';
myTable.delete();
ttscommit;

into that

delete_from firstonly myTable where myTable.NotUniqueField == 'anyValue';

Unfortunately, the result was not as expected: All records which matched the condition were lost. First I thought the problem was, that a delete_from firstonly has the same inaccuracy as a select firstonly and therefore it wouldn't be granted to proceed only one record. But actually it seems that the firstonly qualifier is neither implemented for update_recordset nor for delete_from (even the X++ compiler accept it).

On the other hand, T-SQL provides the TOP clause in select, update and delete statement too - and why the compiler should swallow it, even it makes no sense? I could imagine, that this features will be implemented in later versions. I will use the firstonly qualifier again, but only if the where conditions refers a unique record.

delete_from firstonly myTable where myTable.UniqueField == 'keyValue';
// and
update_recordset firstonly myTable setting AnyField = 'anyValue' where myTable.UniqueField == 'keyValue';

May be, code from today is still active in later AX versions, when update_recordset and delete_from are supporting the firstonly qualifier. Further I like it as a hint when reading and try to understand source code.

So be warned to fall into that trap :)

Monday, March 8, 2010

Einfacher X++ Taschenrechner

Vielen Lesern dürfte die Rechenfunktion des Steuerlements vom Typ FormRealControl bekannt sein. Falls nicht, hier eine kleine Einführung: Wenn man in einem Feld vom Typ Gleitkommazahl eine komplette Kalkulation eintippt (Abb. a) und anschliessend die Eingabe bestätigt, wird dem Feld der ausgerechnete Wert zugewiesen (Abb. b).

Abb. a:

Abb. b:

So dachte ich, den Nutzen dieses Features dürfte auch durch x++ möglich sein. Das Resultat war schlussendlich eine statische Methode zum Auswerten von Kalkulationen.

So sieht die statische Methode aus:
/// <summary>
/// Calculates a simple math task
/// </summary>
/// <param name="_expression">
/// A simple mathematic expression. +-/* operators and brackets can be used
/// </param>
/// <returns>
/// The calculated result. If the expression does not match the expected format, 
zero is returned.
/// </returns>
/// <remarks>
/// Uses functionality from <c>FormRealControl</c>, therefore client execution 
is required.
/// </remarks>
static client real calcExpression(str _expression)
{
    SysFormRun formRun;
    Args args = new Args();
    FormBuildControl buildCtrl;
    FormRealControl realCtrl;
    
    ;
    args.name(formstr(Dialog));
    formRun = classFactory.formRunClass(args);
    
    buildCtrl = formRun.form().design().addControl(FormControlType::Real, 
    classstr(FormRealControl));
    formRun.init();
    realCtrl = formRun.design().control(buildCtrl.id());
    realCtrl.pasteText(_expression);
    return realCtrl.realValue();
}

Und so kann sie verwendet werden:
info(strfmt('%1', MyClass::calcExpression('5+(5*2)')));

Als Anmerkung bleibt zu sagen, dass die Funktion über alle Einschränkungen verfügt, über die auch das Rechen-Feature des FormRealControl-Objekt verfügt.

Simple x++ Calculator

May be you know the AX GUI calculator of the FormRealControl. If not, I will explain in a short way: If you enter a proper calculation expression in a real field (picture a) and commit the input expression will be calculated and the result set to the field (picture b).

picture a:

picture b:

So I found this functionality can also be used trough x++. The result was a static method which can evaluate an expression to be calculated.

That's the code behind:
/// ;
/// Calculates a simple math task
/// ;
/// ;
/// A simple mathematic expression. 
/// +-/* operators and brackets can be used
/// 
/// 
/// The calculated result. If the expression
/// does not match the expected format, 
/// zero is returned.
/// ;
/// ;
/// Uses functionality from 
/// FormRealControl, 
/// therefore client execution 
/// is required.
/// ;
static client real calcExpression(str _expression)
{
    SysFormRun formRun;
    Args args = new Args();
    FormBuildControl buildCtrl;
    FormRealControl realCtrl;
    
    ;
    args.name(formstr(Dialog));
    formRun = classFactory.formRunClass(args);
    
    buildCtrl = formRun.form().design()
        .addControl(FormControlType::Real, 
        classstr(FormRealControl));
    formRun.init();
    realCtrl = formRun.design()
        .control(buildCtrl.id());
    realCtrl.pasteText(_expression);
    return realCtrl.realValue();
}
And that's how we use it:
info(strfmt('%1', 
    MyClass::calcExpression('5+(5*2)')));

Keep in mind, this function inherits all limits which the FormRealControl calculation feature owns.

Saturday, February 20, 2010

SmartStart 3000 V 1.6 for Dynamics AX

This Article is an announcement about the new Version of SmartStart 3000. You don't know what the SmartStart 3000 is? It's a free configuration manager for Dynamics AX. If you are working with more than just one AX configuration, this tool can make your life easier. A Full feature list can be found on the SmartStart 3000 Central web page.

About the new Version 1.6
It tooks some extra time to publish the new release (or even more time than I tought it would take :).

What's new?
  • Support for Business Connector configurations
  • Possibility to switch current/default configuration (for Client and BC configurations as well)
  • Source filter
  • Extended export functionality
  • Quick access button for custom tools/shortcuts
  • Auto remove for invalid favorites
  • Configuration drag'n'drop-to-target possibility for deletion and export
  • When deleting registry based configurations, they will collected in recycle bin as registry export files (so you can restore them if needed)
  • Global keyboard shortcut for application lookup
Bugfixes
  • Fixed favorite storage (AX version is now included, file storage fixed)
  • Fixed registry export (even the configurations work proper for Dynamics AX Client, user was not able to edit them in the Dynamics AX 2009 Configuration Utility)
  • Fixed unexpected flash flare on start up phase
  • Prohibition of deleting original, installed configurations

Language support
Refreshed language support for the following tongues: Czech, Danish, German, English, Spanish, French, Norwegian, Polish and Swedish. A great thank-you for the dedicated translators!

Documentation
The PDF documentations is updated to the new V 1.6 functionality and shipped with inside the application and also downloadable from the Central web page

Remarks
Due the fixed favorite storage process, all favorites will be cleared when running V 1.6 first time

SmartStart 3000 Argument Configurator
The online Argument Configurator has been extended with preview windows

Known Issues
When using SmartStart 3000 on terminal servers with very low color settings (due slow connection), SmartStart 3000 wont start up. In this case, just use the parameter /noGDI. I'm working to fix that bug, but currently I haven't located the bug source.

Download
Download the new release as usual at: SmartStart 3000 Central

Next week I'm out of office, but I'm looking forward to get your feedback!

Friday, January 29, 2010

Wie man die Modifier Private, Protected und Abstract umgeht

Auf Wunsch der Leserschaft werden meine Beiträge nun auch in deutscher Sprache veröffentlicht. Dieser Post bezieht sich auf den englischsprachigen Post How to ignore Private, Protected and Abstract modifiers. 

Bisher habe ich mich immer über die Meldungen "Statisches Konstrukt implementieren, um Änderungen zu ermöglichen." und "'New' muss den Status 'protected' aufweisen." gewundert.

Sagen wir, wir möchten eine neue Verarbeitung mit einer RunBaseBatch-Klasse realisieren: Wenn wir nun den Optimalen Verfahren folgen, werden wir folglich die new()-Methode Protected deklarieren. Aber wie soll nun das Framework der Stapelverarbeitung jemals ein Objekt aus dieser Klasse erstellen, sollte der Benutzer die Verarbeitung in den Stapel stellen?

Weil die new()-Methode ja nun mit dem Modifier Protected geschützt ist, kann aus der Klasse kein Objekt mit new() erstellt werden, richtig?
Theoretisch ja, allerdings hat man da die Rechnung ohne die Möglichkeiten der Reflection-Technik in AX gemacht.

Wie im Allgemeinen bekannt, ist es möglich mit dem Kernelobjekt vom Typ DictClass (oder mit dem vielleicht besser bekannten abgeleiteten Typ SysDictClass) dynamisch Objekte zu erstellen und Objektmethoden aufzurufen. Nach ein paar Tests hat sich herausgestellt, dass das DictClass Objekt seinen ganz eigenen Regeln folgt, wenn es um die Möglichkeiten der Codeverarbeitung geht.

Mit einem DictClass-Objekt kann man Objekte aus Klassen erstellen, die dafür gar nicht geeignet sind. So kann man mit der makeObject()-Methode:
  • Objekte aus abstrakten Klassen erzeugen 
  • Objekte erstellen auch wenn die new()-Methode Private oder Protected deklariert ist (die new()-Methode wird dann auch wirklich durchlaufen) 
  • Objekte erstellen, auch wenn die jeweilige Klasse nicht alle abstrakten Methoden überschrieben hat 
Aber die Möglichkeiten mit dem DictClass-Objekt enden nicht einfach damit verbotene Objekte zu erstellen. Es ist auch möglich:
  • Member- und statische Methoden ungeachtet der Modifier protected, private oder abstract aufzurufen (mit callObject() und callStatic()
Als wäre das nun nicht schon genug der Blasphemie: das DictTable-Objekt verfügt ebenfalls über diese Verhaltenseigenschaften. Mit callStatic() und callObject() des DictTable-Objektes kann jede beliebige Tabellenmethode aufgerufen werden.

Fazit: In AX existieren zwar hässliche, aber äusserst effektive Verfahren um Codesicherheit und Kompilerprüfungen zu umgehen. Auch wenn es möglich ist, diese Tricks einzusetzen, rate ich dringend davon ab. Dieser Artikel soll nur zeigen was möglich ist, das heisst aber nicht, dass es auch sinnvoll ist.

(Inhalt bezieht sich auf AX 2009)

TypeId from table field

How to retrieve the typeId by a given FieldId? I didn't found a method doing this in AX, so I wrote it by my self:

int fieldId2TypeId(TableId _tableId, FieldId _fieldId)
{
    DictField dictField = new DictField(_tableId, _fieldId);
    int ret; // 32 bit length type
    ;
    if (dictField.typeId())
    {
        // extended type is given
        ret = dictField.typeId(); // dictField.typeId() is an int 16 range value
    }
    else if (dictField.enumId())
    {
        // enum type is given
        ret = dictField.enumId(); // dictField.enumId() is an int 16 range value
    }
    ret = ret << 16; // move enum or exteneded type information to left side 16 bit area


    ret = dictField.type() | ret; // combine left side 16 bits from typeId information with right side 16 bits from enum or extended type information


    return ret;
}

Wednesday, January 13, 2010

Import and Export Labels for external translations

When working in international and multi language environment, often situation comes where you have a label file (*.ald) and you should extract the labels for transfer to translation office. After, you have to create a new label file with the translated data from the translation office retrieved.

To make that process more handy, I have created an export and import routine to do the import and the export from and to an excel file.

How to install

  • Download and import the XPO file from here.

How to use it

  • Open Menu Microsoft Dynamics AX\Tools\Development tools\Label\Excel
  • Choose Dump (for export) or Load (for import)
  • Or open from AOT\MenuItems\Action the item Dev_LabelFileToExcel or the item Dev_LabelFileFromExcel


Exportation of Labels



You can export into an Excel file from scratch or in an Excel file which was exported and translated before; template and translation labels will be read from source and written into Excel file. But if the template label expression given by Excel file (in case you select one) is not corresponding to the template expression that comes along the source, the translation expression is put just idle to Excel file so the translation office can translate it again (means: the expression has changed since last time export).
First time exportation should always be done with option Recreate file.
The source can either be the current *.ald files given by application you are working on, or from a user defined application directory (or copy of). Keep in mind that you probably have to restart the AOS to ensure the label files are up to date.
If you choose option File System Directory (Server/AOS), your client does not need file access to the application folder, the server will read the files.
The process will finish with a display of the modified/created Excel file. All orange fields has now to be translated.

Importation of labels


Choose the Excel file where the translated table are stored. The structure must correspond to the Excel file which was exported (otherwise the import will fail).
Choose the directory where an *.ald label file should be created. Don't specify an application directory where an running AOS is accessing (otherwise you can get strange display effects in application, data access violations and label file loose). If a label file for the given translation language is already present, it will be replaced.

Hope this tool makes your live with AX easier as did for me.

It works in AX 2009 with Office 2007 (may be it works for AX 4.0 and Office 2003 as well)
This post will not be translated into German, since it's not very hard to understand this tool ...

Friday, January 8, 2010

Add-In Extension: Create default methods for classes

May be you know the Add-In for automated creation of find(..) & exists(..) method for tables provided by Axaptapedia?

Well, I have created a similar Add-In for classes. The goal of that Add-In is the same as the find(..) & exists(..) Add-In, to speed up code implementation.

What's the functionality?
  • It creates required abstract and interface methods
  • If the class is not abstract, a protected new(..) and a static construct(..) constructor will be created
  • If the class is in the RunBase tree, a static main(..) method is created
  • Already created methods will not be touched

How to install:
  • Import this XPO containing a private project with a class and a menu item.
  • Add the menu Item to the menu SysContextMenu (you can decide on what position)
  • Add a the code below to the class method SysContextMenu/verifyItem before line 217 (switch menuItem)
    case menuItemActionStr(DEV_CreateDefClsMeths):
        return ! docNode && _firstType == UtilElementType::Class;
  • In AOT, select a class and open context menu/Add-Ins. You will see now the new entry named Create default class methods. Klick it to process.
Tip: place the modification on a private layer (a layer which is used internally only): it's not really customer related, so you don't have to deliver it to them.

It works in AX 2009. Not tested for AX 4.0

(this article is available in English only)

Wednesday, January 6, 2010

Join-Queries mit temporären Tabellen (und es funktioniert eben doch)

Join-Queries mit temporären Tabellen (und es funktioniert eben doch)

Dieser Beitrag bezieht sich auf den englischsprachigen Artikel Queries with temporary tables (nevertheless it works)

Die Verwendung von temporären Tabellen innerhalb Queries kann ab und an zu Problemen führen; im Speziellen, wenn man die Fehlermeldung "Ein Datensatz in % (%) kann nicht ausgewählt werden. Temporäre Tabellen müssen bei Verknüpfung mit persistenten Tabellen äußere Tabellen sein." erhält. Ein Arbeitskollege hat allerdings eine solche Query zu meinem Erstaunen trotzdem zum Laufen gebracht. Auf der Grundlage seiner Vorarbeit habe ich das Thema mal genauer untersucht.

Die Inhaltsgrundlage dieses Beitrages basiert auf dem Wissen durchgeführten Tests. Die Funktionsweise von Queries mit temporären Tabellen wird hier allein durch Beobachtungen erklärt (auch wenn AX vielleicht anders als angenommen arbeitet).

Der genannte Fehler wird ausgelöst wenn die Datenselektion der jeweiligen Datenquelle nicht auf dem gleichen Tier ausgeführt wird, wie die übergeordnete (parent) Datenquelle. Die erste Datenquelle nimmt allerdings eine Sonderstellung ein. Die Datenquellen werden nämlich auf dem Tier abgearbeitet wo sich der Datenpuffer der Datenquelle befindet. Hat aber der Server-Tier einmal mit der Abarbeitung einer Datenquelle begonnen, kann für die weitere Datenselektion nicht mehr zurück auf den Client-Tier gewechselt werden.

Unter Beachtung einiger Regeln steht für eine Join-Query mit temporären Tabellen aber absolut nichts im Wege. Es gilt zu beachten:
  • Persistente Tabellen werden stets auf dem Server-Tier verarbeitet
  • Temporäre Tabellen werden auf dem Tier verarbeitet, auf den das Handle der Tabelle hinzeigt
    • Temporäre Tabellen, die üblicherweise persistent sind, aber mit der setTmp()-Methode zur Laufzeit temporär gestellt werden, werden auf dem Tier gehalten, wo die setTmp()-Methode angewendet wird

      CustTable custTable
      ;
      custTable.setTmp(); // now the handle will fixed on the current tier

    • Temporäre Tabellen welche bereits auch als solche im AOT definiert sind, werden dort gehalten wo sich auch die Deklaration befindet

      TmpSysQuery
      tmpSysQuery; // the handle is already fixed on the current tier
      ;

  • Die erste Datenquelle in der Query kann ihren Datenpuffer wahlweise vom Client- oder Server-Tier beziehen
  • Alle weiteren, und somit eingebetteten Datenquellen müssen ihren Datenpuffer entweder auf dem gleichen Tier wie ihre übergeordnete (parent) Datenquelle oder aber auf dem Server-Tier halten

Folgende Konstellationen sind demnach möglich oder eben nicht (einige Beispiele):
(Siehe englischer Artikel)

Fazit: Die Meldung "Ein Datensatz in % (%) kann nicht ausgewählt werden. Temporäre Tabellen müssen bei Verknüpfung mit persistenten Tabellen äußere Tabellen sein." würde wohl besser heissen: "Temporäre Datenquelle % bezieht Daten nicht aus dem gleichen Tier wie die übergeordnete Datenquelle % noch vom Server-Tier selbst.".

Hier noch ein paar allgemeine Anmerkungen zur Verwendung der setTmp()-Methode für persistente Tabellen:
  • Die setTmp()-Methode muss immer in derselben Methode wie die Deklaration der Tabelle erfolgen
  • Es darf noch keine Manipulation mit dem Tabellenpuffer erfolgt sein (andernfalls hat die setTmp()-Methode keinen Einfluss mehr auf den Puffer!)
  • Für Datenmanipulationen sollten die Skip-Methoden wie skipDataMethods()/skipDeleteMethods()/skipEvents()/skipDataBaseLog() verwendet werden und mit den Befehlen doUpdate(), doInsert() und doDelete() gearbeitet werden um ungewollte Änderungen an der persistenten Tabelle in der Datenbank zu verhindern


(Inhalt bezieht sich auf das QueryRun Objekt der Versionen AX 3.0 3tier, AX 4.0 und AX 5.0)

Join-Queries with temporary tables (nevertheless, it works)

Usage of temporary tables often ends in trouble, especially when the error message “Cannot select a record in % (%). Temporary tables must be the inner tables when joined to permanent tables.“ rises up.

But a workmate of mine has solved the issue of that message with a special client/server constellation. Basing his solution I did some further research.

This article is based on knowledge elaborated by tests. All explanation of query functionality is related to its results (even AX perhaps does not work as described).

All problems begin, when the data selection of a data source is made in a different tier than its parent data source. Only the first data source has free tier choice. Data sources will be fetched on the tier where its data buffer is located. Once the server tier has become as data handler, it is not possible to re-access client data.

But if you take care to some rules, a query with temporary tables will run fine. Keep in mind, that:

  • Permanent tables are fetched on server tier
  • Temporary tables are fetched on there, where its data buffer is located:
    • Temporary tables, which are usually permanent, but made temporary with a setTmp() call, gets its tier where you make the setTmp()-call.

      C
      ustTable
      custTable
      ;
      custTable.setTmp(); // now the handle will fixed on the current tier

    • Temporary tables, which are already declared in AOT as temporary, gets its tier there, where its declaration is located

      TmpSysQuery
      tmpSysQuery; // the handle is already fixed on the current tier
      ;

  • First data source’s data buffer can either be located on the client tier or on the server tier as well
  • Every other, and therefore embedded data source must have its data buffer on the same tier as the parent data source or on the server tier

Look these examples which are possible or even not (not all possibilities listed …):













Summary: The message “Cannot select a record in % (%). Temporary tables must be the inner tables when joined to permanent tables.“ would better be named as “The temporary table's data buffer % is neither on the same tier as its parent % nor on the server tier”.

Some common remarks when using permanent tables as temporary tables:
  • The setTmp() method should done in the same method as where the table’s declaration is located
  • Between declaration and setTmp()-call no buffer manipulation is allowed (otherwise setTmp() will not work!)
  • When doing data manipulations use the skip-methods like skipDataMethods()/skipDeleteMethods()/skipEvents()/skipDatabaseLog() and the commands doUpdate(), doInsert() and doDelete() to prevent unwanted manipulation on the permanent data base table
(Content is related to the QueryRun Object from Version AX 3.0 3tier, AX 4.0 und AX 5.0)