...feel the spirit of Microsoft Dynamics AX RSS 2.0
 Sunday, May 27, 2007

Für Microsoft Dynamics AX 4.0 SP1 stehen auf den Microsoft Webseiten 2 PDF Dokumente zur Verfügen, welche die Hardwareanforungen an eine Systemumgebung mit 100 und 200 Benutzern beschreiben.

http://www.microsoft.com/dynamics/ax/product/hardwaresizing.mspx

Man sollte die dort getätigten Aussagen aber eher als eine Art "grundlegende Richtlinie" verstehen, da die realen Hardwareanforderungen eines einzelnen Dynamics AX System, bedingt durch die spezifischen Anpassungen, in einzelnen Punkten variieren können.

Sunday, May 27, 2007 2:30:50 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 | 

 Thursday, May 24, 2007

Heute wurde in den Newsgroups die Frage gestellt, ob es nicht möglich sein, nach einem XPO-Import einen "automatischen" CompileForward für alle mit dem XPO-File importierten Klassen zu starten.

Ganz automatisch ist dies leider nicht möglich. Es kann aber eine Art "Installationsjob" geschrieben werden, der zusammen mit den entsprechenden Klassen durch das XPO-File weitergegeben und importiert werden kann.

Nach dem Import des XPO-File muss dieser Job nur noch gestartet werden und es werden die frisch importierten Klassen sowie alle von diesen Klassen abgeleiteten Klasse neu kompilert (CompileForward).

Ein solcher "Installtionsjob" könnte wie folgt aussehen:

static void runAfterImport(Args _args)
{
    //CompileForward all imported (base) classes
    ;
    SysCompilerOutput::compileForward(className2Id("SalesFormLetter"));
    SysCompilerOutput::compileForward(className2Id("Your next base class"));
    SysCompilerOutput::compileForward(className2Id("Your next base class"));
}

Ein manuelles Auswählen des CompileForward Menüpunktes einzelner importierter Klassen kann dadurch entfallen.

Thursday, May 24, 2007 8:38:38 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 | 

 Friday, May 18, 2007

Anfang der Woche hat Microsoft endlich auch die letzte noch fehlende Microsoft Dynamics AX Development Prüfung

MorphX Solution Development for Microsoft Dynamics AX

veröffentlicht. Bei PearsonVue kann diese Prüfung auch schon gebucht werden.

 

Friday, May 18, 2007 10:11:19 PM (Mitteleuropäische Zeit, UTC+01:00)  Mathias Füßler  #    Comments [2] - Trackback


 Monday, May 14, 2007
Dialog zu erzeugen ist einfach und geht in der Regel sehr schnell. Das einzige Problem bei Dialog und deren Controls ist, das man im Dialog keine Möglichkeit hat auf die Benutzereingangen sofort zu reagieren. Nun das stimmt nicht wirklich, denn auch bei Dialogen und deren Controls kann man auf die Methoden, wie Modified oder Valdidate für jede Control innerhalb des Dialoges erzeugen um auf Benutzereingaben reagieren zu können. Leider geht es nur nicht so einfach wie bei den Forms, in der in der Regel schon alle FormControls im Design vorhanden sind und sehr einfach die entsprechenden Methoden direkt hinter dem aktuellem Control zu finden und anzupassen sind.
Bei Dialog ist dies nicht der Fall, so muss man diese Methoden an dem Objekt hinterlegen, welches den Dialog aufruft.

Ein Beispiel

void DialogOeffnen()
{
Dialog meinDialog;
DialogField dCtrl1, dCtrl2;
;
meinDialog = new Dialog("Neuer Dialog", this);

// Feld Kundennummer hinzufügen
dCtrl1 = meinDialog.addField(Typeid(CustAccount));
// Feld Name hinzüfgen
dCtrl2 = meinDialog.addField(typeid(Name));

// Dialog ausführen
meinDialog.run();
}

Um nun auf Benutzereingaben reagieren zu können um Beispielsweise nach Eingabe der Kundennummer(Feld: dCtrl1) den Name des aktuellen Kunden im zweiten Feld (Feld: dCtrl2) zu erhalten, muss für das Feld Kundennummer die Methode "Modified" überschrieben werden.
Hierzu ist es nötig, dem Dialog bzw. dessen Form inkl. FormRun mitzuteilen, das sich diese Methoden im aktuellen Objekt und nicht im Dialog befinden, denn im Dialog haben wir keinen einfluss.
Das erreicht man mit

meinDialog.doInit();

meinDialog.formRun().controlMethodOverload(true);
meinDialog.formRun().controlMethodOverloadObject(this);

Die Methode "doInit" ist notwendig um FormRun im Dialog zu initialiseren, wird dieses versäumt kommt es zum Laufzeitfehler.

Alle wichtigen Dinge sind nun getan, bis auf die Definition der Methode für das Feld "Kundennummer". Leider kann man den Namen des Feldes bei DialogField nicht manipulieren und muss hier mit den automatisch generierten Controlnamen leben. Da dieses aber immer nach dem gleichen Schema passiert ist das nicht wirklich tragisch. Bei Feldern wird der Name immer wie folgt aussehen


fld + Feldnummer + _ + ArrayIdx

Die Feldnummer kann man in der Regel einfach ermitteln: Das erste Feld hat die Feldnummer 1, das zweite Feld die Feldnummer 2, usw.
Den ArrayIdx hab ich bisher auch nur als 1 gesehen.
Das kann sich natürlich immer anhand der Komplexität verändern.

Zu guter Letzt fehlt noch die Methode "Modified" die die gewünschten Änderungen beinhaltet.

public boolean fld1_1_modified()
{
FormStringControl c = dialog.formrun().controlCallingMethod(); // Die FormControl, von der der Aufruf erfolgt
boolean ret;
;
ret = c.modified(); // Super() der aktuellen FormControl aufrufen ->modified

dControl2.value(CustTable::find(dControl1.value()).Name);
dControl2.enabled(false);
return ret;
}

Eine einfach Klasse die genau dieses auch macht kann man von hier runterladen.

Class_DialogControlMethodOverload.zip (,91 KB)
Monday, May 14, 2007 1:20:27 PM (Mitteleuropäische Zeit, UTC+01:00)  Mathias Füßler  #    Comments [1] - Trackback


 Friday, April 27, 2007

Wie z.B. aus Visual Studio bekannt kann in vielen IDE's der Ausführungsmodus des Quellcodes bestimmt und gewechselt werden.
Beispiele hierfür sind Debugmode oder Releasemode.

Wenn nun im Quelltext auf den Ausführungsmodus abgefragt wird, so kann zum Beispiel im Debugmode erweiterter Quellcode mit in ein Programm kompiliert werden, welcher beispielsweise verschiedene Loggingarten implementiert um eine bessere Problemanalyse durchführen zu können. 
Dies läßt sich auch mit Microsoft Dynamics AX machen.

Als erstes muss ein neues Macro erstellt werden (in diesem Beispiel mit dem Namen RunningMode), welches als globaler Schalter zwischen den einzenen Ausführungsarten eingesetzt wird:

/* Macro for setting the running Mode. */

/* Activate for debug mode. */
#define.Debug('true')

/* Activate for production mode. */
//#define.Debug('')

Jetzt kann an beliebiger Stelle im Quellcode #Debug verwendet werden um auf den Ausführungsmodus zu prüfen: 

static void TestTheRunningMode(Args _args)
{
    #RunningMode
    ;
    print "Code der immer ausgeführt werden soll"
    if(#Debug)
    {
        print "Code der nur im Debug-Mode ausgeführt wird"
    }
    pause;
 

Abhängig davon, wie im Macro RunningMode #Debug definiert wurde, kann nun beliebiger Quellcode aktiviert (ausgeführt) oder deaktiviert (nicht ausgeführt) werden.

Friday, April 27, 2007 6:04:10 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 | 

 Wednesday, April 25, 2007

Möchte man eine Methode der Klasse Info anpassen oder erweitern, können dabei unerklärliche Fehler auftreten.

Soll zum Beispiel die Methode open(FormRun formRun) erweitert werden und man verwendet hierbei eine Variable die in der classDeclaration deklariert ist, erhält man spätestens zur Laufzeit eine Fehlermeldung (dies sogar bei fehlerfreiem Code, keine Fehlermeldung im Debugger).

Grund hierfür ist, dass in Dynamics AX 4.0 die Klasse Info immer nur beim Öffnen des Clients erzeugt wird und man somit, egal ob die Klasse neu kompiliert wurde oder nicht, immer noch mit Teilen der alten Klassenversion arbeitet.

Um die neue Version der angepassten Info Klasse aufzurufen muss der Client geschlossen und wieder neu geöffnet werden.
Erst dann funktioniert die Anpassung wie gewünscht.

Das geschilderte Verhalten kann bei:

1. Erstellen von neuen Methoden
2. Deklarieren einer Variablen in der classDeclaration und Verwendung dieser in einer anderen Methode

allerdings nicht bei:

1. Anpassungen, die nur innerhalb einer einzelnen Methode durchgeführt werden

beobachtet werden.

Wednesday, April 25, 2007 9:57:04 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 | 

 Thursday, April 19, 2007

Eigendlich wollte ich hier noch die erweiterten Möglichkeiten bei den Einschränkungen zum Besten geben, das kann man aber schon alles fix und fertig bei Axaptapedia nachlesen.

Daher vorerst nur noch die eine Sache die ich noch beschreiben möchte:

Möchte man eine Einschränkung auf einen Array Feld (die Dimension: Abteilung, Kostenstelle, Kostenträger sind hierfür ein gutes Beispiel) definieren kann diese wie folgt erstellt werden.

queryRange = queryDS.addRange(fieldid2ext(fieldnum(InventTable, Dimension), 1));

Das erstellen der Range bleibt im Grunde gleich, nur das jetzt mittels fieldid2ext noch der Array Index angegeben wird. Der Index beginnt in Microsoft Dynamics Ax immer mit 1.

Da nun diese Sache hier nicht mehr behandelt wird, möchte ich noch eine komplett andere Möglichkeit zeigen Datensätze aus der Datenbank zu holen um diese beispielsweise in Masken anzuzeigen. Anstelle einer Query kann man auch SQL Anweisungen verwenden.


Das wird ein bisschen Tricky...
Ich fange aber heute nur "einfach" an.

Zuerst bauen wir uns einen neue Form und nennen diese ArtikelSQL (Der Name ist hier eigendlich egal, ich habe mich aber für eine Artikelmaske entschieden).
Dann legen wir die InventTable mittels Drag&Drop als Datasource fest. Zum Abschluss zeigen wir noch ein paar Feld (Artikelnummer und Artikelname reicht erstmal aus) über ein neu hinzugefügtes Grid in dieser Maske an. Fertig - beim Aufruf der Maske erhalten wir alle Datensätze die sich in der Tabelle InventTable befinden.

Als nächsten Schritt definieren wir eine Variable in der Classdeclaration

Source SQLAnweisung;

In dieser Variable schreiben wir später unsere SQL Anweisung. Bevor das jedoch gemacht wird, muss als nächstes noch die Methode executeQuery auf der DataSource überschrieben werden.

void executeQuery()
{
    if (SQLAnweisung)
    {
        runbuf(SQLAnweisung, this.cursor());
    }
    else
    {
        super();
    }
}


Neu  hinzugekommen ist hier die IF Abfrage, die prüft ob eine SQLAnweisung erstellt wurde, bzw. ob die Variable SQLAnweisung nicht leer ist. Wurde  keine SQLAnweisung getroffen, wird die Standardquery auf der DataSource ausgeführt. Wurde aber eine SQLAnweisung getroffen wird diese nun Anstelle der Query ausgeführt.

Zu guter letzt fehlt nur noch die SQLAnweisung, die erstellt werden muss.

void initSQLAnweisung()
{
XppCompiler compiler = new XppCompiler();
DictTable dictTable= new DictTable(Tablenum(InventTAble));
str SELECTAnweisung = 'SELECT * FROM '+dictTable.name();

SQLAnweisung = 'void SQLSTMT('+dictTable.name()+' '+dictTable.name()+')\n{\n'+SELECTAnweisung+';\n}\n';
if (!compiler.compile(SQLAnweisung))
{
setprefix("@SYS57538");
info (SQLAnweisung);
error (compiler.errorText());
SQLAnweisung = '';
<>}
}
Diese Methode erstellt die SQL Anweisung. Es reicht nicht aus einfach ein SELECT Statement zu schreiben, da der Compiler das nicht versteht. Anstelle eines einzelenen SELECT Statement schreibt man einfach eine Methode drumherum, die dann über runbuf ausgeführt wird.
Das eigendliche SELECT Statement sieht wie folgt aus und wird gleich der Variable SELECTANweisung zugewiesen.

SELECT * FROM InventTable

Über diese einfache Select Anweisung erhalten wir alle Datensätze aus der Tabelle InventTable. Auf den ersten Blick ist es nicht einfach zu erkenne aber aus

'void SQLSTMT('+dictTable.name()+' '+dictTable.name()+')\n{\n'+SELECTAnweisung+';\n}\n';

wird in der lesbaren Ansicht

void SQLSTMT(InventTable InventTable)
{
    SELECT * FROM InventTable;
}

und der globalen Variablen SQLAnweisung zugewiesen.
Nur um sicher zugehen ob alles richtig geschrieben wurden und keine Kompilierungsfehler enthalten sind, wird diese Anweisung noch dem Compiler übergeben und kompiliert.

compiler.compile(SQLAnweisung)


Wurden keine Fehler gefunden ist alles Prima, ansonsten wird die Variable noch gelöscht um beim Ausführen der Methode executeQuery keine Fehler zu erhalten. Die Ausgabe der Fehlermeldung vom Compiler ist optional.

Der Aufruf der Methode "initSQLAnweisung" Methode erfolgt noch dem super() in der Methode init der Maske.

Durch die SQL Anweisung hat man den Vorteil auf eine sehr einfache Art&Weise sehr komplexe Abfragen gestalten zu können. Einige Nachteile gibt es leider auch, die Standardfunktionen wie Filtern oder Sortieren funktionieren im genannten Beispiel nicht mehr.

Das komplette Beispiel noch als xpo zum Download. Wie immer wurde auch dieses Beispiel in Microsoft Dynamics Ax 4.0 erstellt.
Form_ArtikelSQL.zip (1.08 KB)

Thursday, April 19, 2007 10:51:15 PM (Mitteleuropäische Zeit, UTC+01:00)  Mathias Füßler  #    Comments [3] - Trackback
 | 

 Friday, April 13, 2007

In Microsoft Dynamics AX werden alle Benutzerberechtigungen über SecurityKeys gesteuert. SecurityKeys können für Forms, Formcontrols, Tables, Tablefields, MenuItems, etc. in deren Eigenschaften hinterlegt werden.

Für Klassen ist dies zwar mit Hilfe eines MenuItems und der Implementation der Methode "static void Main(Args _args)" ebenfalls möglich, doch kann für eine einzelne Methode kein SecurityKey vergeben werden.

Es gibt allerdings Situationen wo die Codeausführung, abhängig von der jeweiligen Berechtigung des Benutzers, gesteuert werden soll/muss.
In einem solchen Fall muss im X++ Code eine Überprüfung der Berechtigungen des Benutzers durchgeführt werden.

Möchte man prüfen, ob ein Benutzer Zugriff auf einen SecurityKey hat, kann dies mit der Methode hasSecurityAccess erfolgen.

if ( hasSecurityKeyAccess(securitykeyNum(CustSetup), AccessType::View) )
{
   //Code ausführen, wenn entsprechende Berechtigung vorhanden ist.
}

Möchte man prüfen, ob ein Benutzer Zugriff auf eine Tabelle hat, geht dies mit der Methode hasTableAccess.

if
( hasTableAccess(tablenum(CustTable), AccessType::Edit) )
{
   //Code ausführen, wenn entsprechende Berechtigung vorhanden ist.
}

Muss nicht nur die Tabelle, sondern auch ein einzelnes Feld überprüft werden, kann dies mit der Methode hasFieldAccess gemacht werden.

if
( hasFieldAccess(tablenum(CustTable), fieldnum(CustTable, AccountNum), AccessType::Delete) )
{
   //Code ausführen, wenn entsprechende Berechtigung vorhanden ist.
}

Der Parameter AccessType bestimmt hierbei auf welche Berechtigung das jeweilige Element gepürft wird (Kein Zugriff, Anzeigen, Bearbeiten, Erstellen, Vollständige Kontrolle).

Alle Methoden (hasSecurityAccess, hasTableAccess, hasFieldAccess) sind globale Methoden, die in der Klasse Global definiert sind. Somit können diese Methoden an jeder beliebigen Stelle im Quellcode verwendet werden. Es ist egal ob es sich um die Methode einer Form, Datasource, Klasse oder Tabelle handelt.

Friday, April 13, 2007 6:47:00 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback


 Friday, April 06, 2007

Nachdem nun in diesem Artikel schon die ersten Schritte unternommen und vorhandene Elemente manipuliert wurden, werden nun auch neue Elemente hinzuzugefügt.


Einfügen einer neuen Formcontrol im Design.

FormRun fr;
Args args = new ARgs();
FormStringControl fsc;
;
args.name(formstr(InventTable));
fr = new FormRun(args);
fr.init();
// Neue Control (Tabellenfeld "AltItemID") im Design anfügen
fsc = fr.design().addDataField(fr.dataSource(1).id(),fieldnum(InventTAble, altitemid));
fr.run();
fr.wait();

In dem o.g. Beispiel wird das Feld Ersatz-Artikelnummer direkt im Design der Form eingefügt. Bei einem Datenfeld ist es wichtig das als Datasource immer die ID der FormDatasource übergeben wird um die richtige Referenz zu erhalten. Anstelle eines Datenfeldes kann natürlich auch jedes andere Formcontrol eingefügt werden, was wiederrum anderen Möglichkeiten der weiteren Verwendung bieten kann. Möchte man kein Datenfeld erzugen kann mit

.addControl(...

auch ein beliebiges FormControl hinzugefügt werden. Über den Enum FormControlType wird die Art des Controls in Methode addControl als erstes Parameter festgelegt.

element.design().addControl(FormControltype::String, "Neue Control");


Um das Feld Ersatz-Artikelnummer Beispieltsweise im Überblick - Grid anzuzeigen muss die Erzeugung der FormControl auch auf dem Grid geschehen.

Hierzu wird die FormGridControl „Grid“ aus der Maske gesucht und dort das neue Feld hinzugefügt.

FormRun fr;
Args args = new ARgs();
FormGridControl fgc;
;
args.name(formstr(InventTable));
fr = new FormRun(args);
fr.init();
// Zugriff auf FormControl "Grid" aus dem Design
fgc = fr.design().controlName("Grid");
// Dem Grid ein neues Control hinzufügen
fgc.addDataField(fr.dataSource(1).id(),fieldnum(InventTAble, altitemid));
fr.run();
fr.wait();

Eine neues FormControl wird also immer vom Vater (Übergeordneten) Control aus erzeugt, im zweifelsfall also mindestens auf dem Design der Form.

parentControl.addControl(…

es muss immer die FormControl gefunden werden auf dem die FormControl erzeugt werden soll. Neue FormControls können nur auf Container FormControls, wie Grid, TabPage, Group usw. erzeugt werden.

Über X++ können alle Eigenschaften der FormControls manipuliert werden. Je nach ControlTyp stehen unterschiedliche Eigenschaften zur Verfügung.

FormGroupControl groupCtrl;
FormStringControl stringCtrl;
;
//Neue Gruppe im Design einfügen
groupCtrl = element.design().addControl(FormControlType::Group, "Gruppe");
//Neue Zeichenfolge der Gruppehinzufügen
stringCtrl = groupCtrl.addControl(FormControlType::String, "ZeichenFolge");
//Zuweisung der DataSource
stringCtrl.dataSource(element.dataSource().name());
//Zuweisung des Feldes
// stringCtrl.dataField(fieldnum(InventtAble, ItemID));
//Zuweisung einer Methode der Tabelle
stringCtrl.dataMethod("ItemName");

stringCtrl.enabled(false);
stringCtrl.visible(true);

In dem Beispiel wird eine Gruppe direkt im Design erzeugt und in dieser Gruppe wird ein Zeichenfolge eingefügt, die über die Methode ItemName auf der InventTable Daten anzeigt.


Überschreiben der Standardevents ausserhalb der Form

Auch Events wie Modified oder Validate können ausserhalb der Form überschrieben werden. Hierzu muss der aufgerufenden Form mitgeteilt werden, das dieses nicht mehr in der Maske passieren soll, sondern in dem aufrufenden Element (zb. In der Class).

// Das Überschreiben der Methoden erlauben
fR.controlMethodOverload(true);
// Das Objekt festlegen in der die Methoden definiert sind
fR.controlMethodOverloadObject(this);

Nun kann man nicht pauschal für alle Controls eine Methode definieren, sondern muss für jede Control eine eigende Definition erzeugen. Die Syntax ist aber immer die gleiche und fängt mit der Benamung der Methode an. D.h. wurden 10 FormControls zur Laufzeit eingefügt müssen für jeden Event der verändert werden soll eine neue Methode erzeugt werden. Eine Ausnahme, die aber oft zur Regel werden kann, ist das nur für jeden unterschiedliche Namen der FormControls zusätzliche Methode erzeugt werden müssen.

           ControlName_MethodenName

Als Beispiel kann die Control ctrlInventTable_ItemId (Artikelnummer im Reiter Übersicht) wie folgt aussehen um die Änderung auf diesem Feld abzufragen

Public boolean ctrlInventTable_ItemID_modified()

   FormStringControl c = formrun.controlCallingMethod(); // Die  FormControl, von der der Aufruf erfolgt
   boolean ret; 
   ; 
   ret = c.modified(); // Super() der aktuellen FormControl
aufrufen ->modified

   return ret;
}

Oben genannte Methode entspricht der Standard Methode "modified" auf der FormControl "ctrlInventTable_ItemID"

public boolean modified()
{
    boolean ret;

    ret = super();

    return ret;
}

In den og. Fall hätte man sich das Überschreiben der Methode Modified sparen können, da nichts gemacht wird als die Standardfunktion aus Dynamics AX aufzurufen. Es werden aber alle wichtigen Elemente dargestellt um einen ordnungsgemäße Funktionalität zu gewährleisten.

Achtung: Gibt es mehrere Controls in der Form die dieselben Namen haben, werden auch die überschriebenen Methoden von all diesen Controls benutzt! Somit muss die Unterschreidung welche Control das Event gerade ausgelöst hat hier abgefragt werden. Es können aber nur Controls die zur Laufzeit per X++ erzeugt wurden den gleichen Namen erhalten. Beim "normalen" Erstellen einer Form kann das nicht vorkommen...

Friday, April 06, 2007 5:24:45 PM (Mitteleuropäische Zeit, UTC+01:00)  Mathias Füßler  #    Comments [0] - Trackback
 | 

 Monday, April 02, 2007

Ab der Version 4.0 von Dynamics AX ist es möglich, an einem AOS, die Neuanmeldung von Benutzern zu sperren.

Um Benutzern die Anmeldung an einem AOS zu untersagen muss der entsprechende AOS auf dem Reiter „Serverinstanzen“ der Maske „Onlinebenutzer“ angewählt werden und der Button „Neue Clients ablehnen“ betätigt werden.
Hierbei wird der AOS in den Status „Belastung“ geschaltet.
Ab diesem Zeitpunkt akzeptiert dieser AOS keine Neuanmeldungen mehr.

 

Mit dem Button „Neue Clients akzeptieren“ kann dies Rückgängig gemacht werden.

Der AOS wird in den Status „Aktiv“ gesetzt.

 

Allerdings bringt das Untersagen von Neuanmeldungen in einer Dynamics AX Umgebung mit nur einem AOS auch einige Gefahren mit sich.

 

Wird der Client bzw. die Benutzersitzung des Benutzers mit administrativer Berechtigung geschlossen, und hat dieser keine weiteren aktiven Sitzungen mit ebenfalls administrativen Berechtigungen geöffnet, so hat sich der Administrator selbst vom System ausgesperrt.
Die Sperrung von Neuanmeldungen kann nun nicht mehr rückgängig gemacht werden.

 

Es ist dann nur noch möglich, den AOS neu zu starten um sich wieder anmelden zu können, da bei einem Neustart eines AOS dessen Status automatisch wieder auf „Aktiv“ gesetzt wird.

 

 

Monday, April 02, 2007 12:39:57 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 | 

 Friday, March 30, 2007

Das man in Microsoft Dynamics Ax mittels Drag&Drop einfach Tabellenfelder (Fields) oder Tabellenfeldgruppen (Fieldgroups) in Masken (Forms) einfügen kann ist allgemein bekannt.
Mittels Tabellenfeldgruppen können die gewünschten Felder in die Masken integriert werden. Dadurch ist das Hinzufügen oder Entfernen der Tabellenfelder auf einfache Art Zentral auf Tabellenebene möglich, ohne die Maske anpassen zu müssen.

Was für mich bis dato noch neu war: Es ist auch möglich auf einem Grid eine Tabellenfeldgruppe zu hinterlegen und direkt diesem FormControl alle Felder, die der Tabellenfeldgruppe hinterlegt wurden, zuzuweisen. Microsoft hat in der aktuellen Version von Microsoft Dynamics Ax 4.0 damit auch schon angefangen, dieses in den Forms aktiv zu nutzen. Bisher ist mir das zumindest noch nicht aufgefallen. In den Masken zur Adressverwaltung ("Address...") werden hier nun auf dem Reiter "Überblick" die Felder mittels Feldgruppe auf dem Grid hinzugefügt (bsp: Form "AddressCountryRegion") oder es wird die Tabellenfeldgruppe direkt dem Grid zugeordnet (bsp: Form "AddressZipCodes").

Die Zuordnung erfolgt immer über die Eigenschaft "DataGroup". Die Eigenschaft "DataSource" muss selbstverständlich auch hierzu vorher gefüllt werden um dann mittels Lookup in der Eigenschaft "DataGroup" eine Auswahl auf alle Tabellenfeldgruppen der aktuell ausgewählten Tabelle zu erhalten. 


Eigenschaft der FormGridControl "Grid"


Nach Auswahl der DataGroup werden alle Felder automatisch dem aktuellen Objekt zugeordnet.


FormGridControl erhält nach Zuweisung der Eigenschaft "DataGroup" alle Felder der ausgewählten Feldgruppe


Man muss aber beachten, das ein manuelles hinzufügen von Feldern nicht mehr funktioniert. D.h. im FormDesigner sieht alles prima aus, alle Felder, auch die manuell hinzugefügten, werden angezeigt, beim Aufruf der Maske sind aber nur die Felder sichtbar, die auch in der aktuellen Tabellenfeldgruppe hinterlegt wurden.
Wird jedoch die Eigenschaft AutoDataGroup auf "Yes" gesetzt ist ein manuelles hinzufügen von Elementen nicht mehr möglich! Auch bereits vorhandene Elemente werde, sofern manuell hinzugefügt und nicht in der aktuellen Feldgruppe hinterlegt, wieder entfernt. Wird eine Feldgruppe mittels Drag&Drop in der Form hinzugefügt, ist die Eigenschaft AutoDataGroup schon standardmäßig auf "Yes" gesetzt.
Es gilt aber immer: Sobald die DataGroup befüllt ist, werden alle Elemente die nicht der aktuellen Feldgruppe auf der Tabelle zugeordnet wurde nicht mehr angezeigt/ berücksichtigt.

Meiner Meinung wieder ein Schritt in die richtige Richtung, denn dadurch lassen sich Anpassungen an einer Form weiter minimieren. Das kann wieder einen verminderten Anpassungsaufwand, speziell bei Upgrades bedeuten, da bei Masken meiner Erfahrung nach mit die größte Zeit aufgewendet werden muss.
Friday, March 30, 2007 10:08:52 PM (Mitteleuropäische Zeit, UTC+01:00)  Mathias Füßler  #    Comments [0] - Trackback
 | 

 Tuesday, March 27, 2007

Wie schon in diesem Artikel beschrieben ist es auch unter Dynamics AX 4.0 möglich, den Text der Titelleiste zu verändern.

Hierzu ein kurzes Beispiel:

void workspaceWindowCreated(int _hWnd)
{
// Put workspace window specific initialization here.
str orgTitleBarText, newTitleBarText;
int posBracket, lenTitle;
;
//Show the configuration file name in the titlebar - START


//Without session id:
orgTitleBarText = WinAPI::getWindowText(_hWnd);
lenTitle = strLen(orgTitleBarText);
posBracket = strScan(orgTitleBarText, "[", 1, lenTitle);

newTitleBarText = subStr(orgTitleBarText, 1, posBracket);
newTitleBarText = strfmt("%1%2]", newTitleBarText, xInfo::configuration());

WinAPI::setWindowText(_hWnd, newTitleBarText);

//Show the configuration file name in the titlebar - END
}

Die Anpassung der Methode "workspaceWindowCreated" der Klasse "Info" liefert folgenden Text in der Titelleiste:

Hierbei wird der Name der Dynamics AX Client Configuration, die für diese Session verwendet, wird innerhalb der eckigen Klammern angezeigt.

Tuesday, March 27, 2007 8:22:24 AM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [6] - Trackback
 | 

 Monday, March 26, 2007

Es gibt Tage da wundert man sich über Dinge mit denen man alltäglich zu tun hat...
So geschehen mit QueryRanges und deren QueryValues...

Beim Definieren einer QueryRange ist darauf zu achten, dass die QueryValue als Datentyp der QueryRange festgelegt wird. Wird beim Filtern auf einem Preis (Real) ein String als Kriterium festgelegt erhält man je nachdem ob dieser String in einen gültigen Realwert gewandelt werden konnte unterschiedliche Resultate. Möglicherweise werden, wenn ein ungültiges Kriterium festgelegt wurde, alle Datensätze zurückgegeben.



Monday, March 26, 2007 7:13:25 PM (Mitteleuropäische Zeit, UTC+01:00)  Mathias Füßler  #    Comments [0] - Trackback
 | 

Unter Microsoft Dynamics AX 3.0 war es recht einfach den Text in der Titelleiste zu verändern. Dies war sogar auf mehere verschiedene Arten möglich.
Unter Dynamics AX 4.0 funktionieren diese bekannten Methoden leider nicht mehr.

Allerdings existiert unter Dyanamics AX 4.0 eine neue Möglichkeit den Text in der Titelleiste zu verändert.
Hierfür ist es nur erforderlich die Methode "workspaceWindowCreated" der Klasse "Info" zu überschreiben.

Wie dies genau gemacht werden kann, ist in dem Artikel Configuration in title bar auf Axaptapedia beschrieben.

Monday, March 26, 2007 7:13:22 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 | 

 Friday, March 23, 2007

Der Umgang mit Queries ist auch in Microsoft Dynamics AX sehr einfach, sofern man mit den Objekten vertraut ist.
Angefangen mit einem einfachen Query, der kann entweder auf Basis eines im AOT (Query) definierten Querys erzeugt werden
oder auch komplett neu mit x++ erzeugt werden kann.

Folgende Okjekte werden verwendet

Query (Abfrage)
QueryRun (führt die Abfrage aus)
QueryBuildDataSource (DataSource in der Abfrage)
QueryBuildRange (Range (Einschränkung) auf der DataSource)


Kurzes Beispiel:

Das Ergebniss ist die Ausgabe alle Artikel bei denen die Artikelgruppe "Teile" hinterlegt wurde.

Auf Basis eines im AOT definierten Queries

//Ein neues Query Objekt, verwendet wird die Query (Im AOT) "InventTable"
Query queryInventTable = new Query(querystr(InventTable));

QueryRun queryRun;
QueryBuildDataSource queryDS;
QueryBuildRange queryRange;
InventTable inventTable;
;
// Datasource zuordnen
queryDS = queryInventTable.dataSourceTable(tablenum(InventTable));

// Prüfen ob Range schon vorhanden
if (queryDS.findRange(fieldnum(InventTable, ItemGroupID)))
queryRange = queryDS.findRange(fieldnum(InventTable, ItemGroupID));
else
queryRange = queryDS.addRange(fieldnum(InventTable, ItemGroupID));

queryRange.value("Teile");

// queryRun mit dem aktuell neu erstellen query auf Basis des Queries "InventTable" erzeugen
queryRun = new QueryRun(queryInventTable);

// alle Datensätze ausgeben
while (queryRun.next())
{
inventTable = queryRun.get(tablenum(InventTable));
print InventTable.ItemID;
}
pause;


Komplett in X++ definierte Query

//Ein neues leeres Query Objekt
Query query = new Query();
QueryRun queryRun;
QueryBuildDataSource queryDS;
QueryBuildRange queryRange;

InventTable inventTable;
;
// Datasource (Tabelle InventTable) hinzufügen
queryDS = query.addDataSource(tablenum(InventTable));

// Range definineren
queryRange = queryDS.addRange(fieldnum(InventTable, ItemGroupID));
queryRange.value("Teile");

// alternative
// Range definineren
query.dataSourceTable(tablenum(InventTable)).addRange(fieldnum(InventTable, ItemGroupID)).value("Teile");

// queryRun mit dem aktuell neu erstellen query erzeugen
queryRun = new QueryRun(query);

// alle Datensätze ausgeben
while (queryRun.next())
{
inventTable = queryRun.get(tablenum(InventTable));
print InventTable.ItemID;
}

Bei diesem einfachen Query macht es keinen Unterschied ob im AOT definiertes Query verwendet wird, oder ob man die definition komplett in X++ vornimmt.
Benutzt man ein Query das schon irgendwo definiert wurde und möchte dieses verwenden, sollte man immer auf schon vorhandene Objekte (siehe findRange)
zurückgegriffen werden sofern schon vorhanden.Ansonsten fügt man immer wieder dasselbe Objekt hinzu, bei Ranges hat das dann zufolge, das in der Abfrage nicht mehr auf die aktuell
definierte Range zurückgegriffen wird, sondern auf alle davor definieren Ranges ebenfalls.
Das kann sehr gut in dem unterem Beispiel getestet werden, indem man einfach eine neue Range auf demselben Feld wie schon zuvor definiert hinzufügt und eine anderen
Wert festlegt.

Das Ergebnis ist die Ausgabe alle Artikel bei denen die Artikelgruppe "Teile" und "Lampen" hinterlegt wurde.

// Range definineren
queryRange = queryDS.addRange(fieldnum(InventTable, ItemGroupID));
queryRange.value("Teile");

// Neue Range auf demselben Feld. ein andere Wert wird festgelegt
queryRange = queryDS.addRange(fieldnum(InventTable, ItemGroupID));
queryRange.value("Lampen");


Der Zugriff auf einzelne Elemente (wie DataSource oder Range) der Query ist jeweils immer gleich. Bei der DataSource sollte immer beachtet werden, dass es ggf. mehrere Objekte vom selben Typ (Tabelle) geben könnte. Ein gutes Beispiel hierfür sind wieder die Artikel (InventTable) mit den drei verknüpften Lagermodulparameter (InventTableModule). Wir hierbei der Zugriff auf die DataSource über tablenum gesteuert erhält man immer ein und dieselbe DataSource und nicht wie evtl gewünscht alle drei DataSources.
Der Zugriff sollte dann über den DataSource-Namen erfolgen, der immer eindeutig ist, sogar dann wenn das hinzufügen per x++ geschieht.

Hinzufügen von drei neuen DataSources und der Zugriff auf jeder DataSource der aktuellen Abfragen, inkl. Ausgabe des Namen

Query query = new Query();
counter dsCount;
;
// Datasource (Tabelle InventTable) hinzufügen
queryDSInvent= query.addDataSource(tablenum(InventTable));

//neue relation auf Lagermodulparameter (3mal)
queryDSInvent.addDataSource(tablenum(InventTableModule)).relations(true);
queryDSInvent.addDataSource(tablenum(InventTableModule)).relations(true);
queryDSInvent.addDataSource(tablenum(InventTableModule)).relations(true);

/ Ausgabe aller DataSources die im aktuellen Query vorhanden sind
for (dsCount = 1; dsCount <= query.dataSourceCount(); dsCount++)
{
print query.dataSourceNo(dsCount).name();
}
pause;

Wird der DataSource kein Name zugewiesen erzeugt Dynamics AX automatisch einen eindeutigen Namen (Tabellenname_Zähler).

Hinterlegt man nun noch bei den LagerModulparametern eine Range (Verkauf, Einkauf, Lager) auf den Lagertyp hat man im groben die Standardquery (es fehlt noch die Lagerortverwaltung Tabelle) der Artikelmaske nachgebaut.

// Lager
queryDS = queryDSInvent.addDataSource(tablenum(InventTableModule));
queryDS.relations(true);
queryDS.joinMode(JoinMode::InnerJoin);
queryRange = queryDs.addRange(fieldnum(InventTableModule,ModuleType));
queryRange.value(queryValue(ModuleInventPurchSales::Invent));

Prinzipiell kann man mit dieser Query auch die Query auf der Maske InventTable überladen. Diee Anzeige der Artikel klappt weiterhin wunderbar
nur die Referenzen auf die zusätzlichen FormDataSources geht hierbei verloren. Dadurch funktioniert die Anzeige und Bearbeitung der Daten aus diesen FormDataSources dann nicht mehr.

Spasseshalber aber noch der Quellcode, der auch die Query der Form überlädt.

Job_QueryEinfachFormRun.xpo (3,05 KB)
Friday, March 23, 2007 10:42:02 PM (Mitteleuropäische Zeit, UTC+01:00)  Mathias Füßler  #    Comments [0] - Trackback
 | 

 Friday, March 16, 2007

Viele Wege führen nach Rom, so auch in Microsoft Dynamics AX beim Anpassen der Masken (Forms).


Eine Form direkt anzupassen kann auf den ersten Blick immer ein einfachere Weg sein, nur sollte man bedenken das es hier bei einem Update mit die meißten Schwierigkeiten bzw. die meißte Arbeit geben kann. Dabei kann man fast jede Anpassung an der Maske auch über X++ steuern.

Hier ein paar einfache Beispiele dafür:

Manipulation der Datasource.

FormRun        fr;
Args           args = new Args();
FormDataSource fds;
;
// Form Name InventTable
args.name(formstr(InventTable));
fr = new FormRun(args);
// init der Form
fr.init();
// Datasource der Form
fds = fr.dataSource(1);
// Neue Range auf der Datasource erzeugen
fds.query().dataSourceTable(tablenuM(InventTable)).addRange(fieldnum(InventTable, itemID)).value(“Wert”);
fr.run();
fr.wait();

In dem o.g. Beispiel wird eine neue Range auf der Datasource „InventTable“ erzeugt, ohne einen direkten Eingriff auf der Form zu machen.

Weitere Möglichkeiten der Datasource Manipulation

// Datasource darf nicht editierbar sein
fds = fr.dataSource(1);
fds.allowEdit(False);

Prinzipiell kann über X++ alles manipuliert werden, was auch direkt in der Form manipuliert werden kann.

Der Zugriff auf einzelne Felder der Datasource erfolgt über die Methode object(), es muss nur noch die Kennung des Objektes übergeben (Feldnummer) um Zugriff zu erhalten.

// Artikelnummer ausblenden
fds = fr.dataSource(1);
fds.object(fieldnum(InventTable, ItemID)).visible(false);


Man erhält den vollen Zugriff auf die Eigenschaften und kann nach belieben die Objekte verändern. Das einzige was zusätzlich noch gemacht werden muss, ist den Aufruf der Maske zu verändern. So kann ein neues MenuItem erstellt werden, welches das Ursprüngliche MenuItem ersetzt oder es wird einfach das MenuItem verändert. So wird anstelle der Maske eine Klasse aufgerufen, die die Manipulation an der Maske vornimmt. Performanceeinbußen habe ich noch nicht oder nur im geringen Maße feststellen können.

Die einzigen wirklichen Probleme die ich bei solchen Formanpassungen hatte, war das Einfügen einer neuen Formdatasource. Man kann die FormDatasource zur Laufzeit in die Form einfügen, leider muss die Form aber geschlossen und wieder neu aufgerufen werden um Zugriff auf die Formdatasource zu erhalten und um diese dann im Design der Form nutzen zu können. Bei manchen Forms kam es beim Neuaufruf der Form zu sehr seltsamen Verhalten, wie zum Beispiel,  das Formcontrols die automatisch Deklariert wurden (Autodeclaration = Yes) ihre Wertigkeit verlieren. So wurde bespielsweise aus einer FormStringControl eine FormDataSource. Hier ist also bei der Anwendung Vorsicht geboten.

Anbei noch ein Job der die Kreditorenmaske manipuliert. Es wird hier eine neue Datasource (Bestellungen) angefügt und in einem neuen Grid angezeigt. Seltsamerweiße klappt dieses Vorgehen mit der Kreditormaske sehr gut, bei den Debitoren bekam ich nur o.g. Fehlermeldungen bei Aufruf.
 

Der ursprünglich Link scheint nicht zu funktionieren. Hier nochmal ein neuer Versuch

Job_MaskeKreditorBestellung.xpo (2,52 KB)

Und wenn das auch nicht funktionieren sollte, nochmal als Text...

// Changed on 15 Mär 2007 at 21:14:18 by jinx (starside.eu)
// Manipulation des Aufrufes der Maske Kreditoren
static void MaskeKreditorBestellung(Args _args)
{
FormRun fr;
Args args = new Args();
FormRun neuformRun;
FormBuildDataSource formBuildDataSource;
FormGridControl fgc;
FormGroupControl fGroupCtrl;
;
// Form Name VendTable -> Kreditoren
args.name(formstr(VendTable));
fr = new FormRun(args);

// Neue DataSource einfügen
formBuildDataSource = fr.form().addDataSource("PurchTable");
// Tabelle der Datasource zuordnen
formBuildDataSource.table(tablenum(PurchTable));
// Datasource mit der Tabelle "VendTable" verknüpfen
// Wichtig: Unbedingt den Namen der Datasource übergeben!
formBuildDataSource.joinSource(fr.form().dataSource(1).name());
// Art der Verknüpfung festlegen
formBuildDataSource.linkType(1);

formBuildDataSource.allowCreate(false);
formBuildDataSource.allowDelete(false);
formBuildDataSource.allowEdit(false);

// Neue Gruppe Erzeugen
fGroupCtrl = fr.form().design().addControl(FormControlType::Group,"PurchOderGroup");
// Die Neue Datasource der neuen Gruppe zuordnen
fGroupCtrl.dataSource(formBuildDataSource.id());
// Neues Grid erzeugen
fgc = fGroupCtrl.addControl(FormControlType::Grid,"PurchOderGrid");
// Die Neue Datasource dem neuen Grid zuordnen
fgc.dataSource(formBuildDataSource.id());
// Feld "Bestellnummer" ins Grid einfügen
fgc.addDataField(formBuildDataSource.id(), fieldnum(PurchTable, PurchID));
// Feld "Kreditorennummer" ins Grid einfügen
fgc.addDataField(formBuildDataSource.id(), fieldnum(PurchTable, OrderAccount));

// Die Form in der wir gerade die Datasource eingefügt haben, den Args übergeben
args.object(fr.form());

// Neue FormRun aufgrund der Manipulierten Form erzeugen und aufrufen
neuformRun = classFactory.formRunClass(args);
neuformRun.init();
neuformRun.run();
neuformRun.wait();
}


Friday, March 16, 2007 7:19:27 PM (Mitteleuropäische Zeit, UTC+01:00)  Mathias Füßler  #    Comments [0] - Trackback
 | 

 Tuesday, February 27, 2007

Kaum ist Version 4 (4.01) von Microsoft Dynamics AX erschienen, tauchen auch schon die ersten Informationen über die weitere Zukunft von Microsoft Dynamics AX auf.

Wie in diesem Bericht (http://www.directionsonmicrosoft.com/sample/DOMIS/update/2006/08aug/0806dwtpcp.htm) zu lesen ist, soll die nächste Version von Microsoft Dynamics AX ein UI ähnlich dem von Microsoft Office 2007 bekommen.
Weiterhin soll es noch eine tiefere Integration in bestehende Microsoft Technologien geben. Ein Beispiel wäre hier die geplante Verlagerung der Entwicklungsumgebung ins Visual Studio.

Genauere Informationen können dem oben genannten Bericht entnommen werden.

Ein erster Screenshot (Prototype) des nächsten Microsoft Dynamics AX ist hier zu finden:
http://www.directionsonmicrosoft.com/sample/DOMIS/update/2006/08aug/0806dwtpcp_illo.htm

Tuesday, February 27, 2007 7:10:50 PM (Mitteleuropäische Zeit, UTC+01:00)  Axel Kühn  #    Comments [0] - Trackback
 | 

 Monday, February 19, 2007

Auch in Microsoft Dynamics AX kann man mittels ADO (ActiveX Data Objects) auf praktisch jede Art von Datenbanken zugriffen. Die Benutzung ist denkbar einfach und wird durch folgende Objekte gehandhabt.

  • CCADOConnection (Datenbankverbindung)
  • CCADORecordSet (Datensatzsammlung)
  • CCADOFields (Feldsammlung)
  • CCADOField (Datenfeld)

Wurde schon eine ODBC Verbindung konfiguriert ist die Verwendung der CCADOConnection denkbareinfach und könnte für eine ODBC Verbindung mit dem Name "altDatenKunden" die Beispielsweise auf eine mySQL Datenbank verweist wie folgt aussehen.

CCADOConnection adoConnection = new CCADOConnection();
adoConnection.open("DNS=altDatenKunden");

Die Parameter werden hier immer als string übergeben und können sich Unterscheiden je nachdem auf welche Datenbank zugegriffen werden soll.

Um CCADORecordSet zu inizialisieren muss vorher erst noch eine SQL Anweisung definiert werden. Die SQL Anweisung wird als String zusätzlich zur CCADOConnection dem Konstruktor übergeben.

str SQLstring = "SELECT * From Artikeltabelle";
CCADORecordSet adoRecordSet = new CCADORecordSet();

adoRecordSet.open(SQLstring, adoConnection);

Über adoRecordSet enthält man nun Zugriff auf alle Datensätze aus der Tabelle "Artikeltabelle". Mittels einer Schleifen kann man nun auf jeden einzelen Datensatz zugreifen.

while (!adoRecordSet.EOF())
{
adoRecordSet.moveNext();
}

Um nun Zugriff auf die Daten zu erhalten brauchen wir noch CCADOFields, bzw. CCADOField

CCADOFields adoFields;
CCADOField adoField;

adoFields = adoRecordSet.fields();
adoField = adoFields.itemName("Artikelnummer");

print adoField.value();

Durch diese Anweisungen wird nun das Feld "Artikelnummer" des aktuellen Datensatzes ausgegeben.

Direkte Zuweisungen zum Datenfeld:

  • Über den Feldnamen  

    adoField = adoFields.itemName("Artikelnummer");

                       
  • Über die Feldnummer 

    adoField = adoFields.itemIDx(1);

Über die Methode

adoFields.count()

erhält man die gesamte Anzahl der Felder, der aktuellen Tabelle und erhält so Zugriff auf jedes einzelne Tabellenfeld auch ohne deren Benennung und/ oder Anzahl zu kennen..

for( i =1 ; i<= adoFields.count() ; i++)
{
adoField = adoFields.itemIdx(i);
print adoField.value();
}