Beim Entwickeln und/oder Testen von AIF-Service, welche als Webservice bereit gestellt werden, entstehen oftmals (mindestens) 2 Fragestellungen.
- Wie kann die Nachricht betrachtet werden, welche zwischen den System über den Webservice ausgetauscht wird?
- Wie lässt sich der Webservice-Aufruf debuggen?
Leider werden diese Fragen bei einer Suche im Internet oft mit “Geht nicht” beantwortet. Dies ist so allerdings nicht richtig.
Es ist z.B. möglich, für alle Webserviceaufrufe eine Tracing-File zu erzeugen, welches u.A. auch die Nachricht protokolliert, die von oder zu Dynamics AX geschickt wurde. Um das Tracing zu aktivieren müssen nur entsprechende Einstellungen in der web.config des Webservices vorgenommen werden.
Wie dies im Detail funktioniert beschreibt dieses kleine How-To des Microsoft Dynamics Developer Centers im MSDN. http://msdn.microsoft.com/en-us/library/cc967372.aspx
Es ist ebenfalls möglich, die Webserviceaufrufe zu debuggen.
Allerdings müssten hierfür einige Schritte beachtet werden, damit das Debuggen von Webserviceaufrufen auch für X++ Code funktioniert. Eine detaillierte Anleitung hierzu ist am Ende des Whitepapers “Tips for Creating Services in Microsoft Dynamics AX 2009” zu finden. http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=90388b14-fb8c-4633-a255-28ff7146c5b2
Um Datensätze in Microsoft Dynamics AX, welche z.B. auf einer Maske angezeigt werden, entsprechend seiner Anforderungen einzuschränken (zu filtern) muss die Query (Abfrageobjekt) durch Erstellung von Ranges (Abfrageeinschränkungsobjekt) entsprechend “manipuliert” werden. Hierfür wird z.B. die Query einer Maskendatenquell (DataSource) verwendet und für diese eine neue Range definiert:
Beispiel:
public void init() { QueryBuildRange range; ; super(); range = CustTable_ds.query().dataSourceTable(tablenum(CustTable)).addRange(fieldnum(CustTable, AccountNum)); range.value("1101");}
Im Dynamics AX Standard kann ähnlicher X++ Quelltext in vielen Masken gefunden werden, da dies der “Standard-Weg” zum Einschränken von Datensatzabfragen bei Masken oder auch Reports ist. Ebenso wird dieses Vorgehen auch in den Schulungsunterlagen, in der Entwicklerhilfe und anderen Stellen beschrieben.
Leider hat dieses Vorgehen eine kleine aber teilweise sehr störende Beschränkung. Über diesen Weg ist es nicht möglich, alle Abfrageeinschränkungen welche durch X++ Quellcode “gesetzt” wurden und Einschränkungen, welche durch einen Benutzer mittels der Standardfilterfunktion von Dynamics AX definiert wurden, zu berücksichtigen. Beim einer Datenaktualisierung (Aufruf von DataSource.executeQuery) gehen die von einem Benutzer definierten Abfrageeinschränkungen verloren.
Dies ist darin begründet, dass es nicht nur ein DataSoucre.query-Objekt, sondern auch ein DataSource.queryrun().query-Objekt gibt. Diese beiden “Query-Objekte” sind jeweils unterschiedliche Objekte, bzw. Objektinstanzen.
Das DataSource.query-Objekt ist das “Basisabfrageobjekt”, welches durch einen Benutzer, mittels der Filterfunktionalität des Standards, nicht verändert werden kann (nur durch X++ Code). Alle durch den Benutzer vorgenommenen Änderungen an der “Basisabfrage” werden in dem Query-Objekt von DataSource-queryrun() “gespeichert”. Dies kann unter Anderem durch Betrachtung des SQL-Statements, welches durch ein Query-Objekt bereit gestellt wird nachgewiesen werden.
Beispiel:
Aufruf einer Maske mit einer durch X++ Code modifizierten Abfrage (Query). Anmerkung: Beim Aufruf der Maske wird in der “Init-Methode” eine Range (CustGroup = “10”) gesetzt. range = CustTable_ds.query().dataSourceTable(tablenum(CustTable)).addRange(fieldnum(CustTable, CustGroup));
range.value("10");
Durch den Benutzer wird nun mittels der Dynamics AX Standard-Filterfunktion die Abfrage bzw. deren Einschränkung(en) angepasst/verändert.
Hierdurch ist zu beobachten, das sich zwar das SQL-Statement des DataSource.queryrun().query-Objekts, aber nicht das SQL-Statement des DataSoucre.query-Objekts ändert. Da bei einem Aufruf von DataSource.executeQuery allerdings immer das DataSource.query-Objekt verwendet wird, gehen die durch den Benutzer gewählten Abfrageeinschränkungen verloren.
Wie ist es nun aber möglich, die von einem Benutzer gewählten Abfrageeinschränkungen/Filtereinstellung doch zu berücksichtigen?
Da alle Abfrageeinschränkungen, welche von einem Benutzer gewählt wurden, in dem Query-Objekt von DataSource.queryrun() “gespeichert” werden und somit auch im X++ Code zur Verfügung stehen ist dies recht einfach. Es muss einfach das Query-Objekt von DataSource.queryrun() genommen werden, um die gewünschten Ranges ergenzt werden und schließlich dass Query-Objekt der DataSource überschrieben werden.
Beispiel:
Basis ist eine einfach Maske, welche alle Kundendatensätze anzeigt.
Dieser Maske/Abfrage wird nun durch die Standard-Filterfunktion (Benutzerfilter) eine neue Abfrageeinschränkung hinzugefügt (Kundennummer = 1101 und 2001).
Wie zuvor beschrieben, wird nun eine neue Abfrageeinschränkung mit X++ Code auf dem Query-Objekt von DataSource.queryrun “gesetzt” und das Query-Objekt der DataSource mit diesem überschrieben.
void clicked() { Query query; QueryBuildRange range; ; super(); query = CustTable_ds.queryRun().query(); range = query.dataSourceTable(tablenum(CustTable)).addRange(fieldnum(CustTable, CustGroup)); range.value("10"); CustTable_ds.query(query); CustTable_ds.executeQuery(); }
Dies hat zur Folge, dass die durch den Benutzer gewählten Abfrageeinschränkungen, wie zu sehen, weiterhin berücksichtigt werden.
Alternativ zur Verwendung der Abfrageeinschränkung (Range) “direkt” über das Query-Objekt kann auch mit einem oder mehreren Filtern gearbeitet werden. Diese unterliegen im Gegensatz zu den Query-Objekt aber einigen Einschränkungen, sodass diese nicht in jeder Situation verwendet werden können.
Der folgende X++ Code zeigt, wie ein Filter gesetzt werden kann.
void clicked() { Query query; QueryBuildRange range; ; super(); CustTable_ds.filter(fieldnum(CustTable, CustGroup), "10"); }
Wird eine Abfrage auf diese Art und Weise eingeschränkt, ist der Aufruf von DataSource.executeQuery() unnötig, da die Datenaktualisierung bereits im Hintergrund durch den Filter-Aufruf durchgeführt wird. Filter einer DataSource funktionieren vom Prinzip her wie die Standardfilter, welche durch einen Benutzer in Dynamics AX gesetzt werden können. Dies hat zur Folge, dass sich diese ebenfalls nur auf das Query-Objekt von DataSource.queryrun() auswirken und somit DataSource.query nicht beeinflussen.
Mit der Methode DataSource.removeFilter können die gesetzten Filter wieder gelöscht werden. Leider werden hierdurch alle gesetzten Filter gelöscht, sodass nach diesem Aufruf unter Umständen einige bereits gesetzte Filter erneut gesetzt werden müssen, um das gewünschte Abfrageergebnis zu erhalten.
Welche der gezeigten Methoden, zum Einschränken von Abfragen, aber nun der beste oder bessere Weg ist, muss von Fall zu Fall entschieden werden.
Oftmals besteht die Anforderung, über einen Zeitgesteuerten Job (Batchjob), das Generieren von statistischen Berichten, welche in einer Datei gespeichert werden soll, zu automatisieren.
Microsoft Dynamics AX stellt hierfür die Möglichkeit bereit, jeden Bericht mithilfe der Stapelverarbeitung zu einem definierten Zeitpunkt zu generieren und in einer Datei, z.B. in einem Netzwerklaufwerk, bereit zu stellen. Soweit stellt dies kein Problem dar, da über den Standard von Dynamics AX diese Anforderung ohne weiteres erfüllt werden kann.
Leider wird hierbei oft vergessen, dass der entsprechende Batchserver (AOS) so konfiguriert werden muss, dass dieser das “Drucken auf dem Server” zulassen muss. Dies ist eine Einstellungsoption des Serverkonfigurations-Utilities.
Weiterhin sollte bei der Angabe der Datei bzw. des Speicherortes der Datei immer ein UNC-Pfad verwendet werden, da die eigentlich Ausführung des Berichtes und somit auch die Erstellung der Datei über das Benutzerkonto des AOS-Dienstes geschieht.
Dies bedingt auch, dass entsprechende Berechtigungen für das Dienstkonto des Batchservers (AOS) für das freigegebene Verzeichnis vergeben werden müssen, damit die Datei und somit der Bericht erfolgreich erstellt werden kann.
Eine weiterführende Beschreibung hierzu ist auch im EMEA Dynamics AX Support Blog zu finden.
In Microsoft Dynamics AX wird das Args-Objekt dazu verwendet, Informationen z.B. an eine aufzurufende Maske oder Klasse zu übergeben.
Mittels des Args-Objektes ist es z.B. möglich, den auf einer Maske ausgewählten Datensatz an die Aufzurufende (Unter)Maske zu übergeben, um mit diesem die Darstellung und/oder Funktionen der Maske anzupassen. Oft wird dieses Vorgehen dazu verwendet, Abfragen (Queries) entsprechend einzugrenzen, damit nur relevante Informationen verarbeitet werden. Eine beispielhafte Anforderung hierfür könnte sein, alle Aufträge des zuvor ausgewählten Kunden in einer neuen Maske anzuzeigen.
Manchmal ist aber notwendig, nicht nur den Aufrufer (oder den gewählten Datensatz), sondern auch dessen Aufrufer zu kennen, um bestimme Funktionalitäten erstellen oder implementieren zu können. Herbei kann es sein, dass der ausgewählter Datensatz über mehrere Aufrufebenen übergeben werden muss und der direkte Aufrufer dennoch bekannt sein muss.
Nehmen wir an, es existiert eine Hauptmaske, auf der ein Kundendatensatz ausgewählten werden kann. Auf einer weiteren Maske (1. Maske), sollen nun alle Aufträge des ausgewählten Kunden angezeigt werden. Diese Maske soll über die Hauptmaske aufgerufen werden. Über eine 2. Maske, welche über die 1. Maske aufgerufen werden soll, sollen die Adressdaten des Kunden angezeigt werden.
Um nun die benötigten Information, ausgewählter Kunden in der Hauptmaske, auf der 2. Maske zur Verfügung zu haben, muss über das FormRun-Objekt der 1. Maske der Aufrufer (Caller) dieser Maske bestimmt werden.
Beispiel (Init-Methode der 2. Maske):
public void init() { Object callerDataSource; FormRun callerFormRun; common callerRecord; common callerRecordOfCallerRecord; ; super(); if(element.args() && element.args().dataset()) { callerRecord = element.args().record(); callerDataSource = callerRecord.dataSource(); callerFormRun = element.args().caller(); callerRecordOfCallerRecord = callerFormRun.args().record(); CtrlCallerTable.text(tableid2name(callerRecord.TableId)); CtrlCallerOdCallerTable.text(tableid2name(callerRecordOfCallerRecord.TableId)); } }
Sicherlicht läßt sich das in dem Beispiel beschriebene Verhalten auch anders (oder eleganter) Lösen. Dieses Beispiel wurde nur gewählt, um den Ablauf oder die notwendigen Schritte zu demonstrieren, wie Aufrufer über mehrere Ebenen bestimmt werden können.
Notizen, Dokumente oder Dateien werden in Dynamics AX mithilfe des “Dokumentenmanagement-Systems” verwaltet.
Zu jedem beliebigen Datensatz einer beliebigen Tabelle (z.B. CustTable -> Debitoren) können beliebig viele Notizen oder Dokumente hinterlegt werden. Per Benutzeroberfläche kann die entsprechende Funktionalität über die Menüleiste der Masken aufgerufen werden.
Das folgende Beispiel zeigt wie dies auch per Programmcode erfolgen kann:
static void AKU_CreateDocuRefNote(Args _args) { CustTable custTable; DocuRef docuRef; DocuType docuType; ; custTable = CustTable::find("1101"); docuType = DocuType::find("Note"); if(custTable && docuType) { docuRef.initValue(); docuRef.RefTableId = custTable.TableId; docuRef.RefRecId = custTable.RecId; docuRef.RefCompanyId = custTable.dataAreaId; docuRef.TypeId = docuType.TypeId; docuRef.Restriction = DocuRestriction::External; docuRef.Name = "Name der Notiz"; docuRef.Notes = "Text (Inhalt) der Notiz"; docuRef.insert(); } }
Für Debitoren (Kunden) kann in Microsoft Dynamics AX ein Kreditlimit vergeben/eingestellt werden.
In der Auftragsmaske wird bei Anlage eines Auftrag (in Abhängigkeit der Kreditlimit-Einstellungen) das verbleibende Kreditlimit berechnet und der Auftragswert gegen dieses geprüft. Bei Überschreitung des Kreditlimits wird eine entsprechende Warnung oder ein entsprechender Fehler ausgegeben.
Die Funktion zur Überprüfung des Kreditlimits kann auch manuell, durch eine entsprechende Funktion auf der Auftragsmaske, aufgerufen werden.
Soll das verbleibende Kreditlimit mit X++ Code berechnet werden, muss leider eine “Kleinigkeit” beachtet werden, die so auf den ersten Blick nicht immer ersichtlich ist bzw. für Verwirrung sorgen kann.
Die Berechnung des verfügbaren Kreditrahmens oder des verbleibenden Kreditlimits ist durch die alleinige Angabe eines Debitors nicht möglich. Es muss immer ein entsprechender Auftrag “vorhanden” sein um diese auszuführen zu können.
Berechtigterweise stellt sich die Frage, wie das verbleibende Kreditlimit eines Debitors berechnet werden kann, wenn keine “Beziehung” zu einem Auftrag besteht bzw. wenn kein Auftrag angegeben werden kann. Hierfür muss ein kleiner “Trick” angewendet werden, der nichts anderes macht, als einen neuen “SalesTable” Datensatz zu initialisieren, diesen aber nicht zu speichern. Ist der “SalesTable” Datensatz initialisiert kann mithilfe der beiden Klassen “SalesTotals” und “CustCreditLimit” der verfügbare Kreditrahmen berechnet werden.
Beispiel:
CustTable custTable = CustTable::find("1101"); CustCreditLimit custCreditLimit; SalesTotals salesTotals; SalesTable salesTable; AmountMST balanceEstimate; AmountMst creditRemain; ; salesTable.CustAccount = custTable.AccountNum; salesTable.initFromCustTable(); salesTotals = SalesTotals::construct(salesTable); salesTotals.calc(); custCreditLimit = CustCreditLimit::construct(salesTable); balanceEstimate = custCreditLimit.balanceEstimate(); if(custCreditLimit.useEstimated()) { creditRemain -= balanceEstimate; } creditRemain += conpeek(salesTotals.displayFieldsCurrency(CustTable.Currency), TradeTotals::posFreeValue()); info(strfmt("Verbleibendes Kreditlimit: %1", creditRemain));
In Microsoft Dynamics AX beziehen Masken ihre Daten, oder anders gesagt die Daten welche sie anzeigen, über so genannte DataSources. In einer DataSource sind somit die aktuellen auf der Maske angezeigten Daten (lokal) gespeichert.
Zugriff auf den jeweils aktuell ausgewählten Datensatz erhält man üblicherweise über genau diese DataSource. Der ausgewählte Datensatz kann unter anderem auch, z.B. durch ein MenuItem(Button), an eine Funktion oder andere Maske übergeben werden. Hierfür muss nur die Eigenschaft “DataSource”, in diesem Beispiel des MenuItem(Button), entsprechend eingestellt sein.
Ist keine DataSource in den Eigenschaften hinterlegt wird immer die erste DataSource der Query verwendet bzw. dessen aktiver Datensatz übergeben. In der Regel ist dies die DataSource, welche beim Erstellen der Maske als erstes hinzugefügt oder erstellt wurde (Ausnahme ist hier eine eventuelle Manipulation der Query per Programmcode).
Dieses Vorgehen ist für 90% aller Fälle das wohl am besten geeignete Vorgehen und wird in dieser Weise auch vom Dynamics AX Standard verwendet. Leider gibt es Anwendungsfälle, bei denen diese “starre Verbindung” von DataSource und z.B. Button oder MenuItem nicht funktioniert, beziehungsweise nicht zum gewünschten Ergebnis führt.
Angenommen man hat eine Maske mit zwei DataSources (CustTable und SalesTable), deren Daten über zwei Grids angezeigt werden, sowie einen Button, welcher eine Operation mit dem zuletzt ausgewählten Datensatz (unabhängig von der DataSource) durchführen soll. Wenn ein Datensatz der DataSource “CustTable” selektiert wurde, soll dieser verarbeitet werden. Ist zuletzt ein Datensatz der DataSource “SalesTable” selektiert wurden, soll die Operation mit diesem Datensatz erfolgen.
Bei dieser Anforderung ergibt sich das Problem, dass die Standardvorgehensweise zur Abfrage des selektierten Datensatzes nicht funktioniert, da hierfür eine der DataSources “direkt” angesprochen werden muss. Welche DataSource nun aber die “aktive” ist, lässt sich leider nicht ermitteln, da standardmäßig jede DataSource einen “aktiven” Datensatz hat und somit eine Unterscheidung, ob der Aufruf für die “CustTable” oder “SalesTable” erfolgen soll, nicht möglich ist.
Über die Methode “docCursor” der Klasse FormRun bietet sich eine zweite Möglichkeit, den aktiven (ausgewählten) Datensatz zu ermitteln. Dieses Vorgehen wird z.B. vom Dokumentenmangement (Form “DocuView”) verwendet, um den zuletzt gewählten Datensatz zu ermitteln und somit die dem Datensatz zugeordneten Dokumente anzuzeigen.
Leider scheidet dieser Weg ebenfalls aus, da ein “Click” auf den Button zur Folge hat, dass der jeweils aktive Datensatz der “Button-DataSource” durch die Methode “docCursor” zurück geben wird. Dies ist soweit auch logisch, da ein Button immer einer DataSource zugeordnet ist (entweder über Angabe in der entsprechenden Eigenschaft des Buttons oder, wenn nicht festgelegt, die erste DataSource der Query).
Wie ist es nun aber möglich, dennoch den zuletzt selektierten (ausgewählten) Datensatz zu ermitteln, wenn die im Standard verwendeten Wege nicht funktionieren?
Um das gewünschte Ziel zu erfüllen (bestimmen, welcher der zuletzt selektierte Datensatz ist) muss eine kleine funktionale Erweiterung der “Info” Klasse durchgeführt werden.
Zuerst müssen in der “classDeclaration” der Klasse “Info” zwei neue Variablen/Buffer zum Speichern des selektierten Datensatzes erstellt werden.
final class Info extends xInfo { #SysTaskRecorderMacro ObjectIdent docuView; ObjectIdent lastActivatedForm; ... // New code --> Common lastSelectedRecord; Common selectedRecord; //New code <-- #Define.CurrentVersion(1) }
Nun müssen noch einige neue Methoden für die Klasse “Info” erstellt werden, damit die Variablen/Buffer geschrieben, abgefragt und gelöscht werden können.
private void setLastSelectedRecord(FormRun _formRun) { ; if(_formRun.docCursor()) { if(lastSelectedRecord) { lastSelectedRecord = selectedRecord; } else { //Only get the record data, not the cursor lastSelectedRecord = _formRun.docCursor().data(); } //Only get the record data, not the cursor selectedRecord = _formRun.docCursor().data(); } }
private void clearLastSelectedRecord() { ; lastSelectedRecord.clear(); selectedRecord.clear(); }
common lastSelectedRecord() { ; return lastSelectedRecord; }
Zum Schluss müssen diese neu erstellten Methoden noch entsprechend in der Methode “formNotify” aufgerufen werden.
void formNotify(FormRun formRun,FormNotify event) { switch (event) { case FormNotify::Activate: this.activate(formRun); if (docu) docu.reSearch(formRun); //New code --> this.setLastSelectedRecord(formRun); //New code <-- break; case FormNotify::DeActivate: break; case FormNotify::Open: this.open(formRun); if (docu) docu.set(formRun); break; case FormNotify::Close: this.close(formRun); if (docu) docu.clear(formRun); //New code --> this.clearLastSelectedRecord(); //New code <-- break; case FormNotify::RecordChange: if (docu) docu.reSearch(formRun); //New code --> this.setLastSelectedRecord(formRun); //New code <-- if (formRun.isWorkflowEnabled()) { // only refresh controls if current ds equals workflow data source if (formRun.objectSet().name() == formRun.workflowDataSource().name()) formRun.updateWorkflowControls(); } break; case FormNotify::NoteClicked: if (docu) docu.note(formRun); break; } }
Durch diese kleine Codeänderung kann nun der zuletzt ausgewählte Datensatz, unabhängig von einer DataSource, abgefragt werden.
void clicked() { Common currentRecord; DictTable dictTable; ; super(); //Get the last selected record currentRecord = infolog.lastSelectedRecord(); dictTable = new DictTable(currentRecord.TableId); setPrefix(tableid2name(currentRecord.TableId)); info(strfmt("%1 - %2", currentRecord.(dictTable.titleField1()), currentRecord.(dictTable.titleField2())));} Bezogen auf die zuvor beschrieben Anforderung könnte das Ergebnis so aussehen.

In mehreren Artikeln wurde bereits beschrieben, wie LookupForms erstellt werden müssen, um alle Funktionen bereit zu stellen, die auch durch einen Standard-Lookup bereit gestellt werden.
Ein guter Artikel ist zum Beispiel auf Axaptapedia zu finden: http://axaptapedia.com/Lookup_Form
Leider wurde in diesem Artikel auf eine Kleinigkeit nicht hingewiesen, die allerdings für sehr viel Verwirrung sorgen kann.
Um beim Öffnen des Lookups den bereits eingetragenen Wert zu selektieren (in dem Control der aufrufenden Maske), müssen wie in dem Artikel beschrieben, die Methoden „executeQuery“ und „init“ der DataSource der Lookup-Maske überschrieben werden.
Beispiel:
public void executeQuery()
{
FormStringControl callerControl = SysTableLookup::getCallerStringControl(element.args());
;
super();
xyz_ds.findValue(fieldnum(xyz,id),callerControl.text());
}
public void init()
{
Query q = new Query();
QueryBuildDataSource qbds;
;
super();
qbds = q.addDataSource(tablenum(xyz));
qbds.orderMode(OrderMode::OrderBy);
qbds.addSortField(fieldNum(xyz,some_other_field));
this.query(q);
}
Es wird auch beschrieben, dass in der Methode “init” der Datasource eigene Ranges oder Sortings definiert werden können. Dies ist soweit auch richtig, allerdings mit einer Ausnahme.
Wird auf dem Feld, welches bei dem Aufruf von „Datasource.findValue“ in der Methode „init“ angegeben wurde (sollte auch immer das Feld sein, dessen Wert durch den Lookup ausgewählt wird), eine Range definiert, so funktioniert die Selektion des zuvor gewählten Wertes nicht mehr und es wird immer der erste Wert im Lookup selektiert bzw. ausgewählt.
Beispiel:
public void executeQuery()
{
FormStringControl callerControl = SysTableLookup::getCallerStringControl(element.args());
;
super();
xyz_ds.findValue(fieldnum(xyz,id),callerControl.text());
}
public void init()
{
Query q = new Query();
QueryBuildDataSource qbds;
QueryBuildRange range;
;
super();
qbds = q.addDataSource(tablenum(xyz));
qbds.orderMode(OrderMode::OrderBy);
qbds.addSortField(fieldNum(xyz,some_other_field));
range = qbds.addRange(fieldnum(xyz,id));
range.value(SysQuery::valueNot(<someValue>));
this.query(q);
}
Dieses Verhalten lässt sich allerdings umgehen, wenn anstelle des Aufrufs von “DataSource.findValue” in der „ExecuteQuery“ Methode der DataSource der Aufruf von „DataSoucre.findRecord“ verwendet wird. Hierfür muss aber der entsprechende Datensatz des zuvor oder bereits ausgewählten Wertes ermittelt werden um diesen beim Aufruf von „DataSource.findRecord“ als Parameter zu übergeben.
Beispiel: public void executeQuery()
{
FormStringControl callerControl;
Xyz xyzRecord;
;
callerControl = SysTableLookup::getCallerStringControl(element.args());
xyzRecord = Xyz::find(callerControl.text());
super();
xyz_ds.findRecord(xyzRecord);
}
public void init()
{
Query q = new Query();
QueryBuildDataSource qbds;
QueryBuildRange range;
;
super();
qbds = q.addDataSource(tablenum(xyz));
qbds.orderMode(OrderMode::OrderBy);
qbds.addSortField(fieldNum(xyz,some_other_field));
range = qbds.addRange(fieldnum(xyz,id));
range.value(SysQuery::valueNot(<someValue>));
this.query(q);
}
Es muss also darauf geachtet werden, ob eine Einschränkung (Range) auf dem „ID-Feld“ benötigt wird oder nicht.
Wird keine Einschränkung benötigt, kann, wie in dem Artikel auf Axaptapedia beschrieben, mit „DataSource.findValue“ gearbeitet werden um den entsprechenden Datensatz zu selektieren. Wird aber eine solche Einschränkung benötigt, muss mit „DataSoucre.findRecord“ gearbeitet werden.
Egal ob Anfänger oder schon erfahrener Dynamics AX Entwickler. Von Zeit zu Zeit tauchen immer die gleichen Fragen und/oder Problemstellungen auf.
Was immer mal wieder in Foren oder Newsgroups zu lesen ist, ist die Frage, wie die Berechnung des Artikelpreises für eine Debitor mit X++ Code durchgeführt werden kann. Der Standard von Microsoft Dynamics AX bringt zwar eine Maske zur “Preisabfrage” mit sich, welche diese Problemstellung eigentlich schon löst, allerdings ist es manchmal notwendig, die Preisberechnung z.B. in einer Klasse durchzuführen, um mit dem Ergebnis weitere Operationen durchführen zu können.
Da in Dynamics AX Preise für einen Artikel nicht nur “direkt” im Artikelstamm, sondern auch in den Handelsvereinbarungen, hinterlegt werden können, stellt der Standard von Dynamics AX die Klasse “PriceDisc” zur Verfügung, welche alle Möglichkeiten der Preispflege berücksichtig. Unter Anderem werden auch Rabatte bzw. Zuschläge der Handelsvereinbarungen durch diese Klasse berücksichtigt.
Die Klasse “PriceDisc” stellt die statische Methode “actualSalesPriceDisc” bereit. Mit dieser können z.B. der Verkaufspreis pro Preiseinheit, der Rabatt oder die Zuschläge berechnet und/oder ermittelt werden. Durch eine zweite statische Methode “price2Amount” lässt sich anschließend der Nettobetrag eines Artikels ausrechnen.
Beispiel:
CustTable custTable = CustTable::find("5010");InventTable inventTable = InventTable::find("1000");Qty qty = 1.00; SalesPrice salesPrice; // Verkaufspreis je Preiseinheit PriceMarkup salesMarkup; //Preis sonst. Zuschläge PriceUnit priceUnit; // Preiseinheit SalesLineDisc salesLineDisc; // Rabatt SalesLinePercent salesLinePercent; DiscPct percent1; DiscPct percent2; PriceDiscTable actualPrice; // Gefundener Datensatz der Handelsvereinbarung (Verkaufspreis) PriceDiscTable actualDisc; // Gefundener Datensatz der Handelsvereinbarung (Rabatte) AmountCur lineAmount; // Nettobetrag ; [salesPrice, salesMarkup, priceUnit, salesLineDisc, salesLinePercent, percent1, percent2, actualPrice, actualDisc] = PriceDisc::actualSalesPriceDisc(custTable, inventTable, qty); lineAmount = PriceDisc::price2Amount(salesPrice, priceUnit, salesLineDisc, qty, qty, salesMarkup, salesLinePercent, custTable.Currency, 0);
In dem Artikel "Anzeige von Lagerdimensionen auf Masken" wurde bereits gezeigt, was zu tun ist, um die Anzeige von Lagerdimensionen auf Masken dynamisch anpassen zu können bzw. das Standardverhalten für die Anzeige von Lagerdimensionen zu implementieren.
Manchmal soll eine ähnliche Funktionalität auch für Berichte bereit gestellt werden, um zum Beispiel vor der Berichtserstellung auswählen zu können, welche Lagerdimensionen auf dem Bericht(Report) angedruckt werden. Auch hierfür sind im Dynamics AX Standard die entsprechenden Funktionalitäten (oder besser Klassen) bereits vorhanden, sodass diese nur verwendet werden müssen.
Als Ausgangsbasis für den Bericht dient ebenfalls die Tabelle „AKU_DemoTable“.

Der Bericht soll nun, die in dieser Tabelle gespeicherten Datensätze andrucken/ausgeben.

Wie bei Masken, muss auch für einen Bericht, die Query entsprechend um die Tabelle InventDim ergänzt werden. Hierbei ist zu beachten, dass die Eigenschaften (Properties) „FetchMode“ auf „1:1“ und „Relations“ auf „Yes“ gesetzt werden.

Als nächstes muss nun, ebenfalls analog zu dem Vorgehen bei Masken, die Feldgruppe „InventoryDimensions“ in den Designzweig des Reports aufgenommen werden. Beispielhaft wird diese in einem Body-Element erstellt.

Nun müssen noch einige Anpassungen an den Methoden des Berichts durchgeführt werden, damit das gewünschte Ergebnis erreicht werden kann. Bezogen auf die Möglichkeit, die zu druckenden Lagerdimensionen bestimmen zu können, müssen die Methoden „classDeclaration“, „run“, „dialog“ und „getFromDialog“ wie folgt überschrieben werden. Auch das Überschreiben der Methoden „pack“ und „unpack“ ist hilfreich (für die Lagerdimensionsanzeige nicht zwingend erforderlich), da über diese die Speicherung der „Nutzungsdaten“ realisiert wird.
public class ReportRun extends ObjectRun { InventDimParm inventDimParm; DialogRunbase dialog; DialogGroup dialogInventoryDimensions;
#define.CurrentVersion(1) #localmacro.CurrentList inventDimParm #endmacro }
void updateDesign() { ; InventDimCtrl::updateReportVisible(element, inventDimParm); }
public void run() { ; this.updateDesign(); super(); }
public Object dialog(Object _dialog) { ; dialog = _dialog; dialogInventoryDimensions = inventDimParm.addFieldsToDialog(dialog,"@SYS53654",true, false, "@SYS102592"); return dialog; }
public boolean getFromDialog() { ; inventDimParm.getFromDialog(dialog, dialogInventoryDimensions); return true; }
public container pack() { return [#CurrentVersion, #CurrentList]; }
public boolean unpack(container packedClass) { Version version = RunBase::getVersion(packedClass); ; switch(version) { case #CurrentVersion: [version,#CurrentList] = packedClass; break; default: return false; } return true; }
Wird nun der Bericht geöffnet, zum Beispiel über ein MenuItem, kann in einem Dialog ausgewählt werden, welche Lagerdimensionen auf dem Report angedruckt werden sollen.

Der ausgegebene Bericht(Report) sieht, unter Berücksichtigung der im Dialog gewählten Einstellungen, wie folgt aus.

Das vorgestellt Bespiel steht hier als Download bereit um die einzelnen Schritte genau ansehen/nachvollziehen zu können.
AKU_Demo_InventDimRep.rar (1,99 KB)
Im Standard von Microsoft Dynamics AX besteht auf jeder Maske, auf der Artikel und deren Lagerdimensionen angezeigt werden, die Möglichkeit, die Lagerdimensionen, bzw. die angezeigten Felder der Lagerdimensionen, über die Funktion "Lager-Dimensionenanzeige" entsprechend zu steuern.
Die einzelnen Elemente (Felder) der Lagerdimension können über diese Funktion ein- bzw. ausgebledet werden.
Weiterhin ist es auch möglich, durch Parametrisierung zu bestimmen, ob ein Feld einer Lagerdimension eingeben werden muss (Mussfeld) oder ob überhaupt eine Eingabe möglich ist.
Ein gutes Beispiel hierfür ist die Maske "Aufträge".
Wie ist es nun, wenn eine neue Maske erstellt werden soll, welche Artikelinformation und Lagerdimensionen anzeigen soll? Wie genau muss vorgegangen werden, um die bereits an vielen Stellen im Standard verwendete Funktionalität auch für die selbst erstellte Maske bereitzustellen?
Gehen wir einmal davon aus, es wurde eine neue Tabelle erstellt, welche die Artikelnummer (ItemId) und die Lagerdimensionsnummer (InventDimId) speichert. Für diese Tabelle soll eine Maske erstellt werden, úm dem Benutzer die Möglichkeit zu geben, Datensätze zu erfassen, zu ändern oder einfach nur anzuzeigen.
Dies könnte z.B. so aussehen:

Um nun die Funktion der Lagerdimensionensteuerung einzubauen muss zuerst die Tabelle InventDim als DataSource der Maske hinzugefügt werden. Anschließend müssen die Eigenschaften (Properties) der DataSource noch auf folgende Werte geändert werden.
|
Name |
InventDim |
|
JoinSource |
Haupt-Datenquelle (hier: AKUDemoTable) |
|
LinkType |
InnerJoin |
|
DelayActive |
No |
|
InsertAtEnd |
No |
|
InsertIfEmpty |
No |

Nun muss eine neue ButtonGroup (Name: "Inventory") im Designzweig der Maske erstellt werden. Diese sollte das LAbel "Lager" zugewiesen werden. Nun noch das MenuItem "InventDimParmFixed" in diese ButtonGroup ziehen (z.B. per drag & drop aus dem AOT) und dem so erstellten MenuItemButton folgende Eigenschaften zuweisen.
|
Name |
InventDimParmFixed |
|
MenuItemName |
InventDimParmFixed |
|
DataSource |
Haupt-Datenquelle (hier: AKUDemoTable) |

Über das MenuItem (oder genauer über den erstellten MenuItemButton) wird nun wie im Standard, die Maske "Lagerdimensionen" zu öffnen.

Allerdings öffnet sich die Maske „Lagerdimensionen“ noch nicht wie gewünscht über den MenuItemButton. Hierfür sind noch weitere Anpassungen notwendig.
Damit sich die Maske „Lagerdimensionen“ wie gewünscht öffnet muss die neue Maske die Methode „inventDimSetupObject“ implementieren welche eine Instanz von „InventDimCtrl_Frm“ zurück gibt.
Die Klasse „InventDimCtrl_Frm“, bzw. eine der von ihr abgeleiteten Klassen, steuert z.B. welche Lagerdimensionen für den aktuellen Datensatz zulässig sind oder welche Dimensionen für den aktuellen Datensatz angegeben werden müssen, damit dieser gespeichert werden kann.
Da über die Parametrisierung der Lagersteuerungsgruppen und der Modulparameter hierfür durchaus unterschiedliche Einstellungen gewählt werden können, sind in Dynamics AX etliche Ableitungen dieser Klasse vorhanden (jede wird für eine oder mehrere andere Masken verwendet).

Je nachdem, was für eine Funktionalität bzw. was für ein Business-Prozess erstellt werden soll, kann entweder eine der bereits im Standard vorhandenen Klassen verwendet werden oder es muss eine neue Klasse geschrieben werden, um die benötigte Funktionalität zu liefern (z.B. welche Dimensionen immer angezeigt werden müssen).
Das Erstellen einer neuen Klasse, welche von „InventDimCtrl_Frm“ abgeleitet ist, ist recht einfach.
Es sollte immer die Methode „new“ überschrieben werden und mindestens die statischen Methoden „construct“ und „newFromForm“ erstellt werden.
class AKUInventDimCtrl_Frm_Demo extends InventDimCtrl_Frm { }
protected void new() { super(); }
public static AKUInventDimCtrl_Frm_Demo construct() { return new AKUInventDimCtrl_Frm_Demo(); }
static AKUInventDimCtrl_Frm_Demo newFromForm(FormRun _formRun) { AKUInventDimCtrl_Frm_Demo inventDimCtrl = AKUInventDimCtrl_Frm_Demo::construct(); InventDimAxFormAdapter adapter = InventDimAxFormAdapter::newFromForm(_formRun); ; inventDimCtrl.parmCallingElement(adapter); inventDimCtrl.init(); return inventDimCtrl; }
Weiterhin können noch andere Methoden überschrieben werden, um z.B. zu steuern, welche Felder der Tabelle „InventDim“ beim Aufruf der Maske angezeigt werden sollen. Weitere Informationen hierzu sind im Microsoft Dynamics AX Developer Center zu finden: http://msdn.microsoft.com/en-us/library/cc618009.aspx
NoYes mustShowGridField(fieldId _dimFieldId) { NoYes ret;
ret = super(_dimFieldId); //always show InventLocationId in Grid if(_dimfieldId == fieldnum(InventDim, InventLocationId)) { ret = NoYes::Yes; }
return ret; }
Wie zuvor beschrieben muss nun die Methode „inventDimSetupObject“ auf der Maske implementiert werden. Das diese eine Instanz von „InventDimCtrl_Frm“ zurück geben muss, ist diese ebenfalls zu erzeugen. Als erstes muss eine Objektvariable für das „InventDimCtrl_Frm“ Objekt erstellt werden.
public class FormRun extends ObjectRun { InventDimCtrl_Frm inventDimFormSetup; }
Anschließend kann die Methode „inventDimSetupObject“ erstellt werden.
Object inventDimSetupObject() { return inventDimFormSetup; }
Da die Objektvariable durch diesen Quelltext noch nicht initialisiert wird, muss noch entsprechender Code zur Initialisierung geschrieben werden.
void updateDesign(InventDimFormDesignUpdate mode) { inventDimParm inventDimParmShow; inventDimParm inventDimParmEnabled; ; switch (mode) { case InventDimFormDesignUpdate::Init : if (!inventDimFormSetup) { inventDimFormSetup = AKUInventDimCtrl_Frm_Demo::newFromForm(element); } inventDimFormSetup.parmSkipOnHandLookUp(true);
case InventDimFormDesignUpdate::Active : inventDimFormSetup.formActiveSetup( inventTable::find(AKUDemoTable.ItemId).dimGroupId); inventDimFormSetup.formSetControls(true); break;
case InventDimFormDesignUpdate::FieldChange : inventDimFormSetup.formActiveSetup( inventTable::find(AKUDemoTable.ItemId).dimGroupId); inventDim.clearNotSelectedDim(inventDimFormSetup.parmDimParmEnabled()); inventDimFormSetup.formSetControls(true); break;
default : throw error(strfmt("@SYS54195",funcname())); } }
public void init() { ; super(); element.updateDesign(InventDimFormDesignUpdate::Init); }
Da die Logik, welche durch die Methode „updateDesign“ bereit gestellt wird, mehrfach benötigt wird, erfolgt die Initialisierung des „InventDimCtrl_Frm“ Objekts nicht direkt in der „init“ Methode.
Damit die in der Maske erstellten Datensätze auch richtig gespeichert werden können, müssen nun noch einige weitere Anpassungen an den Methoden der Datenquellen vorgenommen werden.
Datasource „AKUDemoTable“ (Hauptdatenquelle): Hier sind die Methoden „write“, „validateWrite“ und „active“ zu überschreiben.
public void write() { ; ttsbegin;
AKUDemoTable.inventDimId = InventDim::findOrCreate(InventDim).inventDimId;
super();
if(AKUDemoTable.inventDimId != InventDim.inventDimId) { InventDim.data(InventDim::find(AKUDemoTable.inventDimId)); InventDim_ds.setCurrent(); }
ttscommit; }
public boolean validateWrite() { boolean ret; ; AKUDemoTable.InventDimId = inventDim::findOrCreate(InventDim).inventDimId;
ret = super(); return ret; }
int active() { int ret; ; ret = super();
element.updateDesign(InventDimFormDesignUpdate::Active);
inventDim_DS.active();
return ret; }
Datasource „InventDim“: Hier müssen die Methoden „initValue“ und „write“ überschrieben werden.
public void initValue() { ; InventDim.data(InventDim::find(AKUDemoTable.inventDimId));
super(); }
public void write() { //super(); }
Hierbei muss unbedingt beachtet werden, dass der „super“ Aufruf in der „write“ Methode der Datasource „InventDim“ auskommentiert wird, um das Speichern von falschen InventDim Datensätzen zu verhindern.
Als letzte Methode sollte nun noch die Methode „modified“ des DataSource-Field „ItemId“ der Datasource „AKUDemoTable“ überschrieben werden, damit auf eine Änderung der Artikelnummer reagiert werden kann (z.B. Artikelabhängige Anzeige der Lagerdimensionen).
public void modified() { ; super(); element.updateDesign(InventDimFormDesignUpdate::FieldChange); }
Somit sind alle benötigten Quelltextanpassungen durchgeführt, sodass nur noch die Feldgruppe „InventoryDimensions“ der DataSource „InventDim“ mit in das Grid gezogen werden muss um die Lagerdimensionen auf der Maske anzuzeigen. Optional kann diese auch in die TabPage „Dimensions“ gezogen werden um ein standardkonformes Aussehen der Maske zu erhalten.

Das vorgestellt Bespiel steht hier als Download bereit um die einzelnen Schritte genau ansehen/nachvollziehen zu können. AKU_Demo_InventDimFrm.rar (2,71 KB)
Oft ist in Newsgroups und Foren die Frage zu lesen, wie .NET (CLR) Arrays in X++ verwendet, bzw. wie diese deklariert werden können. Im Großen und Ganzen unterscheidet sich die Syntax für die Verwendung eines .NET Array kaum von der eines reinen X++ Array. .NET Arrays können in X++ sogar auf zwei verschiedene Arten deklariert werden.
Variante 1:
Die Deklaration eines .NET Arrays erfolgt analog zu der Deklaration eines "reinen" X++ Arrays:
System.Object[] arrayOfObjects; System.Int32[] arrayOfIntegers;
Die Syntax für die Instanzierung des .NET Array weicht allerdings leicht von der "normalen" X++ Syntax ab:
arrayOfObjects = new System.Object[10](); arrayOfIntegers = new System.Int32[3]();
Wichtig ist hierbei, dass immer "()" verwendet wird.
Um die Werte eines .NET Arrays zu setzen wird die Methode "SetValue()" verwendet:
arrayOfObjects.SetValue(myObject, 0); arrayOfIntegers.SetValue(300, 1);
Um Werte aus einem Array abzufragen kann die Methode "GetValue" verwendet werden:
arrayOfObjects.GetValue(0); arrayOfIntegers.GetValue(1);
Eine weitere sehr nützliche Methode ist die Methode "get_Length()". Diese liefert die Anzahl der Array-Elemente zurück.
Variante 2:
Alternativ zur ersten Variante besteht noch die Möglichkeit, ein .NET Array über die Klasse System.Array zu erzeugen. Leider stößt man bei diesem Weg immer mal wieder auf kleinere Probleme, weswegen die erste Variante für die Verwendung von .NET Array bevorzugt werden sollte.
Eine etwas ausführlichere Beschreibung der Verwendung von .NET Array in X++ bzw. deren besonderheiten und Abweichungen zur "normalen" X++ Syntax kann im Microsoft Dynamics AX Developer Center gefunden werden.
How to: Use X++ Syntax for CLR Arrays
Wer schon mit dem AIF in der Version 4.0 von Microsoft Dynamics AX gearbeitet hat wird sich daran erinnern, dass ein Debuggen des Quellcodes, welcher durch das AIF ausgeführt wird, nur möglich ist, wenn hierfür eine kleine Codeanpassung in den Klassen "AifInboundProcessingService" und "AifOutboundProcessingService" vorgenommen wird. Das genaue Vorgehen für die Version 4.0 von Dynamics AX ist in diesem Artikel beschrieben.
Für Dynamics AX 2009 kann diese Quellcodeänderung allerdings nicht so ohne weiteres angewendet werden, da für Dynamics AX 2009 einige Features ergänzt wurden (z.B. paralelle Verarbeitung von AIF Nachrichten) und somit der Quellcode der beiden Klassen einige Abweichungen zu dem der Version 4.0 hat.
Debuggen von ausgehenden Nachrichten
Um das Debuggen von ausgehenden Nachrichten zu ermöglichen, muss die Methode "runAsWrapper" der Klasse "AifOutboundProcessingService" angepasst werden. Der Aufruf von "runAS" (Zeile 22) muss durch "AifOutboundProcessingService::processAsUser(messageIdContainer)" ersetzt werden.
... try { // runAs currentUser and process all messages in the container. new RunAsPermission(runAsUserId).assert();
// AKU, Enable Debuging for outbound messages - START --> // Do not use in production system!!! // BP deviation documented //runas(runAsUserId, // classnum(AifOutboundProcessingService), // staticmethodstr(AifOutboundProcessingService, processAsUser), // messageIdContainer, // runAsCompany); AifOutboundProcessingService::processAsUser(messageIdContainer); // AKU, Enable Debuging for ourbound message - END -->
// Revert the permission CodeAccessPermission::revertAssert(); } ...
Debuggen von eingehenden Nachrichten
Um das Debuggen von eingehenden Nachrichten zu ermöglichen, muss die Methode "runAsWrapper" der Klasse "AifInboundProcessingService" angepasst werden. Der Aufruf von "runAS" (Teile 24) muss durch "AifInboundProcessingService::processAsUser(messageIdContainer)" ersetzt werden.
... try { // Convert to Axapta UserId axaptaUserId = AifEndpointUser::getAxaptaUser(runAsUserId).Id;
new RunAsPermission(axaptaUserId).assert();
// AKU, Enable Debuging - START --> // Do not use in production system!!! // BP deviation documented //runas(axaptaUserId, // classnum(AifInboundProcessingService), // staticmethodstr(AifInboundProcessingService, processAsUser), // messageIdContainer); AifInboundProcessingService::processAsUser(messageIdContainer); // AKU, Enable Debuging - END -->
CodeAccessPermission::revertAssert(); } ...
Für beide Quellcodeänderung sollte noch erwähnt werden, dass diese in einem Produktivsystem nicht durchgeführt werden sollten, da dies Auswirkungen auf die Verarbeitung der Stapelprozesse des AIF's haben könnte.
Das Application Integration Framework von Dynamics AX basiert auf Dokumenten (Axd<Document> Klassen). Eigene Dokumente lassen sich reicht einfach per Hand oder mit Hilfe des Dokumenten-Wizards erstellen. In der Version 2009 von Dynamics AX erstellt dieser Wizard auch gleichzeitig den benötigenten Service (WCF-Webservice) und andere benötigte Elemente wie (Serviceklassen und Macros). Welche Schritte hierfür benötigt werden ist zum Beispiel im Microsoft Dynamics AX Developer Center beschrieben.
Ein kleines Problem entsteht, wenn das neue Dokument, genauer gesagt die Elemente oder Objekte des Dokuments, in einem Layer (zum Beispiel CUS-Layer) entwickelt wird und später, aus welchen Gründen auch immer, alle Objekte des Dokuments (Query, Ax<Table> Class, Axd<Document> Class) in einen anderen Layer (zum Beispiel VAR-Layer) verschoben werden.
Nach der "Verschiebung" des neuen Dokuments in einen anderen Entwicklungslayer werden durch die AIF-Konfigurationsmasken (siehe Maske Dienstleistungen) allerdings keine Operationen (Insert, Update, Delete, FindKey, etc.) mehr angezeigt. Auch an anderen Stellen, wie zum Beispiel der Endpunktkonfiguration, sind die entsprechenden Operationen nicht mehr auswählbar oder vorhanden.
Der Grund hierfür liegt in der ClassId der Serviceklasse des neuen Dokuments. Dieser, wie auch jedem anderen Objekt, wird beim Import in einen anderen Layer eine neue ID zugewiesen, wenn nicht explizit angegeben wurde, dass der Export und Import mit ID's erfolgen soll. So kann es sein, dass die Klasse mit der ID 40001 nach dem Import in einen anderen Layer die ID 300001 zugeordnet ist.
Da wärend der Konfiguration des AIF's der AOT nach Dokumenten/Services durchsucht wird und für jedes Dokument bzw. jeden Service ein Datensatz in der Tabelle "AifService" sowie ein bis mehrere Datensätze in der Tabelle "AifAction" erzeugt wird, welche alle eine Referenz auf die ClassId der Serviceklasse enthalten, kommt es jetzt zu dem genannten Problem. Der Id 40001 ist nun keine Klasse oder noch schlimmer eine andere Klasse zugewiesen. Auch eine "Aktualisierung" dieser Datensätze über die Aktualisierungsfunktion, welche auf der Maske Dienstleistungen bereit gestellt wird, führt nicht zum gewünschten Erfolg.
Genau in dieser Funktion scheint sich ein kleiner Fehler eingeschlichen zu haben. Dort wird zwar eine Aktulisierung der ClassId für die Datensätze der Tabelle "AifService", aber nicht für die Datensätze der Tabelle "AifAction", durchgeführt.
Um diese Verhalten zu reproduzieren, muss folgendes gemacht werden:
- Erstellen eines neuen AIF Dokuments bzw. AIF Services.
- Über die Maske Dienstleistungen, zu finden unter "Grundeinstellungen -> Einstellungen -> Application Integration Framework -> Dienstleistungen", Funktion "Aktualisieren" das neue Dokument / den neuen Service "aktivieren".
- Über den Button "Servicearbeitsgänge" können nun alle Operationen welche durch das Dokument / den Service bereit gestellt werden eingesehen werden.
- Verschieben aller Elemente des Dokuments / des Services in einen anderen Layer.
- Schritt 2 erneut druchführen.
- Über den Button "Servicearbeitsgänge" werden keine Operation mehr angezeigt.
Um dieses Problem zu lösen, gibt 2 Möglichkeiten.
Entweder manuelles Löschen alle zu diesem Dokument/Service gehörigen Datensätze der Tabelle "AifAction" oder aber man ergänzt die Methode "registerOperations" der Klasse "AifServiceGenerationManager" um ein wenig X++ Code (bei Zeile 43) welcher nicht nur den Namen der Operation aktualisiert sondern auch die ClassId. Da dieser Code sehr einfach ist verzichte ich an dieser Stelle auf ein Beispiel.
Leider tritt dieses Problem auch mit Dynamics AX 2009 Service Pack 1 auf.
In Microsoft Dynamics AX werden alle Informationsmeldungen, Warnungen und Fehlermeldungen in einem Fenster, dem so genannten Infolog, ausgegeben.
Wie Informationen, Warnungen oder Fehlermeldungen erzeugt werden können, ist an vielen Stellen bereits beschrieben. Ein wie ich finde sehr guter Artikel über dieses Thema ist dieser: The user friendly Infolog.
An dieser Stelle soll aber kurz beschrieben werden, wie die Meldungen des Infologs ausgewertet werden können, um zum Beispiel zu ermitteln, ob das Infolog eine Fehlermeldung oder auch eine Warnung enthält.
Zuerst stellt sich die Frage, warum benötigt man überhaupt diese Art von Information da in Dynamics AX die Möglichkeit besteht, verschiedenste Operationen innerhalb einer Transaktion zu Kapsel und diese bei Auftreten eines Fehlers oder Erzeugung einer Fehlermeldung (Stichwort "throw error") automatisch rückgängig zu machen.
Diese Frage soll anhand eines Beispiels beantwortet werden.
Angenommen wir möchten eine Anpassung in Dynamics AX schreiben, welche es ermöglicht, Änderungen an Stücklisten und Arbeitsplänen von Produktionsaufträgen zu automatisieren und alle getätigten Änderungen in einer Transaktion zu kapseln. Anschließend soll noch der Status der Produktions aktualisiert werden. Ebenfalls innerhalb der Transaktion. Zum Beispiel soll der Produktionsauftrag gestartet werden.
Hierfür ist es notwendig verschiedene Standardfunktionen von Dynamics AX zu verwenden, die bereits eine Fehlerbehandlung implementiert haben und somit keine Fehler mehr "melden", welche den Abbruch einer Transaktion zu Folge haben könnten. Somit wird zwar gewährleistet, dass alle "Unterfunktionen" in sich richtig auf Fehler richtig reagieren, aber dennoch könnte eine Dateninkonsistenz entstehen, da nicht alle Operationen rückgängig gemacht werden. Es könnte Beispielhaft sein, dass die Anlage von neuen Stücklistenpositionen und/oder Arbeitsgangpositionen funktioniert, die spätere Statusaktualisierung des Produktionsauftrags aber nicht. Die erstellten Stücklistenpositionen und/oder Arbeitsgangpositionen würden im System gespeichert (bleiben).
Abhilfe für dieses Problem kann duch die Auswertung des Infologs und manuellen Aufrufs von "ttsabort" geschaffen werden.
Mit dem "SysInfologEnumerator" können alle Meldungen, welche in das Infolog geschrieben wurden, durchlaufen werden. Über die Methode "currentException" kann anschließend ausgewertet werden, um was für eine Meldung (Information, Warnung, Fehler) es sich handelt.
Hier ein kurzes Beispiel, wie dies aussehen könnte:
boolean hasError() { SysInfologEnumerator enum; SysInfoAction action; boolean hasError = false; ; //Analyse the infolog to see if there are any warnings/errors enum = SysInfologEnumerator::newData(infolog.infologData()); while (enum.moveNext()) { switch (enum.currentException()) { case Exception::Error: case Exception::Warning: hasError = true; break; }
infolog.add(enum.currentException(), enum.currentMessage(), enum.currentHelpUrl()); } return hasError; }
Es muss allerdings beachtet werden, dass alle Meldungen des Infologs, durch den Aufruf von "SysInfologEnumerator::newData(...)", gelöscht werden. Sollen diesese Meldungen nach der "Auswertung" dennoch dem Benutzer angezeigt werden, müssen diese wieder manuell in das Infolog geschrieben werden (über "infolog.add(..)").
Im Standard von Microsoft Dynamics AX 2009 werden verschiedenste Business Intelligence Funktionen mitgeliefert. Die hierfür benötigen Cubes und Dimensionen erstellt Dynamics AX 2009 unter Verwendung der Analysis-Extension direkt in den Analysis Services des SQL Servers. Dies ist schon einmal sehr schön, da die meiste Arbeit durch die Installationsroutinen abgenommen wird.
Muss oder möchte man allerdings die mitgelieferten Cubes oder Dimensionen an die eigenen Gegebenheiten anpassen, muss zuerst ein BI-Projekt für Visual Studio erzeugt werden, da die Bearbeitung in Visual Studio erfolgt (über die Funktion „BI-Projekt generieren“).
Es können einige „allgemeine“ Einstellungen für die Erstellung eines BI-Projektes über die Funktion „Generierungsoptionen für BI-Projekt“ getätigt werden. Beispielhaft sei hier die Einstellung der Zeitdimensionen genannt.
Allerdings kann es sein, dass man anstatt der erwartet Maske eine Fehlermeldung ausgegeben bekommt. Dies kann besonders bei Verwendung, der für Dynamics AX 2009 bereit gestellten, Demo Daten der Fall sein.

Leider ist diese Fehlermeldung nicht sehr Aussagekräftig.
Hier hilft ein Blick in das Ereignisprotokoll des Dynamics AX Object Servers.

Dies lässt zumindest schon einmal den „groben“ Grund erkennen, was für ein Fehler verursacht wurde bzw. wo der Fehler liegen könnte.
Nach einem Blick in die Tabellendefinition und den Tabellebrowser der angegebenen Tabelle „BIUDMROLES“ wird man feststellen, dass es die angegeben Spalte wirklich nicht gibt. Es gibt aber einen Datensatz mit entsprechender UserGroupId (UserGroupId = PRComplete). Verwendet man nun die Funktion „Gehe zu Haupttabelle“, wird man feststellen, dass es diese Benutzergruppe nicht im System gibt.
Somit ist die Lösung recht einfach. Nachdem der Fehlerhafte Datensatz gelöscht wurde, kann die Maske „Generierungsoptionen für BI-Projekt“ ohne Probleme geöffnet werden.
Microsoft Dynamics AX 2009 bietet die Möglichkeit, den Verlauf eines Dokuments, welches über das AIF exportiert oder importiert wurde zu betrachten. Dies war auch schon mit Microsoft Dynamics AX 4.0 möglich.
Über die Maske "Dokumentverlauf" können alle Dokumente/Nachrichten eingesehen werden, welche über das AIF verarbeitet wurden. Über den Button "Korrelation" ist es sogar möglich, die von der Verarbeitung (schreiben, ändern, lesen, etc.) betroffenen Datensätze anzuzeigen.

So ist es zumindest in der Theorie. In der Praxis sieht es leider etwas anders aus. Nach einem Klick auf den Button "Korrelation" öffnet sich leider nicht wie erwartet die Maske "Dokumentkorrelierung". Stattdessen wird der Debugger (wenn installiert) geöffnet und die Fehlermeldung ausgegeben, dass ein Objekt nicht über die Methode "extendedTypeId" verfügt.

So wie es scheint, hat sich in den Quellcode ein kleiner Fehler eingeschlichen, welcher dazu führt, dass die Maske "Dokumentkorrelierung" niemals geöffnet werden kann. Nach einem Vergleich der Funktionalitäten zwischen Dynamics AX 4.0 und Dynamics AX 2009 kann dieses Verhalten (der Fehler) aber wie folgt beschrieben behoben werden.
- AOT öffnen und zu der Tabelle "AifCorrelation" navigieren.
- Den Quelltext der Methode "displayEntityKey" anzeigen lassen bzw. diese für die Bearbeitung öffnen.
- Folgende Quelltextzeile suchen:
dictField = new DictField(entityKey.parmTableId(), enumerator.currentKey());
- Dieses Zeile abändern in:
dictField = new SySDictField(entityKey.parmTableId(), enumerator.currentKey());
Nach dieser kleinen Quelltextänderung sollte alles wie erwartet funktionierten und die Maske "Dokumentkorrelierung" mit den entsprechenden Datensätzen angezeigt werden.
Wie bereits im diesem Artikel "Fehlermeldung beim Starten des Microsoft Dynamics AX Clients" beschrieben, kann es zu Fehlermeldungen beim Starten des Dynamixs AX 4.0 Client kommen.
Eine weitere Fehlermeldung, welche erzeugt werden kann ist "Incompatible ext. version". Es ist auch möglich, dass diese sogar mehrfach ausgegeben wird.
Grund hierfür ist meist ein Problem mit der TAPI-Integartion des CRM Moduls, bzw. genauer gesagt, ein Problem mit den eingestellten Wählregeln/Standorte der Windows Telefon- und Modemoptionen.
Die Behebung des Fehlers ist eigentlich ganz einfach.
- Wenn die TAPI-Integration nicht genutzt wird, kann diese deaktiviert werden.
Wie dies genau geht kann in diesem Artikel "Fehlermeldung beim Starten des Microsoft Dynamics AX Clients" nachgelesen werden.
- Wenn die TAPI-Integration verwendet werden soll, muss ein neuer Standort in den Windows Telefon- und Modemoptionen erstellt werden.
Das Erstellen eines neuen Standorts erfolgt über "Start -> Systemsteuerung -> Telefon- und Modemoptionen" auf dem jeweiligen Clientcomputer (pro Benutzer). Dort sollte, wenn vorhanden, ein bestehender Standort gelöscht werden und ein neuer angelegt werden.
Wie bereits in diesem Artikel "Auswahl von mehreren Datensätzen in einem Grid-Control (MultiSelect)" beschrieben, kann für ein Grid-Control die Eigenschaft MultiSelect gesetzt werden, womit es ermöglicht wird, dass mehrere Datensätze für eine weitere Verarbeitung ausgewählt werden können.
Dies Funktioniert solange, bis <DataSource>_ds.research() aufgerufen wird. Dieser Aufruf hat zur Folge, dass die Daten der DataSource neu geladen werden und somit auch die Selektierung verworfen wird.
Ein Beispiel wie es nicht funktioniert:
void clicked() { CustTable custTable; ; for (custTable = CustTable_ds.getFirst(true) ? CustTable_ds.getFirst(true) : CustTable_ds.cursor(); custTable; custTable = CustTable_ds.getNext()) { //do something with custTable info(custTable.accountNum); custTable_ds.research(); } }
Es gilt also genau zu beachten zu welchem Zeitpunkt bzw. an welcher Stelle im Quelltext die Methode <DataSource>_ds.research() aufgerufen wird.
Weiterhin kann es zu Problemen beim MultiSelect kommen, wenn in den Methoden der DataSource ein Aufruf von <DataSource>_ds.research() erfolgt. Normalerweise werden die DataSource-Methoden für jeden selektierten Datensatz ausgeführt. Wenn aber innerhalb einer der Methoden, wie z.B. Delete(), wird diese Methode nur für den ersten ausgewählten Datensatz ausgeführt und dann ein Research ausgeführt, was wie schon beschrieben zu einem Verwerfen der Selektierung führt.
Manchmal ist es möglich, dass beim Starten des Microsoft Dynamics AX 4.0 Clients eine oder meherer Fehlermeldungen in einem Infolog-Fenster ausgegeben werden.
Diese Fehlermeldung könnten z.B. "Corrupted ini file" sein.
Die große Frage ist nun, woher kommt diese Fehlermeldung bzw. wodurch wird diese erzeugt. Leider ist die Fehlermeldung, welche im Ereignisprotokoll gefunden werden kann, meist auch nicht besonders hilfreich.
Sollten solche, eher unerklärlichen Fehlermeldungen beim Starten des Dynamics AX Client ausgegeben werden, lohnt sich oft ein Blick in die Systemkonfiguration, welche über Verwaltung, Einstellungen, System, Konfiguration aufgerufen werden kann.
Der Grund für diese Fehlermeldung könnte die aktivierte Telefonieintegration des CRM Moduls sein. Wird diese deaktiviert, sollte die Fehlermeldung nicht mehr erzeugt werden.
Für jede Tabelle können Systemfelder wie Erstellt von, Geändert von, Erstellungsdatum, Erstellungszeit oder Änderungsdatum von Dynamics AX aktiviert werden. Diese Felder werden durch Dynamics AX automatisch gefüllt. Wird zum Beispiel ein neuer Datensatz erzeugt, füllt Dynamics AX die Systemfelder mit den entsprechenden Daten.
Es gibt aber Situationen wo man selber Einfuß auf die Werte dieser Felder nehmen muss. Ein Beispiel hierfür könnte eine Datenübernahme sein, bei der die Informationen über den Ersteller oder das Erstellungsdatum des Datensatzes nicht verloren gehen dürfen.
Wie dies gehen kann zeigt dieses kleine Beispiel:
YourTable table; ; ttsbegin;
//can only be called on server tier. -> method must be executed on server tier. new SkipAOSValidationPermission().assert(); table.skipAosValidation(true);
table.YourField = "Value";
table.overwriteSystemfields(true);
//set your own values for the system fields. table.(fieldnum(Table1, ModifiedDate)) = today() - 2; table.(fieldnum(Table1, CreatedDate)) = today() - 5; table.(fieldnum(Table1, CreatedBy)) = "TEST";
table.insert(); ttscommit;
table.skipAosValidation(false);
Allerdings können die Systemfelder nur beim Erstellen eines neuen Datensatzes "von Hand" festgelegt werden.
Wie das Ändern von Werten der Systemfelder bei bereits bestehenden Datensätzen geht demonstriert die Klasse "BatchRun", Methode "runJob" und "finishJob". Kurz gesagt wird genau genommen der Datensatz nicht geändert, sondern es werden nur die Daten des bestehenden Datensatzes in den neuen Datensatz kopiert (mit newBuffer = oldBuffer.data()) und dann wie bereits beschrieben die Systemfelder mit eigenen Werten befüllt. Dann wird der bestehnde Datensatz gelöscht und der neue Datensatz in die Datenbank geschrieben.
Häufig werden Optionen (Ja/Nein-Fragen) in Microsoft Dynamics AX durch ein CheckBox Control und einem entsprechendem Feld einer Tabelle abgebildet.
Ein gutes Beispiel hierfür ist die Maske „Lagerparameter“, Reiter „Lagerungsdimensionen“. Hier kann eingestellt werden, welche Lagerungsdimension wo im System angezeigt werden soll. Zur Speicherung der gewählten Einstellungen wird die Tabelle „InventDimSetupGrid“ verwendet. Diese Tabelle enthält für jede Option jeweils ein Feld (abgeleitet vom Enum „NoYes“). Diese Art der Speicherung von Optionswerten ist sicherlich sehr leicht zu verstehen und auch sehr einfach zu erstellen. Da aber für jede Option ein Feld in der Tabelle angelegt werden muss, kann dies relativ zeitaufwendig sein.
Es besteht aber die Möglichkeit, Optionswerte in nur einem Feld zu speichern. Diese Art der Speicherung kann unter Umständen sogar als die elegantere angesehen werden, da z.B. für einen Datensatz weniger Speicher in der Datenbank benötigt wird.
Um dies zu realisieren, wird als erstes ein Feld vom Typ „int“ in der Tabelle benötigt. Dieses Feld dient als Datenspeicher für alle benötigten Optionswerte (Ja oder Nein). Jedes Bit dieses „int“ Feldes stellt genau einen Optionswert und somit eine Option dar. Um nun die gewählten Werte der Optionen speichern zu können, müssen diese mit Bit-Operationen (right / left shift, binary and, etc.) in das „int“ Feld geschrieben werden.
 Das Feld "bitMask" wird in dieser Darstellung als Datenspeicher der Optionswerte verwendet.
 Auf einer Maske werden alle Optionen als einzelne CheckBoxen bereit gestellt.
Da diese Art der Speicherung in Dynamics AX nicht besonders oft verwendet wird und Quellcode oft mehr sagt als (nur) ein langer Artikel, habe ich ein kleines „Tutorial“ erstellt, um die benötigten Schritte zu beschreiben. SharedProject_AKU_EnumControl_Frm.rar (2,03 KB)
Eine Erklärung aller Bit-Operatoren ist im Microsoft Dynamics AX Developer Center zu finden.
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").
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(); }
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)
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.
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
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.
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.
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)
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 LagerModulparametern eine Range (Verkauf, Einkauf, Lager) auf den Lagertyp hat man im groben die Standardquery (es fehlt noch die Lagerortverwaltung Tabelle) der Artikelmaske nachgebaut.
// Lager queryDS = queryDSInvent.addDataSource(tablenum(InventTableModule)); queryDS.relations(true); queryDS.joinMode(JoinMode::InnerJoin); queryRange = queryDs.addRange(fieldnum(InventTableModule,ModuleType)); queryRange.value(queryValue(ModuleInventPurchSales::Invent));
Prinzipiell kann man mit dieser Query auch die Query auf der Maske InventTable überladen. Diee Anzeige der Artikel klappt weiterhin wunderbar nur die Referenzen auf die zusätzlichen FormDataSources geht hierbei verloren. Dadurch funktioniert die Anzeige und Bearbeitung der Daten aus diesen FormDataSources dann nicht mehr.
Spasseshalber aber noch der Quellcode, der auch die Query der Form überlädt. Job_QueryEinfachFormRun.xpo (3,05 KB)
Viele Wege führen nach Rom, so auch in Microsoft Dynamics AX beim Anpassen der Masken (Forms).
Eine Form direkt anzupassen kann auf den ersten Blick immer ein einfachere Weg sein, nur sollte man bedenken das es hier bei einem Update mit die meißten Schwierigkeiten bzw. die meißte Arbeit geben kann. Dabei kann man fast jede Anpassung an der Maske auch über X++ steuern.
Hier ein paar einfache Beispiele dafür:
Manipulation der Datasource.
FormRun fr; Args args = new Args(); FormDataSource fds; ; // Form Name InventTable args.name(formstr(InventTable)); fr = new FormRun(args); // init der Form fr.init(); // Datasource der Form fds = fr.dataSource(1); // Neue Range auf der Datasource erzeugen fds.query().dataSourceTable(tablenuM(InventTable)).addRange(fieldnum(InventTable, itemID)).value(“Wert”); fr.run(); fr.wait();
In dem o.g. Beispiel wird eine neue Range auf der Datasource „InventTable“ erzeugt, ohne einen direkten Eingriff auf der Form zu machen.
Weitere Möglichkeiten der Datasource Manipulation
// Datasource darf nicht editierbar sein fds = fr.dataSource(1); fds.allowEdit(False);
Prinzipiell kann über X++ alles manipuliert werden, was auch direkt in der Form manipuliert werden kann.
Der Zugriff auf einzelne Felder der Datasource erfolgt über die Methode object(), es muss nur noch die Kennung des Objektes übergeben (Feldnummer) um Zugriff zu erhalten.
// Artikelnummer ausblenden fds = fr.dataSource(1); fds.object(fieldnum(InventTable, ItemID)).visible(false);
Man erhält den vollen Zugriff auf die Eigenschaften und kann nach belieben die Objekte verändern. Das einzige was zusätzlich noch gemacht werden muss, ist den Aufruf der Maske zu verändern. So kann ein neues MenuItem erstellt werden, welches das Ursprüngliche MenuItem ersetzt oder es wird einfach das MenuItem verändert. So wird anstelle der Maske eine Klasse aufgerufen, die die Manipulation an der Maske vornimmt. Performanceeinbußen habe ich noch nicht oder nur im geringen Maße feststellen können.
Die einzigen wirklichen Probleme die ich bei solchen Formanpassungen hatte, war das Einfügen einer neuen Formdatasource. Man kann die FormDatasource zur Laufzeit in die Form einfügen, leider muss die Form aber geschlossen und wieder neu aufgerufen werden um Zugriff auf die Formdatasource zu erhalten und um diese dann im Design der Form nutzen zu können. Bei manchen Forms kam es beim Neuaufruf der Form zu sehr seltsamen Verhalten, wie zum Beispiel, das Formcontrols die automatisch Deklariert wurden (Autodeclaration = Yes) ihre Wertigkeit verlieren. So wurde bespielsweise aus einer FormStringControl eine FormDataSource. Hier ist also bei der Anwendung Vorsicht geboten.
Anbei noch ein Job der die Kreditorenmaske manipuliert. Es wird hier eine neue Datasource (Bestellungen) angefügt und in einem neuen Grid angezeigt. Seltsamerweiße klappt dieses Vorgehen mit der Kreditormaske sehr gut, bei den Debitoren bekam ich nur o.g. Fehlermeldungen bei Aufruf.
Der ursprünglich Link scheint nicht zu funktionieren. Hier nochmal ein neuer Versuch
Job_MaskeKreditorBestellung.xpo (2,52 KB)Und wenn das auch nicht funktionieren sollte, nochmal als Text...
// Changed on 15 Mär 2007 at 21:14:18 by jinx (starside.eu) // Manipulation des Aufrufes der Maske Kreditoren static void MaskeKreditorBestellung(Args _args) { FormRun fr; Args args = new Args(); FormRun neuformRun; FormBuildDataSource formBuildDataSource; FormGridControl fgc; FormGroupControl fGroupCtrl; ; // Form Name VendTable -> Kreditoren args.name(formstr(VendTable)); fr = new FormRun(args);
// Neue DataSource einfügen formBuildDataSource = fr.form().addDataSource("PurchTable"); // Tabelle der Datasource zuordnen formBuildDataSource.table(tablenum(PurchTable)); // Datasource mit der Tabelle "VendTable" verknüpfen // Wichtig: Unbedingt den Namen der Datasource übergeben! formBuildDataSource.joinSource(fr.form().dataSource(1).name()); // Art der Verknüpfung festlegen formBuildDataSource.linkType(1);
formBuildDataSource.allowCreate(false); formBuildDataSource.allowDelete(false); formBuildDataSource.allowEdit(false);
// Neue Gruppe Erzeugen fGroupCtrl = fr.form().design().addControl(FormControlType::Group,"PurchOderGroup"); // Die Neue Datasource der neuen Gruppe zuordnen fGroupCtrl.dataSource(formBuildDataSource.id()); // Neues Grid erzeugen fgc = fGroupCtrl.addControl(FormControlType::Grid,"PurchOderGrid"); // Die Neue Datasource dem neuen Grid zuordnen fgc.dataSource(formBuildDataSource.id()); // Feld "Bestellnummer" ins Grid einfügen fgc.addDataField(formBuildDataSource.id(), fieldnum(PurchTable, PurchID)); // Feld "Kreditorennummer" ins Grid einfügen fgc.addDataField(formBuildDataSource.id(), fieldnum(PurchTable, OrderAccount));
// Die Form in der wir gerade die Datasource eingefügt haben, den Args übergeben args.object(fr.form());
// Neue FormRun aufgrund der Manipulierten Form erzeugen und aufrufen neuformRun = classFactory.formRunClass(args); neuformRun.init(); neuformRun.run(); neuformRun.wait(); }
Möchte man nach einem Upgrade von Microsoft Dynamics AX 4.0 auf Microsoft Dynamics AX 4.01 einen neuen Benutzer anlegen erhält man die Fehlermeldung "".
Dies läßt erst einmal auf ein Problem im Quellcode von Microsoft Dynamics AX schließen.
Grund für die Fehlermeldung ist aber kein Fehler im Quellcode, sondern ein fehlerhafter Datensatz in der Tabelle "SysPerimeterNetworkParms". In diesem Datensatz steht im Feld "PNType" ein ungültiger Wert.
Wird dieser Wert auf einen gültigen Wert (None) geändert, können auch wieder neue Benutzer im System angelegt werden.
Folgender Job kann zur Behebung des Problems verwendet werden: (Verwendung auf eigene Gefahr. Es wird keine Garantie oder Haftung für die Funktion und Richtigkeit des Quellcodes gegeben. )
static void CorrectAxaptaUserImportError(Args _args) { SysPerimeterNetworkParams p; DataArea a; ; while select a { changecompany(a.Id) { p = null; ttsbegin; while select forupdate p { p.PNType = PerimeterNetworkType::None; p.update(); } ttscommit; } } }
Im Microsoft PartnerSource ist ein Dokument erhältlich, welchen den Updateprozess beschreibt, der durchzuführen ist, wenn man ein Update von Microsoft Dynamics AX 4.0 auf Microsoft Dynamics AX 4.01 durchführen möchte. Alles in allem ist dieses Dokument eine sehr gute Informationsquelle, die fast alle Einzelheiten, die bei dem Update zu beachten sind, erläutert. Bedauerlich ist nur, dass eine sehr wichtige "Kleinigkeit" nicht in diesem Dokument erwähnt wird. Hält man sich strickt an die Dokumentation, so wird man leider feststellen, dass sich der AOS nach der Installation von Microsoft Dynamics AX 4.01 nicht mehr starten lässt. Es wird zwar ein Eintrag im EventLog erzeugt, dieser ist aber nicht besondern hilfreich (Error 110). Grund für diese Fehlermeldung sind die beiden StoredProzedures die ab Microsoft Dynamics AX Version 4.0 in jeder Microsoft Dynamics AX Datenbank vorhanden sein müssen. Da die Datenbank an sich nicht "geupdatet" wird, werden diese SP`s ebenfalls nicht geändert und bleiben damit auf dem Stand von Microsoft Dynamics AX 4.0. Und leider scheint Microsoft Dynamics AX 4.01 genau mit diesen SP`s nicht zusammen arbeiten zu können. Die Lösung des Problems gestaltet sich zum Glück recht einfach: Die SP's müssen einfach nur gegen die beiden SP's einer Microsoft Dynamics AX 4.01 Datenbank ausgetauscht werden und schon funktioniert der AOS wie erwartet.
Wenn die MSDB Datenbank eines SQL Servers einen Datenbankfehler meldet und man keine funktionierende / fehlerfreie Sicherung hat, stellt dies meist ein größeres Problem dar, da diese nicht mit DBCC CHECKDB repariert werden kann.
Abhilfe schafft meist nur das neu Erstellen der gesamten MSDB Datenbank.
(Leider gehen hierbei unter anderem die eingerichteten Sicherungsjobs verloren)
Die MSDB kann wie folgt beschrieben neu erstellt werden (SQL Server 2000):
- Im SQL Server Enterprise Manager die Eigenschaften des Datenbankservers öffnen.
(Rechtsklick auf den Datenbankserver -> Eigenschaften)
- Auf dem Reiter „Allgemein“ auf den Button „Startparameter“ klicken.
- Den Parameter "-T3608" hinzufügen.
- Den Datenbankserver stoppen und neu starten.
- Überprüfen, dass der SQL Server Agent gestoppt ist.
- Die MSDB Datenbank auswählen und über „Extras -> SQL Query Analyzer“ den QueryAnalyzer starten.
- Die MSDB Datenbank mit folgendem Skript abhängen
use master go sp_detach_db ‚msdb’ go
- Die defekte MSDB Datenbank (msdbdata.mdf, msdblog.ldf) löschen oder umbenennen (auf Fileebene).
- Das "instmsdb.sql" Skript mit dem QueryAnalyzer ausführen.
(Liegt unter: …\Microsoft SQL Server\MSSQL\Install)
- Im SQL Server Enterprise Manager wieder den Startparameter "-T3608" entfernen.
- Den Datenbankserver stoppen und neu starten.
Jetzt sollte wieder eine funktionierende und fehlerfreie MSDB Datenbank vorliegen.
Bei einem SQL Server 2005 sollte dies genau so funktionieren.
Ein weiteres Problem beim Update auf die neue Version Microsoft Dynamics AX 4.0 kann der Name des Unternehmenskontos sein. Wird ein Unternehmenskonto verwendet, dass in seinem Namen ein „&“ enthält, z.B. „A&B“, werden einige Prozesse beim „Datenaktualisierung nachsynchronisieren“ mit einem Fehler abgebrochen. Im Ereignislog ist dann ein Eintrag des Dynamics AX 4.0 Servers zu finden, der wie folgt lautet (Ausschnitt): „…[Microsoft][ODBC SQL Server Driver][SQL Server]Falsche Syntax in der Nähe von '&'.. The SQL statement was…“ Daraus wird ersichtlich, das dass „&“ Zeichen als SQL Statement erkannt wird und die SQL Anweisung so fehlerhaft interpretiert wird. Dieses Problem kann nur gelöst werden, indem man in dem zu updatenden Microsoft Dynamics AX 3.0 ein neues Unternehmenskonto erstellt das kein „&“ Zeichen beinhaltet (Am einfachsten geht dies mit der Dublizierfunktion der Unternehmenskonten).
Für die Planung und Durchführung eines Updates stehen folgende Informationsquellen zur Verfügung:
- Microsoft Dynamics AX Upgrade Tools Guide (PDF Dokument, Erhältlich im PartnerSource)
- Microsoft Dynamics AX 4.0 Implementation Guide (CHM File, Dynamics AX 4.0 Dokumentation)
- Inside Microsoft Dynamics AX 4.0 (Buch, Microsoft Press)
Diese enthalten eine gute Beschreibung der einzelnen Schritte die erforderlich sind, um von der Version 3.0 auf die Version 4.0 von Microsoft Dynamics AX zu updaten.
Allerdings können noch einige Probleme bei dem Updateprozess entstehen, die leider nicht Besprochen werden und für die auch in den bekannten Newsgroup, Blogs und Communitys noch keine Lösungsvorschläge gibt.
So sollte ein Upgrade immer in dem Layer durchgeführt werden, in dem die Anpassungen durchgeführt wurden (z.B. CUS oder VAR). Dies hat zur Folge das man u.U. die Anwendung mehrfach kompilieren muss, da man erst nach dem einlesen der Lizenzdatei zugriff auf diese Layer erhält.
Weiterhin kann man beim Durchführen der Synchronisierung folgenden Fehler erhalten: "Cannot execute a data definition language command on (). The SQL database has issued an error."
Ursache hierfür kann sein, dass der ConfigurationKey "CSESpain" ist in dem Microsoft Dynamics AX 3.0 System, welches geupdatet werden soll, nicht angeschaltet ist/war. Dadurch sind drei Felder ("Action", "CustVendParameter", "CustVendAccount") der Tabelle "SalesPurchaseCycle" deaktiviert, die allerdings den eindeutigen Index "SalesPurchaseCycle" bilden. Dadurch werden Datensätze in die Tabelle geschrieben, die nicht dem eindeutigen Index entsprechen. Beim Update auf Microsoft Dynamics AX 4.0 wird dieser Index überprüft bzw. neu geschrieben und das Synchronisieren wird mit einem Fehler abgebrochen, da die Datensätze der Tabelle nicht eindeutig sind.
Das Problem kann gelöst werden, indem man auf der Tabelle "SalesPurchaseCycle" den eindeutigen Index "SalesPurchaseCycleIdx" auf "Enabled = NO" setzt. Anschließend sollten alle Datensätze in dieser Tabelle in allen Unternehmen gelöscht werden und der Index "SalesPurchaseCycleIdx" wieder auf "Enabled = YES" gesetzt werden.
Die Deaktivierung des ConfigurationKey "CSESpain" reicht leider nicht aus, da es dann zu Problemen beim "Datenaktualiserung nachsynchronisieren" kommen kann, da dort die einzelnen Datensätze überprüft werden (5 Prozesse werden deswegen mit einem Fehler beendet).
Weiterhin sei noch angemerkt, dass alle Anpassungen in Bereich der "Forms" zu erheblichen Problemen beim Codeupgrade führen können. Viele Formelemente wurden in der neuen Version 4.0 unbenannt, was dazu führen kann, dass eine einzige Anpassung (z.B. Änderung nur einer Formproperty) mehrere hundert Fehler erzeugen kann.
Bsp.: Änderung einer Property der Form "CompanyInfo" im upzudatenden Microsoft Dynamics AX 3.0 System führte zu 126 Fehlern im Microsoft Dynamics AX 4.0 System.
Deswegen sollte man vor einem Update genauestens überlegen/überprüfen, ob es überhaupt Sinn macht die getätigten Anpassungen mit zur neuen Version (4.0) zu migrieren.
Oft können getätigte Anpassungen durch Erweiterungen im Standard abgelöst werden, die eine ähnliche Funktionalität bieten. In machen Fällen ist es unter Berücksichtigung der benötigten Zeit sogar Sinnvoller, die Anpassung erneut im Microsoft Dynamics AX 4.0 System vorzunehmen.
Eine Migration der Anpassungen sollte nur in Erwägung gezogen werden, wenn keine andere Lösung gefunden werden kann.
Oft wird ein Export von Dynamics AX Daten in ein Exceldokument benötigt. Z.B. für einfache Auswertungen oder für Datenimporte in andere Systeme.
Hier ein kurzes Beispiel, wie man aus Dynamics AX ein neues Exceldokument per Code erstellen kann.
static void CreateExcelDokument(Args _args) { SysExcelApplication xlsApplication; SysExcelWorkBooks xlsWorkBookCollection; SysExcelWorkBook xlsWorkBook; SysExcelWorkSheets xlsWorkSheetCollection; SysExcelWorkSheet xlsWorkSheet; SysExcelRange xlsRange; CustTable custTable; int row = 1; str fileName; ; // Name des Exceldokuments. fileName = "C:\\test.xsl";
// Excel initalisieren und öffnen. xlsApplication = SysExcelApplication::construct(); xlsApplication.visible(true);
// Neues Excel Worksheet erzeugen. xlsWorkBookCollection = xlsApplication.workbooks(); xlsWorkBook = xlsWorkBookCollection.add(); xlsWorkSheetCollection = xlsWorkBook.worksheets(); xlsWorkSheet = xlsWorkSheetCollection.itemFromNum(1);
// Zellenüberschriften in das Worksheet schreiben. xlsWorkSheet.cells().item(row,1).value('Account Num'); xlsWorkSheet.cells().item(row,2).value('Name');
row++;
// Excel Worksheet mit Daten füllen (Excel-Zellen füllen). while select custTable { xlsWorkSheet.cells().item(row,1).value(custTable.AccountNum); xlsWorkSheet.cells().item(row,2).value(custTable.Name); row++; }
// Prüfen ob das Dokument schon existiert. if(WinApi::fileExists(fileName)) { WinApi::deleteFile(fileName); }
// Excel Dokument speichern. xlsWorkbook.saveAs(fileName);
// Excel schließen. xlsApplication.quit(); xlsApplication.finalize(); }
Um den Text, der in der Titelleiste des Dynamics AX Clients angezeigt wird, zu ändern muss man folgendes machen:
- Auf dem Clientrechner in das Axapta Client Installationsverzeichnis wechseln.
- Im Unterverzeichnis "Client\Bin" die Datei "Axsys$$.KTD" mit einem Texteditor öffnen.
($$ steht hierbei für das entsprechde Länderkürzel. Z.B. de)
- Das Label "#1076" wie gewünscht anpassen.
Nach einem (Neu)Start des Dynamics AX Clients wird nun der in dem Label eingetragene Text in der Titelleiste des Dynamics AX Clients angezeigt.
Es ist nicht gerade einfach, einen Screenshot einer einzelnen Maske (z.B. Debitoren) von Dynamics Ax zu erstellen.
Verwendet man die Windows-Boardmittel, ist in dem erzeugten Screenshot immer der gesamte Dynamics AX Bildschirm enthalten. Da Dynamics AX eine MDI Anwendung ist, erkennt Windows immer nur den gesamten Dynamics AX Bildschirm und nicht wie gewollt, die in diesem "Hauptbildschirm" aktive Dynamics AX Maske (z.B. Debitoren).
Abhilfe kann hier mit zahlreichen zusätzlichen Tools geschaffen werden, die es ermöglichen, nur einen bestimmten Bildschirmausschnitt in den Screenshot zu übernehmen (z.B. Hardcopy oder Paintshop Pro).
Aber geht das nicht doch etwas einfacher?
Klare Aussage: Ja es geht einfacher! Dynamics AX ist in der Lage, automatisch von einer oder mehreren Masken gleichzeitig (natürlich einzeln und nacheinander) Screenshots zu erzeugen.
Der AOT von Dynamics AX beinhaltet unter dem Zweig "Forms" die Form "sysDocCaptureForm" sowie im Zweig "Menuitems/Display" das gleichnamige MenuItem. Öffnet man eines dieser beiden Objekte, öffnet sich die Maske "Erfassungsserie", die stark an ein übliches Journal erinnert. Hier kann eine Art "Erfassungsjournal für Screenshots" angelegt werden. Es können allgemeine Informationen wie Ablageverzeichnis für die Screenshots im Dateisystem oder deren Format bestimmt werden. Über den Button "Schritte" kommt man zu der Maske "Schritte erfassen", auf der die einzelnen Masken bestimmt werden können, von denen automatisch Scrennshots erstellt werden sollen. Die Screenshots können schließlich über den Button "Erfassen" auf der Maske "Erfassungsserie" erstellt werden. Hilfreich für die Erstellung automatischer Screenshots ist die Funktion, von jedem Reiter einer Maske einen neuen Screenshot zu erstellen. Dies kann mit dem Haken "Erfassen" auf der Maske "Schritte erfassen" eingestellt werden.
Nach meiner Meinung ein nettes kleines Feature, was die Arbeit durchaus erleichtern kann. Schade ist nur, dass der Dynamics AX Standard keine "direkte" Aufrufmöglichkeit hierfür bietet.
Es gilt aber noch anzumerken, dass man für die Verwendung dieses Tools die Berechtigung auf dem SecurityKey "SysDevelopmentMorphX" braucht.
Dynamics Ax erlaubt das mehrmalige Anmelden unter einer Benutzerkennung. So kann ein Benutzer eine beliebige Anzahl an Sitzungen mit seiner Benutzerkennung öffnen.
Möchte man aber die mögliche Anzahl an Sitzungen unter einer Benutzerkennung begrenzen, ist dafür eine Anpassung der Info Klasse notwendig.
Fred Shen beschreibt in seinem Blog, wie diese Anpassung auszusehen hat.
Oft muss von Dynamics Ax aus z.B. auf eine Datei zugegriffen werden. Hierbei muss man beachten, dass die Pfadangabe zu der Datei eine Escape-Sequenz darstellt/enthält.
Bsp.: str file = "C:\\test\\test.csv";
Es existiert aber eine, soweit mir bekannt allerdings undokumentierte, Möglichkeit den Dateipfad auch ohne Escapezeichen anzugeben (raw string).
Bsp.: str file = @"C:\Test\test.csv";
Zusätzlich kann man bei Verwendung von @"" auch Zeilenumbrüche innerhalb des String zu verwenden/schreiben.
Bsp.: str text = @"Das ist ein Text mit mehr als einer Zeile";
|