Eine Listbox ist einfach erstellt. Entweder man benutzt eine datengebundene Listbox, indem einfach die Datasource und DataField/ DataMethod im FormControl festgelegt wird oder man befüllt die Listbox manuell.
Das manuell befüllen der Listbox aus einem Tabellenfeld sieht dann beispielsweise so aus:
void initListBox() { counter elementCnt; str elementStr; ; meineListBox.clear(); for (elementCnt = 1; elementCnt <= conlen(Tabelle.ContainerFeld); elementCnt++) { meineListBox.add(onpeek(Tabelle.ContainerFeld, elementCnt)); } }
Über die angegebene Methode werden alle Elemente des aktuellen Containers der Tabelle in die Listbox übertragen.
Hierbei bin ich auf ein seltsames Verhalten der Listbox gestossen. Im aktuellen Fall ist die Darstellung der Listbox einwandfrei. Beim Datensatz wechsel habe ich die Listbox mit den neuen Werten befüllen können. Alles war gut.
Bis auf die Tatsache, dass ich bei der Auswahl in der Listbox nicht das ausgewählt Element angezeigt bekam. Es wurde immer das erste angezeigte Element der ListBox gewählt.
Grund hierfür war oder ist: Meine Listbox befindet sich in einer FormGroupControl, bei der FormGroupControl war automatisch eine DataSource hinterlegt. Das hat scheinbar zu Folge, dass das Event "SelectionChanged" der Listbox ignoriert wurde und ich somit immer der erste Wert über
meineListBox.getText(meineListBox.selection()); erhalten habe.
Nachdem die Datasource aus der FormGroupControl entfernt wurde, funktionierte alles wieder wie erwartet und gewünscht.
Um die FormDataSource einer Form anzusprechen benutzt man das Suffix _ds. So erhält man in der Maske CustTable mit
CustTable_ds
den Zugriff auf die FormDatasource der CustTable.
Es gibt aber noch andere Suffixe, die nicht so häufig verwendet werden und mir bis dato auch unbekannt waren. So erhält man mit dem Suffix _q oder _qr Zugriff auf die Query oder QueryRun der aktuellen FormDataSource.
Das heisst, das mit
CustTable_q
die Query der FormDataSource CustTable und mit
CustTable_qr
die QueryRun der FormDataSource CustTable direkt angesprochen wird.
Hiermit kann man sich also verschiedene Deklarationen oder Zuweisungen sparen und hat direkten Zugriff auf diese Objekte.
- FormDataSource = Zugriff auf den Datensatzpuffer
- FormDataSouceName_DS = Zugriff auf FormDataSource
- FormDataSourceName_Q = Zugriff auf Query der FormDataSource
- FormDataSourceName_QR = Zugriff auf QueryRun der FormDataSource
Das Erstellen von Produktionsaufträgen sollte an sich kein Problem darstellen. Leider ist dem nicht ganz so.
Anders als in anderen Modulen (z.B. Aufträge) von Dynamics AX, existiert hierfür keine Klassenstruktur, welche die entsprechenden Funktionen bereit stellt. Der Dynamics AX Standard erstellt Produktionsaufträge immer über die Maske „ProdTableCreate“. Es gibt aber Situationen, wo für die Erstellung eines Produktionsauftrages keine Maske verwendet werden kann. Ein Beispiel hierfür könnte eine Schnittstelle sein, welche über eine Textdatei die zu produzierenden Waren einließt und entsprechende Produktionsaufträge im System generiert.
Die Frage ist nun, wie erstellt man Produktionsaufträge per Quellcode, damit diese auch „richtig“ im System erzeugt werden (inkl. Stückliste, Arbeitsplan und Lagerbuchung).
- Zuerst muss der Produktionsauftrag mit den Daten des zu produzierenden Artikels initialisiert werden.
- Weiterhin müssen Produktionsmenge und Lieferdatum festgelegt werden.
- Ebenfalls sind die zu verwendende Stückliste und der Arbeitsplan zu definieren.
- Und als letzter Schritt muss der Produktionsauftrag noch erzeugt werden.
Hierbei gilt es aber zu beachten, dass die Erstellung (Speichern in der Datenbank) nicht mit der Methode „insert“ der Tabelle „ProdTable“ geschieht, sondern dass hierfür die Klasse „ProdTableType“ und deren Methode „insert“ verwendet wird. Nur so wird die entsprechende Lagerbewegung / Lagerbuchung im System erzeugt und wenn notwendig Referenzen zu einem Verkaufsauftrag oder einer anderen Produktion hinterlegt.
Um dies zu veranschaulichen ein kurzes Beispiel, in welchem ein neuer Produktionsauftrag erstellt wird.
static void CreateProductionOrder(Args _args) { //Die zu produzierende Menge ProdQtySched productionQty = 1; //Der zu produzierende Artikel ItemId productionItem = "Artikelnummer";
ProdTable prodTable; InventTable inventTable; ; inventTable = InventTable::find(productionItem);
//Initialisierung des Produktionsauftrags prodTable.initValue(); prodTable.ItemId = inventTable.ItemId; prodTable.initFromInventTable(inventTable);
//Lieferdatum festlegen prodTable.DlvDate = today();
prodTable.QtySched = productionQty; prodTable.RemainInventPhysical = prodTable.QtySched;
//Die zu verwendende Stückliste und Arbeitsplan bestimmen prodTable.initRouteVersion(); prodTable.initBOMVersion();
//Produktionsauftrag erstellen prodTable.type().insert(); }
Selbstverständlich sind auch weitere Angaben bei der Erstellung des Produktionsauftrags möglich. Z.B. kann ein Produktionsauftrag auch aus einer Verkaufsauftragsposition erzeugt werden (bei Verwendung der Methode "initFromSalesLine").
Mit Hilfe von SysDictClass kann man wertvolle Erkenntnisse über Klassen gewinnen und abfragen. Ich habe einige Dinge, die ich am häufigsten gebrauche, anhand einfacher Beispiele zusammengetragen.
Dieses kleine Stück Quellcode ermittelt alle abgeleiteten Klassen der Klasse FormControl.
SysDictClass formCtrlClss; ListEnumerator listEnumerator; ;
formCtrlClss = new SysDictClass(classnum(FormControl));
listEnumerator = formCtrlClss.extendedBy().getEnumerator();
while (listEnumerator.moveNext()) { formCtrlClss = new SysDictClass(listEnumerator.current()); print formCtrlClss.name(); } pause;
Diese Funktionalität wird auch im Standard von Microsoft Dynamics AX verwendet, um alle möglichen Funktionen der Stabelverarbeitung abzufragen/ zu ermitteln.
Man kann natürlich auch ermitteln, von welcher Klasse(n) die aktuelle Klasse abgeleitet ist.
SysDictClass dictClass = new SysDictClass(classnum(SalesFormLetter_Invoice)); ; while (dictClass.extend()) { dictClass = new SysDictClass(dictClass.extend()); print dictClass.name(); } pause;
Um zu ermitteln, ob eine Klasse von einer bestimmten Klasse abgeleitet wurde, muss folgendes geschrieben werden.
SysDictClass dictClass = new SysDictClass(classnum(SalesFormLetter_Invoice)); ; // Überprüfen ob die Klasse von der Klasse Object abgeleitet ist print dictClass.isExtending(classnum(Object)); // Überprüfen ob die Klasse von der Klasse FormControl abgeleitet ist print dictClass.isExtending(classnum(FormControl)); pause;
Es kann auch auf die Methoden der Klasse zugegriffen werden.
SysDictClass dictClass = new SysDictClass(classnum(SalesFormLetter_Invoice)); ; //Statische Methode der Klasse SalesFormLetter_Invoice aufrufen, wenn vorhanden if (dictClass.hasStaticMethod("Description")) print dictClass.callStatic("Description");
//Methode der Klasse SalesFormLetter_Invoice aufrufen, wenn vorhanden if (dictClass.hasObjectMethod("canGoBatchJournal")) print dictClass.callObject("canGoBatchJournal", dictClass.makeObject());
pause;
Das ist nur ein Teil der Funktionen, die ich persönlich am Wichtigsten empfinde und hin und wieder benötige.
Heute habe ich eine Email erhalten, in der folgendes zu lesen war:
"...Herzlichen Glückwunsch! Wir freuen uns, Ihnen den Microsoft MVP Award 2008 verleihen zu können..."
Mir wurde der MVP für Microsoft Dynamics AX verliehen. Wow!
Ich möchte mich an dieser Stelle bei allen Teilnehmern der Microsoft Dynamics AX Community bedanken, die es überhaupt ermöglich haben, dass mir dieser Titel verliehen wurde. Besonderen Dank möchte ich an Helmut Wimmer [axaptafreak] und Mathias Füßler [jinx, meinen Co-Autor, oder bin ich seiner? ] aussprechen, die mich immer tatkräftig unterstützt haben.
Natürlich werde ich jetzt nicht aufhören, weiterhin Hilfestellung bei Fragen zum Thema Dynamics AX zu geben. Ich sehe diese Auszeichnung als Ansporn, mich auch weiterhin aktiv in der gesamten Dynamics AX Community zu beteiligen.
Nochmals, danke an euch alle.
In Foren, Newsgroups oder auch in persönlichen Gesprächen ergibt sich oft die Frage, wo man anfangen soll/kann um den Umgang mit Microsoft Dynamics AX zu lernen.
Gerade für Einsteiger oder Anfänger ist es machmal schwer, die Informationen zu finden, die gerade benötigt werden. Dank der Dynamics AX Community, welche zum Glück immer größer und besser wird, stellt das Internet mit seinen vielen Dynamics AX Blogs, Foren, Newsgroups und nicht zuletzt dem Microsoft Dynamics AX Developer Center eine sehr gut Informationsquelle dar.
Trotzdem hört man oft die Frage, ob es denn keine Bücher über Microsoft Dynamics AX gibt. Um etwas mehr Klarheit zu schaffen, welche Bücher über Microsoft Dynamics AX erhältlich sind, hier eine Liste aller Bücher die mir bekannt sind:
Ich habe die Erfahrung gemacht, dass FormControls, die zur Laufzeit direkt auf dem Design der Form erzeugt wurden, sich meisst am rechten Rand befinden.
Sollen diese FormControls nun aber nicht am Rechten, sondern am Linken Rand erscheinen, kann man dieses natürlich schon beim Erzeugen dieser Controls mittels
FormGroupControl.leftMode(FormLeft::LeftEdge)
festlegen.
Die Probleme treten dann auf, wenn mehrere FormControls erzeugt werden und diese am Linken Rand erscheinen sollen.
Werden nun alle FormControls mit FormControl.leftMode(FormLeft::leftEdge) an den Linken Rand verschoben, erhält man dann unter Umständen erstaunliche Ergebnisse – nur nicht die Gewünschten. Mit Hilfe der Methode
FormControl.moveControl(int _controlId [, int _insertAfterId = 0]) kann jedes Control an eine beliebige Stelle verschoben werden. Der Übergabeparameter ist hier die ID des zu verschiebenen FormControls. Diese Methode ist nur bei Container Controls, wie FormGroupControl, FormGridControl oder auf dem Design der Form verfügbar.
Wird nur
ÜbergeordneteFormControl.moveControl(ControlIDderzuveschiebenenControl) verwendet, wird das Control nur nach links (an die erste Position des Übergeordneten Controls) verschoben.
Soll das FormControl hinter einem bestimmten FormControl innerhalb desselben übergeordneten FormControls platziert werden, wird
Übergeordnete.moveControl(ControlIDderzuveschiebenenControl, ControlIDhinterDerdieControlangefügtwerdensoll)
benutzt.
Mit .moveControl() können auch FormControls in das übergeordnete Control eingefügt werden, die vorher nicht in dem übergeordneten Control enthalten waren.
Anbei eine einfache Form, die schon ein FromGroupControl mit zwei Controls enthält, bei der zur Laufzeit zwei neue Controls hinzugefügt werden und die mittels Button nach links verschoben werden.
FormDynamicControlsMove.zip (2,55 KB)
In Kürze soll ein neues Buch über Microsoft Dynamics AX erscheinen. Im Schwerpunkt soll es sich mit dem Thema Qualitätssicherung beschäftigen. Titel des Buchs ist "Quality Assurance for Dynamics AX-Based ERP Solutions".
Ein Auszug der einzelnen Themenpunkte:
Customization Best Practices backed by theory
Learn rapidly how to test Dynamics AX applications
Verify Industry Builder Initiative-compliance of ERP software
Get ready-made testing templates
Code, design, and test a quality Dynamics AX-based ERP solution
Genaueres über das Buch kann man hier erfahren.
Das Buchen von Bestellungen in Microsoft Dynamics AX geschieht über die Klasse „PurchFormLetter“ bzw. einer ihrer konkretisierten (abgeleiteten) Klassen. Jeder Buchungstyp (z.B. Bestätigung oder Rechnung) ist durch eine eigene Klasse abgebildet, welche von der Basisklasse „PurchFormLetter“ abgeleitet ist (siehe Abbildung).

Abbildung 1 - Klassenhierarchie der Klasse „PurchFormLetter“
Vergleicht man die Klassenhierarchie der „PurchFormLetter“ Klassen mit der Klassenhierarchie der „SalesFormLetter“ Klassen, so ist zu erkennen, dass auch das Buchen von Bestellungen vom Prinzip her genau so funktioniert wie das Buchen von Aufträgen (Vergleiche hierzu: Microsoft Dynamics AX API – Teil 3 „Buchen von Aufträgen“).
Deswegen sind auch für das Buchen von Bestellungen im Wesentlichen nur zwei Schritte notwendig.
- Über die Methode „construct“ der Klasse „PurchFormLetter“ ein dem Buchungstyp einsprechendes Objekt erzeugen.
- Über den Aufruf der Methode „update“ die Bestellung buchen.
Hierzu ein Beispiel (Buchen des Lieferscheins für eine Bestellung):
static void PurchPostPackingSlip(Args _args) { PurchFormLetter purchFormLetter; PurchTable purchTable; PurchId purchId; Num packingSlipId; ; //Angabe der Bestellung, für welche der Lieferschein gebucht werden soll. purchId = "00244_049"; purchTable = PurchTable::find(purchId);
//Bestimmen des Buchungstyps durch Angabe des DocumentStatus (Lieferschein). purchFormLetter = PurchFormLetter::construct(DocumentStatus::PackingSlip);
//Festlegen der externen Lieferscheinnummer. packingSlipId = "EXT-100155L";
//Buchen des Lieferscheins. purchFormLetter.update(purchTable, packingSlipId, SystemDateGet(), PurchUpdate::All, AccountOrder::Auto, NoYes::No, NoYes::No, NoYes::No, NoYes::No); }
Einziger Unterschied zu den Auftragsbuchen ist, dass bei der Buchung einer Bestellung die „externe“ Nummer des Belegs (Lieferscheinnummer, Rechnungsnummer, etc.) angegeben werden muss.
Analog zu den Auftragsbuchen, sind auch beim Buchen von Bestellungen umfangreichere oder etwas speziellere Buchungsszenarien möglich (Vergleiche hierzu: Microsoft Dynamics AX API – Teil 3 „Buchen von Aufträgen“).
Alexei Eremenko hat auf seinem Blog einige Artikel über die kommende Version von Microsoft Dynamics AX und dessen neue Features veröffentlicht. Da diese leider in Russich geschrieben sind, hier eine kurze Zusammenfassung:
- Aus Microsoft Dynamics AX 5.0 wird Microsoft Dynamics AX 2009.
- Geänderte Benutzeroberfläche (Office 2007 Style, inkl. Ribbon's).
- Neue "Funktion" Rollcenter, die dem Benutzer schnellen Zugriff auf die, für seine Arbeit, relevanten Daten geben soll.
- Unterstützung des UNION Befehl's für SQL Abfragen, bei Verwendung der Query-Klassen.
- Neuer Exception-Typ "DublicateKeyException" zur Ausnahmebehandlung wenn ein Datensatz schon besteht.
- Die SQL DML bulk Anweisungen erlauben die Verwendung von Inner- und Outer-Joins.
- Zugriff auf das Ergebnis der "update_recordset" Anweisung, um zu bestimmen wie viele Datensätze durch die Operation geändert wurden.
- CrossCompany-Unterstützung für Datenbankabfragen (Daten aus unterschiedlichen Mandaten können in einer SQL Anweisung behandelt werden).
Wer die orginalen Artikel einmal selber lesen möchte, findet die einzelnen Blog-Posts hier: (Die Links verweisen auf die Übersetzung der Artikel ins Englische)
Wird in einem Bericht die Fetch Methode überschrieben sollte man drauf achten, dass die Standardfunktionen auch noch richtig funktionieren. Ein gutes Beispiel hier sind die Einstellungen im Seitenbereich.

Wird in der Fetch Methode nicht super() aufgerufen und mit einer individuellen Abfrage gearbeitet, welche die Daten dem Bericht übergibt, werden die Einstellungen in der Gruppe "Seitenbereich" meist ignoriert. Es werden dann zwar nur die angebenden Seiten gedruckt, die Abfrage wird aber weiter durchgeführt. Das führt dann dazu, dass immer abgewartet werden muss bis die komplette Abfrage ausgeführt wurde, obwohl man evtl. nur einen kleinen Bruchteil der Daten (wie z.B. die erste Seite) benötigt.
Das kann man ganz einfach verhindern indem in der Fetch Methode abgefragt wird, ob die Daten an den Bericht gesendet wurden. Hierzu wird die send(...) Methode benutzt.
public boolean fetch() { Query queryBuild; QueryRun queryRunBuild; InventTrans iTrans; ; if (this.prompt()) { queryBuild = this.query(); queryRunBuild = new QueryRun(queryBuild);
while (queryRunBuild.next()) { iTrans = queryRunBuild.get(tablenum(InventTrans));
// Wurde der aktuelle Datensatz ausgeben if (!this.send(iTrans)) return true; } } return true; }
Es muss also immer überprüft werden, ob die Daten gedruckt werden oder nicht. Das passiert mit:
if (!this.send(meineDaten)) return true;
Für ein Grid-Control kann über die Einstellung "MultiSelect" gesteuert werden, od dieses Control die Auswahl von mehr als einem Datensatz erlaubt.
Gültige Einstellungen sind: Yes - Es können mehrere Datensätze ausgewählt werden. No - Es kann immer nur ein Datensatz ausgewählt werden.
Auswahl eines Datensatzes
Auswahl mehrerer Datensätze
Zugriff auf die aktuelle Selektion (einer oder mehrere) erhält man wie folgt beschrieben:
Ist nur ein Datensatz markiert, bzw. soll mit einfacher Auswahl gearbeitet werden (MultiSelect = No), kann der ausgewählte Datensatz über den aktuellen DataSource-Cursor der Grid-Control DataSource ermittelt werden. Der DataSource-Cursor steht immer auf dem zu letzt ausgewählten Datensatz eines Grid-Control's.
Beispiel:
void clicked() { //CustTable ist die DataSource des Grid-Controls ; //Datenoperationen für den Datensatz ausführen. //Do something.... info(CustTable.AccountNum);
//Angezeigte Datensätze im Grid Control aktualisieren element.lockWindowUpdate(true); CustTable_ds.research(); element.lockWindowUpdate(false); }
Soll eine Mehrfachauswahl möglich sein, reicht der Zugriff auf den aktuellen DataSource-Cursor nicht mehr aus. Um alle ausgewählten Datensätze der DataSource zu erhalten, muss diese mit einer Schleife unter Verwendung der Methoden "getFirst" und "getNext" durchlaufen werden. Hilfreich hierbei ist die Methode "anyMarked", mit welcher ermittelt werden kann ob mehrere Datensätze ausgewählt sind oder nicht.
Auch hierfür ein Beispiel:
void clicked() { CustTable selectedCustTable; Common currentRecord; ; if (CustTable_ds.anyMarked()) //Es sind meherer Datensätze selektiert. { //Ersten selektierten Datensatz ermitteln. selectedCustTable = CustTable_ds.getFirst(1);
while(selectedCustTable) { //Datenoperationen für den Datensatz ausführn. //Do something.... info(selectedCustTable.AccountNum);
//Nächsten selektieren Datensatz ermitteln. selectedCustTable = CustTable_ds.getNext(); } } else //Nur ein Datensatz ist selektiert. { //Selektierten Datensatz ermitteln. currentRecord = CustTable_ds.cursor().data(); selectedCustTable = CustTable_ds.cursor();
//Datenoperationen für den Datensatz ausführen. //Do something.... info(selectedCustTable.AccountNum); }
//Angezeigte Datensätze im Grid Control aktualisieren element.lockWindowUpdate(true); CustTable_ds.research(); CustTable_ds.findRecord(currentRecord); CustTable_ds.refresh(); element.lockWindowUpdate(false); }
Der Quellcode des Beispiels:
Form_GridSelectedRecords.xpo (6.2 KB)
Von Zeit zu Zeit ist es hilfreich sich den Abfragebefehl, der durch eine Query erzeugt wird, zur Laufzeit anzusehen.
Leider unterstützt der Debugger von Microsoft Dynamics AX das Debuggen von Querys nicht. Ein Datenbanktrace durchzuführen ist auch nicht immer sinnvoll, da man durch das Traceprotokoll keine direkte "Verbindung" von Abfragebefehl und Query einsehen kann.
Einen Workaround hierfür gibt es aber.
Die Methode "toString" einer Query DataSource gibt den Abfragbefehl, der durch die Query an die Datenbank geschickt wird, zurück. Dieser kann dann, durch die statischen Methoden des "Debug" Objekts, im Debuggers ausgegeben bzw. angezeigt werden.
Ein Beispiel:
debug::printDebug(query.dataSourceTable(tablenum(CustTable)).toString())
Weiterhin ist unter http://www.axaptapedia.com/DEV_QueryBrowser ein Tool für die Analyse von Querys erhältlich. Dieses Tool funktioniert ähnlich wie der Tablebrowser von Dynamics AX. Mit dem Tool können nicht nur der Abfragebefehl, der durch eine Query erstellt wird, sondern auch die von der Query ermittelten Daten betrachtet und ausgewertet werden.
Eine ausreichende Beschreibung zur Verwendung des Tool's ist ebenfalls auf Axaptapedia (siehe Link weiter oben) zu finden.
Einen ersten Ausblick auf die "neuen" Quellcodeverwaltungsfeatures in Microsoft Dynamics AX 5.0 zeigt der Screencast "Version control in MorphX" auf Channel9.
http://channel9.msdn.com/Showpost.aspx?postid=367024
Ich zitiere: "This screencast is a preview of the version control system integration options in the next release of MorphX - the IDE of Dynamics AX. It shows a side-by-side comparison of the integration options with Team Foundation Server, Visual Source Safe, and MorphX VCS. The latter is a simple, yet powerful alternative without any additional infrastructure requirements. The last half of the screencast gives a demonstration of MorphX VCS."
Durch die neuen Features die MorphX VCS mit sich bringt, sowie die Möglichkeit Visual Studio Team System, oder genauer der Team Foundation Server, (nicht nur) als Quellcodeverwaltung zu verwenden, sollte nun für jeden ein "passendes" Quellcodeverwaltungsystem bereit stehen.
Vielen Dank an dieser Stelle an Michael Fruergaard Pontoppidan (http://blogs.msdn.com/mfp/default.aspx) für diesen und die bisherigen Screencasts über Microsoft Dynamics AX.
Die Version 4.0 von Microsoft Dynamics AX enthält eine neue Funktion ("Nach Raster filtern" oder "Filter by Grid") mit der die Datenfilterung direkt in einem Grid ermöglicht wird.

Die Funktion "Nach Raster Filtern" kann entweder über die Tastenkombination "STRG + G", über die Symbolleiste oder den Menüpunkt "Bearbeiten - Filtern - Nach Raster filtern" aktiviert werden.
Das "Nach Raster filtern" ist per Standard deaktiviert und muss somit immer manuell durch den Benutzer aktiviert werden. Leider wird in den benutzerspezifischen Einstellungen für eine Maske nicht gespeichert ob das "Nach Raster filtern" aktiviert oder deaktiviert ist. Somit muss die Funktion, wenn diese verwendet werden soll, für jede Maske und nach jedem Schließen einer Maske erneut aktiviert werden.
Soll nun für eine Maske die Funktion "Nach Raster filtern" dauerhaft aktiviert sein, ist eine kleine Anpassung im Quelltext der Maske notwendig.
Als erstes muss die Eigenschaft "AutoDeclaration" für das Grid Control, bei welchem die Funktion "Nach Raster filtern" aktiviert werden soll, auf "Yes" gesetzt werden. Weiterhin muss die Methode "run" der Maske, nach dem Aufruf von "super", um diese Quellcodezeilen ergänzt werden:
Grid.enter(); this.task(2855);
"Grid" ist hierbei der Name des Grid Controls, für welches die Funktion "Nach Raster filtern" aktiviert werden soll.
Soll gleichzeitig noch ein Filter für die Datensätze aktiviert werden, erfolgt dies über die Ranges der Datasource-Querys. Hierbei kann wie gewohnt eine Range für die entsprechende Query definiert werden. Wird die Range für ein Feld gesetzt, welches in dem Grid Control angezeigt wird, wird der Wert der Range entsprechend in der "Filterzeile" angezeigt.
Man kann viel darüber lesen, dass es ab der Version 4.0 von Microsoft Dynamics AX möglich ist auch mit .NET Objekten innerhalb von Dynamics AX zu arbeiten. Leider fehlt es oft an konkreten Beispielen, die einen ersten Überblick geben, wie man was machen muss.
Jeder der sich mit dem Thema schon einmal auseinander setzen musste, wird mit hoher Wahrscheinlichkeit auf eines dieser „Probleme“ gestoßen sein:
- Wie kann ein Datum zwischen Dynamics AX und .NET ausgetauscht werden?
- Wie kann die aktuelle Zeit an ein .NET Objekt übermittelt werden?
- Wie rufe ich eigentlich Werte eines .NET Enums ab?
Diese Liste könnte man noch beliebig erweitern.
Damit man einen kleinen Anhaltspunkt hat, wie die „Arbeit“ mit .NET Objekten gehen kann, soll dies anhand des Beispiels „Anlegen eines Termins in Outlook“ vorgestellt werden.
Zuerst muss eine Referenz zur der .NET Assembly erstellt werden, welche im Weitern verwendet werden soll. Hierzu muss im AOT der Zweig „References“ gewählt werden und über das Kontextmenü „Verweis hinzufügen“ ausgewählt werden.

In der neuen Maske wird die Assembly ausgewählt zu der eine Referenz erstellt werden soll. Hierzu navigiert man zu der entsprechenden Assembly und wählt diese über „Auswählen“ aus. Mit dem Drücken von „OK“ werden zu den ausgewählten Assemblys die Referenzen erstellt.
Bei diesem Beispiel wurde eine Referenz zur Assembly „Microsoft.Office.Interop.Outlook“ erstellt.

Ab jetzt können alle .NET Objekte, die in dieser Assembly enthalten sind, in Dynamics AX verwendet werden.
Wie dies im Einzelnen geschieht kann dem folgenden Beispiel entnommen werden, wo demonstriert wird, wie man aus Dynamics AX heraus ein Outlook Termin erstellt werden kann.
static void CreateOutlookAppointment(Args _args) { //.NET (CLR) Datentypen InteropPermission permission; Microsoft.Office.Interop.Outlook._Application outlookApplication; Microsoft.Office.Interop.Outlook._AppointmentItem outlookAppointment; Microsoft.Office.Interop.Outlook.OlItemType outlookItemType; System.DateTime appointmentStartTime; System.DateTime appointmentEndTime;
//Dynamics AX Datentypen Date1980 startDate, endDate; TimeExpected startTime, endTime; ; permission = new InteropPermission(InteropKind::ClrInterop); if (permission == null) { return; } //”Unsichere” Aufrufe starten permission.assert();
//Das Outlook Application Objekt instanzieren //(Beispiel für Erstellung eines .NET Klassenobjekts) outlookApplication = new Microsoft.Office.Interop.Outlook.ApplicationClass();
//Gewünschten Outlook-Elementtyp auswählen //(Beispiel für Zuweisung eines .NET Enum Wertes) outlookItemType = ClrInterop::parseClrEnum('Microsoft.Office.Interop.Outlook.OlItemType', 'olAppointmentItem');
//Gewünschten Outlook-Elemnttyp erstellen (Termin) outlookAppointment = outlookApplication.CreateItem(outlookItemType);
//Den Betreff zuweisen //(Beispiel für Zuweisung eines "einfachen" Wertes) outlookAppointment.set_Subject("Ein Test aus Dynamics AX");
startDate = str2Date("25.10.2007", 123); startTime = str2Time("10:30:00"); //"Beginnt um" zuweisen //(Beispiel für Konvertierung Dynamics AX Datum/Zeit -> .NET DateTime) appointmentStartTime = System.Convert::ToDateTime(strfmt("%1 %2", startDate, time2str(startTime, 0, 0))); outlookAppointment.set_Start(appointmentStartTime);
endDate = str2Date("26.10.2007", 123); endTime = str2Time("11:00:00"); //"Endet um" zuweisen appointmentEndTime = System.Convert::ToDateTime(strfmt("%1 %2", endDate, time2str(endTime, 0, 0))); outlookAppointment.set_End(appointmentEndTime);
//Den Termin speichern (ab jetzt ist der Termin in Outlook zu sehen) outlookAppointment.Save();
//”Unsichere” Aufrufe beenden CodeAccessPermission::revertAssert(); }
Das Buchen von Aufträgen in Microsoft Dynamics AX geschieht über die Klasse „SalesFormLetter“ bzw. einer ihrer konkretisierten (abgeleiteten) Klassen. Jeder Buchungstyp (z.B. Bestätigung, Lieferschein, Rechnung) ist durch eine eigene Klasse abgebildet, welche von der Basisklasse „SalesFormLetter“ abgeleitet ist (siehe Abbildung).

Um einen Auftrag per Programmcode zu buchen, muss ein Objekt der Klasse „SalesFormLetter“ erstellt werden.
salesFormLetter = SalesFormLetter::construct(DocumentStatus::Confirmation);
Dies geschieht, wie Allgemein in Microsoft Dynamics AX üblich, über die “construct” Methode der Klasse. Als Parameter muss dieser Methode die gewünschte Art der Buchung (z.B. Bestätigung, Lieferschein, Rechnung) angegeben werden. Die „construct“ Methode erzeugt ein,der Buchungsart entsprechendes, Objekt und gibt dieses zurück (In diesem Fall wird ein „SalesFormLetter_Confirm“ Objekt erzeugt). Die eigentliche Buchung wird über die Methode „update“ aufgerufen. Da dieser Methode alle für die Buchung notwendigen Daten als Parameter übergeben werden können, ist eine einzelne Zuweisung von z.B. dem Auftrag, welcher gebucht werden soll, nicht notwendig.
Hierzu ein Beispiel:
// --- Buchen ohne Ausdruck --- static void PostingConfimation(Args _args) { SalesFormLetter salesFormLetter; SalesTable salesTable; SalesId salesId; PrintJobSettings printJobSettings; ; //Angabe des Auftrags, welcher gebucht werden soll. salesId = "00423_036"; salesTable = SalesTable::find(salesId);
// Bestimmen des Buchungstyps durch Angabe des DocumentStatus salesFormLetter = SalesFormLetter::construct(DocumentStatus::Confirmation);
//Buchen des Auftrags (aber nicht Drucken). salesFormLetter.update(salesTable, SystemDateGet(), SalesUpdate::All, AccountOrder::None, NoYes::No, NoYes::No); }
Bei diesem Beispiel ist gut zu sehen, dass für die Buchung eines Auftrags im Wesentlichen nur zwei Schritte notwendig sind.
-
Über die Methode „construct“ ein dem Buchungstyp einsprechendes Objekt erzeugen.
-
Über den Aufruf der Methode „update“ den Auftrag buchen.
Natürlich können auch noch umfangreichere oder etwas speziellere Buchungsszenarien mit der Klasse „salesFormLetter“ abgebildet werden. So ist es z.B. möglich, gleich bei der Buchung entsprechende Dokumente auszudrucken (einmal, mehrfach und in verschiedene Formate), die Maske für die Buchung zu öffnen (damit der Benutzer Einfluss auf die Buchung nehmen kann) oder die Buchung nicht direkt auszuführen, sondern diese für die Stapelverarbeitung bereit zu stellen.
Damit es nicht zu komplex wird, kurz noch ein Beispiel zum Buchung und gleichzeitigen ausdrucken entsprechender Dokumente.
// --- Buchen mit Ausdruck --- static void PostingConfimation(Args _args) { SalesFormLetter salesFormLetter; SalesTable salesTable; SalesId salesId; PrintJobSettings printJobSettings; ; //Angabe des Auftrags, welcher gebucht werden soll. salesId = "00423_036"; salesTable = SalesTable::find(salesId); salesFormLetter = SalesFormLetter::construct(DocumentStatus::Confirmation);
//Buchen des Auftrags und drucken (Druckmedium aus Std. Einstellung). salesFormLetter.update(salesTable, SystemDateGet(), SalesUpdate::All, AccountOrder::None, NoYes::No, NoYes::Yes);
//2ter Ausdruck. printJobSettings = new PrintJobSettings(salesFormLetter.printerSettingsFormletter( PrintSetupOriginalCopy::Original)); //Wohin möchten wir drucken (hier Datei). printJobSettings.setTarget(PrintMedium::File);
//In welches Format soll gedruckt werden (hier PDF). printJobSettings.format(PrintFormat::PDF); printJobSettings.fileName(@"C:\Test_Order.pdf");
//Übergabe der Druckoptionen an das SalesFormLetter Objekt. salesFormLetter.updatePrinterSettingsFormLetter(printJobSettings.packPrintJobSettings());
salesFormLetter.printJournal(); }
Nicht nur in der Microsoft Dynamics AX Entwicklungsumgebung hat man die Möglichkeit mittels Drag & Drop Veränderungen vorzunehmen. Es ist recht einfach diese Funktion auch in der Applikation verfügbar zu machen. Hier gibt es im Standard nur sehr wenige Beispiele und die es gibt verwenden alle einen Tree oder eine List als Ziel. Ich zeige diese Drag & Drop Funktion mit zwei Grids als Quelle und als Ziel anhand eines einfachen Beispiels.
Um diese Drag & Drop Funktion in einer Maske zu implementieren braucht es zuerst einmal zwei Datenquellen, eine als Quelle und eine als Ziel. Um die Daten auf der Maske anzuzeigen hab ich jeweils ein Grid gewählt. Als Beispiel hab ich einfach eine neue Maske erstellt, die als Datenquelle SalesLine (Zieldatenquelle) und Inventtable (Quelldatenquelle) beinhaltet. Die Anzeige der Daten spielt hier nur eine untergeordnete Rolle.
Nun muss nur noch auf den Grid (SalesLineGrid und InventTableGrid) die Eigenschaft DragDrop auf „Manual“ gestellt werden um Drag & Drop zu aktivieren (Es gibt nur die Möglichkeit auf None oder Manual).
Nun kann man auf der Maske schon die Daten mittels Drag & Drop verschieben. Die Funktion zum Einfügen des Datensatzes aus der Quelle ins Ziel ist natürlich noch nicht vorhanden, man kann aber das optische Verhalten (Datensatz in ein anderes Grid ziehen) schon betrachten.
Nun müssen noch einige Ereignisse überschrieben werden um die Funktion zu implementieren.
dragOver = hier wird festgelegt welche Aktion durchgeführt wird (Move, Copy, None).
- Move = Mouse+SHIFT Taste (Default)
- Copy = Mouse+STRG Taste
So kann z.B. festgelegt werden, das auf dem Grid “InventTableGrid” kein Drag&Drop möglich sein soll. Hierzu wird diese Methode wie folgt überschrieben.
public FormDrag dragOver(FormControl _dragSource, FormDrag _dragMode, int _x, int _y) { FormDrag ret;
// ret = super(_dragSource, _dragMode, _x, _y); // Kein Drag&Drop zulassen ret = FormDrag::None;
return ret; }
Es können hier natürlich alle möglichen Überprüfungen stattfinden um festzulegen, wann welche Option erlaubt/ nicht erlaubt ist.
Um die Funktion nun abzuschließen fehlt nur noch das erzeugen des Datensatzes auf der Tabelle SalesLine. Hierzu wird die Method „Drop“ auf dem Grid „SalesLineGrid“ überschrieben.
Um Auftragspositionen aus den Artikelstamm anzulegen könnte die Methode so ausehen
public void drop(FormControl _dragSource, FormDrag _dragMode, int _x, int _y) { SalesLine sLine;
//Ist Quelle identisch mit aktuellem Grid (SalesLineGrid) if (_dragSource.equal(this)) { //TODO: Hier kann bspw. das Verschieben der Auftragsposition implementiert werden (LineNum) } //Ist Quelle InventTableGrid else if (_dragSource.name() == InventTableGrid.name()) { // Nur Aktion ausführen wenn Copy oder Move if(_dragMode == FormDrag::Copy || _dragMode == FormDrag::Move) { sLine.initValue(); sLine.SalesId = SalesID.valueStr(); sLine.initFromSalesTable(SalesTable::find(salesLIne.SalesId)); sLine.ItemId = inventTable.ItemId; sLine.initFromInventTable(InventTable); sLine.createLine(NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes); salesLine_ds.executeQuery(); } } super(_dragSource, _dragMode, _x, _y); }
In der Maske kann nun aus dem Artikelstamm eine neue Auftragsposition mittels Drag & Drop erstellt werden. Hierfür habe ich noch eine Vorbelegung/ Einschränkung auf die Aufragsnummer vorgenommen um die Auftragsposition erzeugen zu können.

Die Drag & Drop Funktion kann nur von der Artikeltabelle zu den Auftragsposition durchgeführt werden. Umgekehrt funktioniert das nicht, was man auch optisch sehen kann. (Screenshots haben aus irgendwelchen Gründen nicht funktioniert) Ein weiterer Vorteil bei der Drag & Drop Funktion ist, das Sie auch Maskenübergreifend funktioniert. Hierfür sind gar keine weiteren Änderungen notwendig. Es muss in der Drop Methode, wenn dort Überprüfungen stattfinden, nur der Ursprung auch erlaubt, bzw. mit berücksichtigt wurden sein.
Die gerade erstellte Maske lässt sich schon jetzt zweimal öffnen um dort von der einen zu der anderen Maske Daten mittels Drag & Drop zu übertragen (Inventtable -> SalesLine)
Durch kleine Änderungen kann diese Funktion auch aus der Artikelmaske ausgeführt werden.
Hierzu muss in der Artikelmaske einfach auf dem Grid DragDrop auf Manual gesetzt werden und die Drop Funktion in der neu erstellten Maske leicht angepasst werden.
Beispielprojekte für das einfache Drag&Drop innerhalb einer Maske und die kleine Erweiterung für das Drag&Drop aus der Artikelmaske herraus gibts auch wieder.
In der Maske "BOMDesigner" kann die Drag&Drop Funktion noch anhand einer Baumstruktur (FormTreeControl) als Quelle bewundert werden.
Beispiel SimpleDragAndDrop Form_SimpleDragDrop.rar (1,99 KB) Beispiel Projekt für Drag&Drop aus Artikelmaske in die Maske SimpleDragAndDrop SharedProject_DragAndDropInventTable.rar (11,71 KB)
Im letzten Artikel bin ich nur auf das lesen externer Datenquellen in Microsoft Dynamics AX eingegangen, diesesmal wird anhand eines kleine Beispiels eine externe Datenquelle befüllt. static void ArtikelinExterneDatenquelleeinfuegen(Args _args) { CCADOConnection adoConnection; CCADOCommand adoCommand; Inventtable inventtable; ; adoConnection = new CCADOConnection(); //ODBC-Connection adoConnection.connectionString(strfmt("Dsn=%1",dsnName)); //Connection öffnen adoConnection.open();
adoCommand = new CCADOCommand(); adoCommand.activeConnection(adoConnection); //Alle Datensätze der Artikeltabelle while select inventtable { // Neue Anweisung für das Einfügen des Datensatzes adoCommand.commandText(strfmt("Insert into ExterneArtikelTabelle (Artikelkennung,Artikelname) values ('%1','%2')", inventtable.ItemId, inventtable.itemName)); // Anweisung ausführen adoCommand.execute(); } }
Das Erstellen der Verbindung ist identisch, nur habe ich die Klasse CCADOCommand anstelle von CCADORecordSet verwendet. Mit CCADOCommand können Manipulationen, wie das Einfügen oder das Löschen von Datensätzen, an externe Datenquellen vorgenommen werden. Wenn man noch einen Schritt weiter gehen will kann auch die Tabelle vorher neu erstellt werden um diese dann nutzen zu können. Dies geschieht auch über SQL Befehle. Hier nochmal ein Beispiel für das anlegen und befüllen einer Tabelle (Artikeltabelle mit den Feldern Artikelnummer und Artikelname). static void ArtikelinExterneDatenquelleeinfuegen(Args _args) { CCADOConnection adoConnection; CCADORecordSet adoRecordSet; CCADOCommand adoCommand; str sql; Inventtable inventtable; SysDictTable dictTable = new SysDicttable(inventTable.TableId); SysDictField dictFieldID = new SysDictField(dicttable.id(), fieldnum(InventTable, ItemID)); SysDictField dictFieldName = new SysDictField(dicttable.id(), fieldnum(InventTable, ItemName)); ; //Neu Verbindung adoConnection = new CCADOConnection(); adoConnection.connectionString(strfmt("Dsn=%1",dsnName)); adoConnection.open();
//Neu Command adoCommand = new CCADOCommand(); adoCommand.activeConnection(adoConnection);
//Tabelle löschen wenn sie existiert /* sql = strfmt("DROP Table IF EXISTS %1", dictTable.label());
adoCommand.commandText(sql); adoCommand.execute(); */ //Tabelle erzeugen sql = strfmt("Create Table %1 (%2 %3(%4), %5 %6(%7))", strrem(dictTable.label(), " "),//Leerzeichen löschen strrem(dictFieldID.label(), " "), dictFieldID.baseType(),//Datentyp festlegen dictFieldID.stringLen(),//Größe festlegen strrem(dictFieldName.label(), " "), dictFieldName.baseType(), dictFieldName.stringLen());
adoCommand.commandText(sql); adoCommand.execute(); //Datensätze in die neue Tabelle schreiben while select inventtable { sql = strfmt("Insert into %1 (%2,%3) values ('%4','%5')", strrem(dictTable.label(), " "), strrem(dictFieldID.label(), " "), strrem(dictFieldName.label(), " "), inventtable.ItemId, inventtable.itemName);
adoCommand.commandText(sql); adoCommand.execute(); } }
Erhält man bei seiner täglich Arbeit die Fehlermeldung „Ein Befehl der Datendefinitionssprache kann nicht für () ausgeführt werden. Die SQL Datenbank hat einen Fehler gemeldet.“ oder auf englisch „cannot excecute a data definition language command on (). The SQL database has issued an error.” stellt sich einem meist eine große Hürde in den Weg, weil aus dieser Fehlermeldung nicht eindeutig zu erkennen ist, wo ein Fehler entstanden ist bzw. wieso der Fehler auftritt.
Durch Zufall bin ich drauf gestoßen, wie man genau diese Fehlermeldung produzieren kann und somit Rückschlüsse auf die Ursache des Fehlers schließen kann.
Das Folgende Vorgehen...
- Erstellen eines EDT’s vom Typ „string“ mit dem Namen „MY_BaseId“.
- Verwenden des EDT’s in einer Tabelle (Name: MY_BaseTable).
- Erzeugen von mehreren Datensätzen in der Tabelle.
- Löschen des EDT’s „MY_BaseId“.
- Erstellen eines EDT’s vom Typ „real“ mit dem Namen „MY_BaseId“ (gleicher Name wie der gelöschte EDT vom Typ „string“ hatte).
- Kompilieren oder synchronisieren der Tabelle „MY_BaseTable“.
...ergibt die genannte Fehlermeldung.
Daraus ist zu schließen, dass der Fehler immer Auftritt, wenn der Typ eines vorhandenen EDT’s den bereits eine oder mehrere Tabellen verwenden, geändert wird. Ich konnte die Fehlermeldung bzw. den Fehler aber nur reproduzieren, wenn die jeweilige Tabelle ein oder mehrere Datensätze enthielt. War in der Tabelle kein Datensatz vorhanden kam es nicht zu der Fehlermeldung.
Um besagten Fehler oder besagte Fehlermeldung zu beheben müssen entweder alle Datensätze in der Tabelle oder die Tabelle selbst gelöscht werden.
Ich habe in letzter Zeit ein sehr merkwürdiges Verhalten beim Verlassen von Microsoft Dynamics AX 4.01 feststellen müssen. Es kam beim Abmelden immer zum Absturz der Application Object Server (AOS) Instanz. Das war bei zwei unterschiedlichen Installationen zu beobachten. Grund hierfür war das bei den aktuelle Applikationen der beiden AOS Instanzen eine Labeldatei (*.ald) gefehlt hatte. Diese fehlende Labeldatei hat dann beim Abmelden den Absturz verursacht. Nach Neuanlage der fehlende Labeldatei kam es zu keinen Problemen mehr.
Eine Bestellung umfasst in Microsoft Dynamics AX immer einen Datensatz der Tabelle „PurchTable“ und wenn die Bestellung einen Artikel enthält, auch einen Datensatz in der Tabelle „PurchLine“. Zusätzlich werden in Abhängigkeit von den Daten der Bestellung (Einmallieferant: Ja/Nein, Intercompany: Ja/Nein, etc.) zusätzliche Datensätze in anderen Tabellen erzeugt bzw. geändert. Beispielhaft sei hier die Tabelle „VendTable“ genannt. In dieser wird ein neuer Lieferant erstellt, wenn beim Erstellen der Bestellung angegeben wurde, dass es sich um einen Einmallieferanten handelt. Ein weiteres Beispiel wäre die Tabelle „MarkupTrans“ in der in Abhängigkeit von den Einstellungen für sonstige Zuschläge ebenfalls weitere Datensätze erzeugt werden.
Die Logik, die das Erstellen der einzelnen Datensätze der verschiedenen Tabellen steuert wird in Microsoft Dynamics AX durch die Klassen „PurchTableType“ (Abbildung 1) und „PurchLineType“ (Abbildung 2), sowie deren abgeleiteten Klassen abgebildet. Diese Klassen steuern das Verhalten bei Anlage, Änderung und Löschung einer Bestellung. Dies beinhaltet auch, welche Werte ein Feld bei welchem Bestellungstyp annehmen darf, was geschieht wenn ein Feld geändert wird, was wird wie gebucht und so weiter.


Diese Klassen werden von überschriebenen Methoden der Tabellen „PurchTable“ und „PurchLine“ aufgerufen. So ruft zum Beispiel die Methode „Insert“ der Tabelle „PurchTable“, die Methode „Insert“ der Klasse „PurchTableType“ auf. Abhängig vom Bestellungstyp wird über die Methode „construct“ bei der Initialisierung eines „PurchTableType“ Objekts gesteuert, welches konkrete Objekt erzeugt wird („PurchTableType_Purch“, „PurchTableType_ReturnItem“, etc.). Dies erfolgt in der Methode „type“ der Tabelle „PurchTable“ oder „PurchLine“. Unter anderem sind weiterhin die Methoden „Update“, „Delete“, „InitValue“, „ValidateField“ und „Delete“ auf die gleiche Weise überschrieben. Ein Blick in die Methoden der Tabelle „PurchTable“ sollte dies verdeutlichen.
Somit ist die Logik, die für die Steuerung von Bestellungen in Microsoft Dynamics AX verantwortlich ist, vom Prinzip her vergleichbar mit der Logik welche die Aufträge „steuert“ (vergleiche hierzu: Microsoft Dynamics AX API – Teil 1 „Erstellen von Aufträgen“).
Deswegen ist das Erstellen einer Bestellung genau so einfach wie das Erstellen eines Auftrags. Um eine neue Bestellung zu erstellen muss im Wesentlichen nur:
- Eine neue Nummer des entsprechenden Nummernkreises gezogen werden.
- Die Methode „InitValue“ der Tabelle „PurchTable“ aufgerufen werden.
- Die Methode „InitFromVendTable“ der Tabelle „PurchTable“ mit Angabe des Lieferanten Datensatzes aufgerufen werden.
- Die Methode „Insert“ der Tabelle „PurchTable“ aufgerufen werden.
Soll für diese gerade erzeugte Bestellung nun noch eine Artikelposition erzeugt werden, muss im Wesentlichen nur die Methode „CreateLine“ der Tabelle „PurchLine“, mit vorheriger Definition von Bestellungsnummer („PurchLine.PurchId“) und Artikelnummer („PurchLine.ItemId“), aufgerufen werden.
Hierzu ein Beispiel:
void createPurchTableAndLine() { VendAccount vendAccount = "<yourVendAccount>"; ItemId itemId = "<yourItemId>";
PurchTable purchTable; PurchLine purchLine; NumberSeq numberSeq; InventTable inventTable; ; //Bestellungskopf (PurchTable) //Neue Bestellungsnummer aus Nummernkreis erzeugen NumberSeq = NumberSeq::newGetNumFromCode( PurchParameters::numRefPurchId().numberSequence); purchTable.PurchId = NumberSeq.num();
//Bestellungskopf initialisieren purchTable.initValue();
//Initialisierung der lieferantenspezifischen Bestellungsdaten purchTable.initFromVendTable(VendTable::find(vendAccount));
//Bestellungskopf erstellen purchTable.insert();
//Bestellungsposition (PurchLine) purchLine.clear();
//Zuweisen von Bestellungsnummer und Artikelnummer purchLine.purchId = purchTable.PurchId; purchLine.ItemId = itemId;
//Bestellungsposition erstellen (ruft PurchLine.insert auf) purchLine.createLine(NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes); }
Hin und wieder stösst man auf etwas ungewöhnliches. So geschehen vor einiger Zeit: Es konnten zu einigen Aufträge, die Funktion "Aufträge zuordnen" nicht mehr aufgerufen werden, ohne das der Microsoft Axapta 3.0 Client abstürzt. Nach einigen Nachforschungen war die Fehlerursache klar. Die besagt Funktion erstellt für jeden zugeordneten Datensatz eine neue Einschränkung (nur in Microsoft Axapta 3.0 (welche Service Packs betroffen sind kann ich leider nicht sagen), diese Funktion wurde in Microsoft Dynamics AX 4.0 bereits überarbeitet, sodass es hier nicht mehr zu Fehlern kommen kann). Bei sehr vielen neu erzeugten Einschränkungen hat das dann zum Absturz des Clients geführt. Folgende Grenzwerte (in Microsoft Axapta 3.0) konnte ich ermitteln: Anzahl von Einschränkungen (Ranges) die einen Absturz des Axapta Clients verursachen: ab ~1475 Anzahl von Einschränkungen (Ranges) die "nur" eine interne Axapta Fehlermeldung auslösen: ab ~475 In Microsoft Dynamics AX 4.0 konnte ich auch bei mehr als 1475 Einschränkungen keinen Absturz provozieren, es erscheint nur eine Fehlermeldung. *Alle hier beschriebenen Einschränkungen (Ranges) wurden auf demselben Tabellenfeld vorgenommen.
Ein Auftrag umfasst in Microsoft Dynamics AX immer einen Datensatz in der Tabelle „SalesTable“ (Auftragskopf) und wenn der Auftrag einen Artikel enthält (Auftragsposition), auch einen Datensatz in der Tabelle „SalesLine“. Zusätzlich werden in Abhängigkeit von den Daten des Auftrags (Einmalkunde: Ja/Nein, Intercompany: Ja/Nein, etc.) zusätzliche Datensätze in anderen Tabellen erzeugt bzw. geändert. Beispielhaft sei hier die Tabelle „CustTable“ genannt. In dieser wird ein neuer Kunde erstellt, wenn beim Erstellen des Auftrags angegeben wurde, dass es sich um einen Einmalkunden handelt. Ein weiteres Beispiel wäre die Tabelle „MarkupTrans“ in der in Abhängigkeit von den Einstellungen für Sonstige Zuschläge ebenfalls weitere Datensätze erzeugt werden.
Die Logik, die das Erstellen der einzelnen Datensätze der verschiedenen Tabellen steuert (die so genannte Geschäftslogik) wird in Microsoft Dynamics AX durch die Klassen „SalesTableType“ (Abbildung 1) und „SalesLineType“ (Abbildung 2), sowie deren abgeleiteten Klassen abgebildet. Diese Klassen steuern das Verhalten bei Anlage, Änderung und Löschung eines Auftrags. Dies beinhaltet auch, welche Werte ein Feld bei welchem Auftragstyp annehmen darf, was geschieht wenn ein Feld geändert wird, was wird wie gebucht und so weiter.


Diese Klassen werden von überschriebenen Methoden der Tabellen „SalesTable“ und „SalesLine“ aufgerufen. So ruft zum Beispiel die Methode „Insert“ der Tabelle „SalesTable“, die Methode „Insert“ der Klasse „SalesTableType“ auf. Abhängig vom Auftragstyp wird über die Methode „construct“ bei der Initialisierung eines „SalesTableType“ Objekts gesteuert, welches konkrete Objekt erzeugt wird („SalesTableType_Sales“, „SalesTableType_ItemReq“, etc.). Unter anderem sind weiterhin die Methoden „Update“, „Delete“, „InitValue“, „ValidateField“ und „Delete“ auf die gleiche Weise überschrieben. Ein Blick in die Methoden der Tabelle „SalesTable“ oder „SalesLine“ sollte dies verdeutlichen.
Somit gestaltet sich das Erstellen eines neuen Auftrags sehr einfach, da die gesamte Geschäftslogik die hinter einem Auftrag steht, automatisch aufgerufen wird.
Um einen neuen Auftrag zu erstellen muss im Wesentlichen nur
- Eine neue Nummer des entsprechenden Nummernkreises gezogen werden.
- Die Methode „InitValue“ der Tabelle „SalesTable“ aufgerufen werden.
- Die Kundennummer zugewiesen werden.
- Die Methode „InitFromCustAccount“ der Tabelle „SalesTable“ aufgerufen werden.
- Die Methode „Insert“ der Tabelle „SalesTable“ aufgerufen werden.
Soll für diesem gerade erzeugten Auftrag nun noch eine Artikelposition erzeugt werden, muss im Wesentlichen nur die Methode „CreateLine“ der Tabelle „SalesLine“, mit vorheriger Definition von Auftragsnummer („SalesLine.SalesId“) und Artikelnummer („SalesLine.ItemId“), aufgerufen werden.
Hierzu ein Beispiel:
void createSalesTableAndLine() { AccountNum custAccount = <yourCustAccount>; ItemId itemId = <yourItemId>;
SalesTable salesTable; SalesLine salesLine; NumberSeq NumberSeq; ; //Auftragskopf (SalesTable) //Neue Auftragsnummer aus Nummernkreis erzeugen NumberSeq = NumberSeq::newGetNumFromCode( SalesParameters::numRefSalesId().numberSequence); salesTable.SalesId = NumberSeq.num();
//Auftragskopf initialisieren salesTable.initValue(); salesTable.CustAccount = custAccount;
//Initialisierung der kundenspezifischen Auftragsdaten salesTable.initFromCustTable();
//Auftragskopf erstellen salesTable.insert();
//Auftragsposition (SalesLine) salesLine.clear();
//Zuweisen von Auftragsnummer und Artikelnummer salesLine.SalesId = salesTable.SalesId; salesLine.ItemId = itemId;
//Auftragsposition erstellen (ruft SalesLine.insert auf) salesLine.createLine(NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes); }
Immer wieder wurde ich darauf angesprochen, ob es nicht möglich sei, die SQL Server Reporting Services Berichte auch ohne Umweg über einen Browser direkt aus dem Dynamics AX 4.0 Menü aufzurufen.
Leider ist dies im Standard nicht möglich. So habe ich mich entschlossen ein kleines Tool hierfür zu schreiben.
Mit der SSRS Berichtsviewer Erweitung können nun die SQL Server Reporting Services Berichte direkt aus dem Dynamics AX 4.0 Menü geöffnet werden. Dadurch kann eine einfachere und vor allem durchgängigere Benutzung von Berichten innerhalb von Dynamics AX 4.0 gewährleistet werden, da es für den Benutzer keine Unterschiede mehr zwischen dem Aufruf von Dynamics AX 4.0 Berichten und SQL Server Reporting Services Berichten gibt.

Innerhalb von Dynamics AX 4.0 werden die SQL Server Reporting Services Berichte in einem neuen Berichtsviewer gerendert und dargestellt.

Die SSRS Berichtsviewer Erweiterung für Dynamics AX 4.0 - Version 0.1 kann hier herunter geladen werden: SSRSReportingExtension_V0.1.zip (403,96 KB)
Der Downlaod enthält eine Anleitung zur Verwendung. Sollten dennoch einige Fragen durch die Anleitung nicht beantwortet werden können, bitte ich diese über die Kommentarfunktion zu stellen. Gleiches gilt auch für Bugs oder Featurewünsche für kommende Versionen.
Manchmal ist es hilfreich beim Debuggen eines Codeblocks zusätzliche Informationen im Debugger auszugeben. Dies kann zum Beispiel der aktuelle Wert eines Tabellenfelds sein.
Ebenfalls ist es machmal hilfreich, zusätliche Überprüfungen von Werten einzelner Variablen oder Tabellenfeldern durchzuführen, wenn der jeweilige Codeblock im Debugger ausgeführt wird.
Hierzu ein Beispiel:
CustTable custTable; ; while select custTable { //Den Kundennamen im Debuggerfenster ausgeben. debug::printDebug(custTable.Name);
//Information in einem beliebigen Debugger-Info-Tab ausgeben. debug::printTab(DebugPrintTab::Method, "Aufruf einer Methode");
//Eine Überprüfung eines Wertes durchführen (nur im Debug-Mode). debug::assert(CustTable.Name != nullValue(CustTable.Name));
info(custTable.Name); }
Alle "Debug::" Anweisungen werden nur beachtet/ausgeführt, wenn der Code im Debugger ausgeführt wird (gesetzter Breakpoint). Wird der Code "normal" ausgeführt, wird in dem Beispiel nur die "info()" Anweisung ausgeführt.
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)
|