Manchmal ist es notwendig eine Abfrage (Query) über den Quellcode zu manipulieren um dort bspw. Einschränkungen vorzubelegen. Am häufigsten ist mir das in letzter Zeit bei Berichten passiert, es ist aber auch schon vorgekommen das ich komplette Abfragen in Tabellen abgespeichert habe um diese später nochmals benutzen bzw. manipulieren zu können. In beiden Fällen bin ich dabei auf die gleichen Probleme gestossen.
Möchte man Abfragen (bspw. In Berichten) mittels Quellcode manipulieren kann es Aufgrund der Nutzungsdaten zu Problemen in der Anzeige und in der Ausführung der Abfrage kommen. Denn trotz manueller Änderung an der Abfrage werden beim Aufruf des Dialoges der Abfrage noch die Nutzungsdaten des letzten Aufrufes der aktuellen Abfrage genommen/ geladen. Das führt dann dazu das die Veränderungen nicht angezeigt oder beim Ausführen benutzt werden, sondern genau die Einschränkungen/ Einstellungen die auch im Dialog erscheinen.
Um das zu verhindern können mittels der Klasse SysQueryRun noch zusätzliche Einstellungen getroffen werden. Hierzu wird eine neue Instanz von SysQueryRun angelegt, die mit der aktuellen Abfrage initialisiert wird.
Mittels
sysQueryRun.promptLoadLastUsedQuery(false);
wird festgelegt, dass die Nutzungsdaten des letzten Aufrufes nicht vorbelegt bzw. verwendet werden. Somit hat man nun die Möglichkeit die Abfrage mittels Quellcode zu manipulieren, ohne das es hier zu Problemen der Nutzungsdaten kommt, da diese nicht mehr berücksichtigt werden.
Im Bericht kann das beispielsweise so aussehen
public void init() { SysQueryRun sysQueryRun;
super();
element.query().dataSourceNo(1).addRange(fieldnum(Tabelle, Feld)).value("NeuerWert");
sysQueryRun = new SysQueryRun(element.query());
sysQueryRun.promptLoadLastUsedQuery(false);
element.queryRun(sysqueryRun); }
Weitere nette Möglichkeiten bieten noch folgende Methoden
- sysQueryRun.promptAllowSave(boolean); - speichern der Abfrage erlauben
- sysQueryRun.promptShowSorting(boolean); - Sortierung anzeigen
- sysQueryRun.promptAllowAddRange(QueryAllowadd); - Hinzufügen neuer Einschränkungen erlauben
- sysQueryRun.promptAllowAddSorting(QueryAllowadd); - Hinzufügen neuer Sortierungen erlauben
- sysQueryRun.promptAllowAddDataSource(boolean); - Hinzufügen neuer Tabellen erlauben#
- sysQueryRun.promptShowReset(boolean); - Zurücksetzten der Abfrage
- sysQueryRun.promptSaveQueryPrUser(boolean);
Da SysQueryRun von QueryRun abgeleitet ist, kann SysQueryRun von jeder Standardquery initialisiert werden um dann die erweiterten Funktionen von SysQueryRun nutzen zu können. Noch eine kleine Besonderheit, die mir bei den Berichten und deren Aufruf aufgefallen ist: Wird der Bericht direkt (ohne MenuItem) aufgerufen erhält man immer die Originalen inkl. der per Quellcode getroffenen Einschränkungen angezeigt. Wir der Bericht aber über ein MenuItem aufgerufen erhält man die zu letzt vom Benutzer inkl. der per Quellcode getroffenen Einschränkungen/ Einstellungen. Dabei werden die evtl. vorhandenen Einstellung überschrieben, wenn auf vorhandene Element zugriffen wird (findRange (Wert wird überschrieben) anstelle von addRange (alter Wert wird beibehalten und ein neuer hinzugefügt)). Somit gehen in diesem Fall nicht alle vom Benutzer festgelegten Einstellungen verloren.
Microsoft Dynamics AX bietet eine performante Möglichkeit viele
Datensätze einzufügen. Hierzu wird die Klasse RecordInsertList
verwendet.
RecordInsertList recordList; CustTable custtable; ;
recordList= new RecordInsertList(tableNum(custtable));
while { //TODO: Datensätze erzeugen ohne insert aufzurufen
recordList.add(custtable); //Datensatz der Liste übergeben }
recordList.insertDatabase();// Datensätze einfügen
Hierbei werden die Datensätze nicht mehr sofort in die Datenbank
geschrieben werden, sondern im RecordListInsert Buffer lokal
zwischengespeichert. Die dort enthaltenen Datensätze werden spätestens
beim Aufruf der Methode insertDatabase() -das Einfügen der Datensätze
wird hier vom Kernel gesteuert, der einen geeigeneten Zeitpunkt zum
Einfügen auswählt- in die Datenbank geschrieben.
Bei meinem Versuch auf einem VirtualPC habe ich eine Geschwindigkeitsvorteil von ca. 25% erzielt.
Im AXforum.info
(eines der größten Dynamics Foren) kann man noch einen Testjob finden,
der deutlich macht, was an Zeit eingespart werden kann, wenn mit
RecordInsertList gearbeitet wird.
Update
Bei der Instanzierung gibt es noch optionale Parameter wie
- Insert Methode der Tabelle überspringen [Default=false]
- Datenbanklog überspringen [Default=false]
- Alerts überspringen [Default=false]
- AOS Validierung überspringen [Default=false]
Ist bei der aktuellen Tabelle einer dieser Punkte (Insert Methode,
Datenbanklog...) vorhanden, müssen diese dann mittels der Parameter
übersprungen werden, sonst wird aus dem Bulk-Insert wieder ein
Single-Record-Insert!
Vielen Dank an SebDra für diese Informationen!
Möchte man alle Datensätze einer Tabelle löschen, kann hierfür der Befehl "Delete_From" verwendet werden.
Dies funktioniert soweit und es ist auch nichts gegen diese "Art" des Löschen einzuwenden. Wenn allerdings in der Tabelle mehrere millionen Datensätze gespeichert sind, dauert dies schon recht lange (mehrere Stunden).
Um das Löschen aller Datensätze einer Tabelle zu beschleunigen, kann der SQL Server Befehl (Transact SQL) "TRUNCATE TABLE" verwendet werden.
Dieser Befehl erfernt alle Zeilen aus einer Tabelle, ohne die einzelnen Löschungen zu protokollieren. Der "TRUNCATE TABLE" Befehl ist wesentlich schneller und verwendet weniger Systemressourcen als der "Delete" Befehl.
Microsoft Dynamics AX unterstütz diesen Befehl leider nicht direkt. Somit muss der Aufruf von "TRUNCATE TABLE" über eine ADO-Connection oder in einem der SQL Server Verwaltungs-Tools erfolgen.
Update: Der "Truncate Table" Befehl ist doch in Dynamics AX implementiert. Und zwar wird er durch die Methode "tableTruncate" der Klasse "SqlDataDictionary" implementiert.
Beispiel zur Verwendung:
SqlDataDictionary sqlDict; ; sqlDict = new SqlDataDictionary(); sqlDict.tableTruncate(tablenum(SysDataBaseLog));
Weitere Informationen zum "TRUNCATE TABLE" Befehl können über das MSDN bezogen werden. http://msdn2.microsoft.com/de-de/library/ms177570.aspx
Das man in Microsoft Dynamics AX mittels SYSCompare nicht nur Quellcode oder AOT Objekte, sondern auch Datensätze vergleichen kann zeigt das Tutorial "Tutorial_CompareContextProvider". Dieses Tutorial veranschaulicht wie man mittels SysCompareContextProvider und SysComparable die Unterschiede herausfindet und darstellt. Die Darstellung des Datensatzvergleichs ist hier genauso aufgebaut, wie die Darstellung des Quelltextvergleichs. Ich habe mich mal darangesetzt und versucht diese Funktionalität im gesamten Dynamics AX für jeden Benutzer zu integrieren. Prinzipiell habe ich mich an dem Tutorial orientiert und es in der Hinsicht verändert, das es nicht nur für die Debitortabelle funktioniert, sondern für jede beliebige Tabelle in jeder beliebigen Maske. Für die Anzeige der Datensätze in der Auswahl, habe ich die Felder TitleField1 und TitleField2 genommen, die an der Tabelle festgelegt werden können. Wurd keine Auswahl getroffen, wird auch nichts angezeigt. Die zu vergleichenen Datensätze werden mittels des aktuell aktiven Datensatz in der Maske bestimmt. Die auszuwählen Datensätze werden anhand der Abfrage der Maske bestimmt. Das heißt das man bei den Auftragsposition in der Aufragsmaske nur die Auftragspositionen für den aktuell ausgewählten Auftrag angezeigt bekommt. Wird mehr als ein Datensatz markiert, werden nur die markierten Datensätze in der Auswahl sichtbar. Bei zwei markierten Datensätzen können diese sofort über "Vergleichen" verglichen werden. Der Aufruf der Funktion erfolgt in jeder Maske mittels Shift+Enter. Es können auch mehrere Vergleiche hintereinander aufgerufen werden. Diese Funktion ist in jeder Maske, auch im Tabellenbrowser verfügbar. Anwendungsbeispiel
Aufträge Auftragsmaske: Vergleich der Aufträge. Es werden nur die markierten Datensätze angezeigtAufragspositionen
Auftragsmaske: Vergleich der Auftragspostion. Es werden alle Datensätze zum aktuellen Auftrag angezeigt.
Das Ergebniss wird dann in einer neuen Maske angezeigt. Ergebniss des Datensatzvergleiches
Für die Integration in die Maske musste ich die Klasse SysSetupFromRun überschreiben. Dort habe ich auch festgelegt wie der Datensatzvergleich aufgerufen wird. Leider konnte ich nur bereits vorhandene TaskIds verwenden und habe mich für Shift+Enter entscheiden. Dieses kann natürlich verändert werden, dazu ist nur ein Blick in das Makro Task erforderlich. Wie immer auch hierzu das komplette Projekt als erste Beta Version (geschrieben in Microsoft Dynamics AX 4.01) als Download. Labels, Security Keys usw. habe ich hierfür noch nicht angelegt. Kommentare und Anregungen sind immer gerne willkommen. (Verwendung auf eigende Gefahr, es wird keine Haftung übernommen) SharedProject_CompareRecord_Ver_1.0.zip (3,69 KB)
Möchte man die schon gepflegten Daten eines Dynamics AX 3.0 System über exportieren/importieren in ein Dynamics AX 4.0 SP1 System übernehmen, wird bei diesem Vorgang leider eine Fehlermeldung erzeugt, dass ein Importieren in das Dynamics AX 4.0 SP1 System nicht möglich ist, da die Daten aus einem älteren System stammen.
Dies ist soweit auch in Ordnung, da sich Tabellen und Felder von Version 3.x zu Version 4.0 SP1 verändert haben (Namensänderungen, Feldergänzungen, etc.). Da bei einigen Tabellen aber nur Felder weggenommen wurden, könnte man theoretisch die Daten aus dem „alten“ 3.x System übernehmen. Ein Beispiel hierfür ist der Kontenplan (LedgerTable).
Damit dies funktioniert muss aber eine Änderung an der aus dem Dynamics AX 3.x System exportierten „.def“ Datei vorgenommen werden. Die erste Zeile der „.def“ Datei muss so bearbeitet werden, dass diese wie folgt lautet:
Wenn beim Export als Dateityp „Binär“ gewählt wurde: "EXPFORMAT VER. 4.01","Binary"
Wenn beim Export als Dateityp „Komma“ gewählt wurde: "EXPFORMAT VER. 4.01","Comma"
Wurde die „.def“ Datei entsprechend angepasst, kann es zwar sein, dass beim Import Fehlermeldungen über nicht vorhandene Felder erzeugt werden aber der Importvorgang an sich funktioniert nun.
Anzumerken ist nur noch, dass diese Version der „Datenübernahme“ nur für Spezialfälle gewählt werden sollte. Ein Datenupdate, mit den von Microsoft bereitgestellten Tools, sollte vorgezogen werden.
Fragen in Newsgroups bringen mich manchmal auf Ideen. So geschehen heute mit der Frage, wie Methoden automatisch in (Auto)Berichten angezeigt werden können. Ich habe mir mal die Mühe gemacht, den Aufruf des AutoReports aus den Maske so umzuschreiben, das automatisch Methoden (Display Methoden sind angedacht, z.Z. werden alle Methoden berücksichtigt) in den AutoReport integriert. Hierfür ist es notwendig, das auf Tabellenebene eine Methode erzeugt wird deren Name mit "AutoReport" beginnt. Selbstverständlich sollte diese Methode auch einen Rückgabewert enthalten, der im Report angezeigt werden kann. Alle Methoden werden zum Schluss am Ende der Zeile angezeigt, nachdem alle Felder in der Gruppe "AutoReport" gedruckt wurden sind. Dieses ist vorläufig nur ein erster "Schnellschuss"! Für den einen oder anderen bestimm nicht uninteressant, da somit eine beliebige Anzahl von Benutzerdefinierten Methoden in den Autobericht gebracht werden kann. Um diese Funktion zu erreichen habe ich die Maske "SysTableForm" angepasst, da von dort der Aufruf und Aufbau des AutoReports stattfindet. Möglich Erweiterungen die mir spontan noch einfallen:
- Parametrisierung der Postion, sodass die Anzeige nicht immer am Ende erfolgt.
- Erweiterung der Überprüfung der anzuzeigenen Methoden
Getestet habe ich diese Funktion vorerst nur bei Debitoren und Kreditoren. Verwendung auf eigende Gefahr, es wird keine Haftung übernommen. Viel Spass damit
Form_SysTableForm_AddMethod2Report.zip (13,8 KB)
Der einfacher Weg ist natürlich, die Methoden (das funktioniert scheinbar nur bei Display) per Drag&Drop in die jeweilige Tabellengruppe zu ziehen :) Danke an NC der darauf nochmal aufmerksam gemacht hat, denn mir war das so erstmal noch nicht bewusst.
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.
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.
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.
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)
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.
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.
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)
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.
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...
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.
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.
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.
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.
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.
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 LagerModulparameter |