Das Speichern eines Datensatzes in einem Tabellen-Feld ist an sich kein Problem. Aber leider ist dies eine der nicht so oft verwendeten Sachen in der Dynamics AX Entwicklung und wird deshalb immer mal gerne wieder vergessen. 
Wir haben also eine Tabelle mit einem Feld, welches einen Container als Datentyp hat. Weiterhin haben wir einen Datensatz welchen wir in diesem Feld speichern wollen. Wie bekommen wir nun den Datensatz in dem Feld gespeichert? Ein direkte Zuweisung für hierbei leider zu einem Fehler.
Für diese Operationen stellt der Dynamics AX Standard folgende Funktionen zur Verfügung:
- buf2con(common buffer)
- con2buf(container c, common buffer)
Schreiben des Datensatzes in das Tabellenfeld
Die Funktion “buf2Con” wandelt eine Datensatz in einen Container, welcher entsprechend in dem Tabellen-Feld gespeichert werden kann.
Lesen des Datensatzes aus dem Tabellenfeld
Die Funktion “con2buf” wandelt einen Container (genauer dessen Inhalt) wieder zu einem Datensatz um.
Allerdings sollte bei der Verwendung dieser beiden Funktionen immer bedacht werden, dass Änderungen an Tabellen, von denen Datensätze auf diese Art gespeichert werden zu Problemen führen können.
Mit Microsoft Dynamics AX 2009 wird für die Erstellung neuer dokumentenbasierter AIF-Services ein Tool/Wizard mitgeliefert, der so genannte “Assistent für AIF-Dokumentendienste”, welcher den Entwickler bei der Erstellung eines neuen AIF-Services unterstützt.
Wie dieser Wizard verwendet wird, kann zum Beispiel in der MSDN nachgelesen werden:
How to: Create a Service Using the AIF Document Service Wizard Walkthrough: Creating a Service Using the AIF Document Service Wizard
In den meisten Fällen funktioniert dieser Wizard auch sehr gut und bietet somit eine echte Entwicklungsunterstützung. Es gibt allerdings auch Fälle in denen der Wizard auf einen Fehler läuft, welche auf den ersten Blick sehr schwer zu verstehen sind.
Ein Beispiel hierfür sind Tabellen, die ein Feld beinhalten welches als Datentyp den Extended Data Type “InventDimId” definiert hat. Wird beim Erstellen des Query-Objekts für den AIF-Service nicht darauf geachtet, dass auch die Tabelle InventDim mit entsprechender Relation in die Query aufgenommen wird, erzeugt der Wizard eine Fehlermeldung.
Wird folgende Query für den neuen AIF-Service angelegt und diese für die Generierung des AIF-Service verwendet
erzeugt der Assistent für AIF-Dokumentendienste folgenden Fehler:
Um diesem Problem aus dem Weg zu gehen, muss die Tabelle “InventDim” mit in die Query aufgenommen werden, selbst wenn diese für den Dokumentkontext nicht von Bedeutung ist.
Anmerkung: An dieser Stelle soll auch auf den Punkt hingewiesen werden, dass der Assistent für AIF-Dokumentendienste keinen vollständig funktionsfähigen AIF-Service erstellt. Es wird eher mehr das Grundgerüst des AIF-Service angelegt und der Entwickler muss nach Ausführung des Wizards unter Anderem noch die benötigte Business-Logik ergänzen um die gewünschte Funktionalität bereit stellen zu können.
Ein großer Vorteil des Application Integration Frameworks (AIF) gegenüber “selbst geschriebene” Schnittstellen ist es, dass man sich über Dinge wie Datentyp-Mapping keine Gedanken machen muss. Das Application Integration Framework verfügt über die entsprechende Logik, um alle Dynamic AX Datentypen automatisch in den jeweils gültigen XSD-Datentyp zu “mappen” (oder umgekehrt).
Zeiten, in denen sich der Entwickler zum Beispiel Gedanken machen musste, wie viele Nachkommastellen eine Zahl haben darf, oder welches Zeichen als Dezimaltrennzeichen verwendet werden muss, sind somit vorbei. Da alle Daten die aus Dynamics AX exportiert oder nach Dynamics AX importiert werden, in einem XSD-Schema konformen XML-Dokument “transportiert” werden, und das AIF entsprechendes Mapping bereits stellt, geschieht das Datentyp-Mapping automatisch.
Allerdings kann auf Seiten der Anwendung, welche über das Application Integration Framework (AIF) angebunden werden soll, ein wenig “Verwirrung” entstehen. Durch die von Programmiersprache zu Programmiersprache durchaus unterschiedlichen Datentypen kann es vorkommen, dass Dynamics AX Datentypen nicht in dem erwarteten Datentyp der anderen Programmiersprache erscheinen. Dies ist allerding kein “wirkliches” Problem des Application Integration Frameworks (AIF), sondern eher eine Frage, welche Datentypen eine Programmiersprache bereit stellt und wie diese in XSD-Datentypen “gemappt” werden.
Ein gutes Beispiel hierfür sind die Dynamics AX Datentypen “Date”, “Time” und “DateTime” (inklusive aller von diesen Basisdatentypen abgeleiteten EDT’s).
Ohne genauere Betrachtung liegt die Annahme nahe, dass ein DateTime Datentyp von Dynamics AX in einen DateTime Datentyp von z.B. C# “gemappt” wird. Dies ist allerdings nicht richtig. Da nicht direkt zwischen Dynamics AX Datentyp und C# Datentyp gemappt wird, sondern immer von/zu einem XSD-Datentyp gemappt wird, wird in C# eine neue Klasse hierfür erzeugt.
Ein wenig schwieriger wird es bei den beiden Dynamics AX Datentypen “Date” und “Time”. Für diese Datentypen wird z.B. in C# kein direkt vergleichbarer Datentyp bereit gestellt. Diese Datentypen werden jeweils als C# DateTime Datentypen gemappt.
Das Mapping der Datentypen geschieht wie folgt:
|
Dynamics AX |
XSD Schema |
.NET (C#) |
|
Date |
xs:date |
System.DateTime |
|
Time |
xs:time |
System.DateTime |
|
DateTime |
xs:dateTime |
new class i.e. “AxdType_DateTime” |
Da die beiden Dynamics AX Datentypen “Date” und “Time” in den C# Datentyp “DateTime” gemappt werden, kann an dieser Stelle leider ein kleines Problem entstehen. In C# ist nun leider nicht mehr zu erkennen, um was für einen Dynamics AX Datentyp es sich z.B. bei einem Feld handelt, und ob nun ein Datum oder eine Zeit in diesem enthalten ist.
Oftmals entsteht diese Problem dadurch nicht, dass der jeweilige Business-Kontext die Datentypverwendung entsprechend einschränkt und es somit teilweise egal ist ob nun in ein Dynamics AX Date oder Time gemappt wird. Ist es aber erforderlich zu wissen, um ob ein Feld nun den Dynamics AX Datentyp Date oder Time hat, kann der generierte Code der Proxyklasse Aufschluss geben (oder das XSD-Schema).
Durch die Angabe eines Serialisierungs-Attributes wird bestimmt, welcher “Teil” des DateTime Datentyps verwendet wird. Für ein Feld, welches in einen Dynamics AX Date Datentyp gemappt wird, wird nur der “Datumsteil” in das XML-Dokument serialisiert. Entsprechendes geschieht für einen Dynamics AX Time Datentyp.
Mapping eines C# Datetime Datentyps in einen Dynamics AX Date Datentyp (generierter Code der Proxyklasse):
1: [System.Xml.Serialization.XmlElementAttribute(DataType="date", IsNullable=true, Order=54)] 2: public System.Nullable<System.DateTime> MyDateField { 3: get { 4: return this.myDateFieldField; 5: } 6: set { 7: this.myDateFieldField = value; 8: this.RaisePropertyChanged("MyDateField"); 9: } 10: }
Mapping eines C# Datetime Datentyps in einen Dynamics AX Time Datentyp (generierter Code der Proxyklasse):
1: [System.Xml.Serialization.XmlElementAttribute(DataType="time", IsNullable=true, Order=56)] 2: public System.Nullable<System.DateTime> MyTimeField { 3: get { 4: return this.myTimeFieldField; 5: } 6: set { 7: this.myTimeFieldField = value; 8: this.RaisePropertyChanged("MyTimeField"); 9: } 10: }
Wie durch den generierten Code der Proxyklasse ersichtlich wird, wird nur der jeweils benötigte “Teil” eines C# Datetime Datentyps serialisiert/deserialisiert und somit verwendet.
Für einen Dynamics AX DateTime Datentyp wird bei Erstellung des Proxys eine neue Klasse generiert. Somit kann der Dynamics AX Datentyp hierbei immer eindeutig identifiziert werden.
1: [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "2.0.50727.4016")] 2: [System.SerializableAttribute()] 3: [System.Diagnostics.DebuggerStepThroughAttribute()] 4: [System.ComponentModel.DesignerCategoryAttribute("code")] 5: [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/dynamics/2008/01/documents/Customer")] 6: public partial class AxdType_DateTime : object, System.ComponentModel.INotifyPropertyChanged { 7: 8: private System.DateTime localDateTimeField; 9: 10: private bool localDateTimeFieldSpecified; 11: 12: private AxdEnum_Timezone timezoneField; 13: 14: private bool timezoneFieldSpecified; 15: 16: private System.DateTime valueField; 17: 18: [System.Xml.Serialization.XmlAttributeAttribute()] 19: public System.DateTime localDateTime ... 20: 21: [System.Xml.Serialization.XmlIgnoreAttribute()] 22: public bool localDateTimeSpecified... 23: 24: [System.Xml.Serialization.XmlAttributeAttribute()] 25: public AxdEnum_Timezone timezone... 26: 27: [System.Xml.Serialization.XmlIgnoreAttribute()] 28: public bool timezoneSpecified... 29: 30: [System.Xml.Serialization.XmlTextAttribute()] 31: public System.DateTime Value { 32: 33: public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; 34: 35: protected void RaisePropertyChanged(string propertyName)... 36: }
Dieses Verhalten ist nicht nur mit C# zu beobachten. Auch JAVA oder andere Programmiersprachen verhalten sich ähnlich und muss entsprechend berücksichtigt werden.
Macros werden innerhalb von Dynamics AX z.B. für die Best-Practice konforme Verwendung von festen Zeichenfolgen innerhalb des X++ Quellcodes verwendet. An vielen Stellen im System finden sich Quellcodezeilen wie diese: #define.MySimpleMacro('The string value')
Dies ist die am meist verwendete Art der Macrodefinition innerhalb von Dynamics AX. So weit, so gut.
Es gibt allerdings Konstellationen von “Werten”, welche bei dieser Art der Macrodefinition zu einer Fehlermeldung beim speichern führen. Es bei dieser Art der Macrodefinition z.B. nicht möglich, eine schließende Klammer als Macrowert zu definieren. Dies ist auch so im MSDN dokumentiert: http://msdn.microsoft.com/en-us/library/cc197110.aspx
Gerade bei der Verwendung von Regular Expressions (Regex) kann dies zu regelmäßiger Verwirrung führen, da man gerne die weiteren Macrodefinitions-Möglichkeiten vergisst und oder diese nicht so präsent sind.
Die Syntax #define.Macroname(Wert)
sollte eigentlich nur verwendet werden um (einfache) Konstanten innerhalb des Quellcodes zu definieren. Für alle anderen Fälle und wenn ein Macro mit mehr als einer Zeile benötigt wird, sollte folgende Syntax zu Definition eines Macros verwendet werden: #localmacro.AnExample
// Some statements or text
#endmacro
Die Verwendung von #localmacro anstelle eines "(einfachen) #define erlaubt es nun auch mit schließenden Klammern als Macrowert zu arbeiten oder sogar ganze SQL oder X++ Codeblöcke zu verwenden. Eine genauere Beschreibung von Dynamics AX Macros (deren Möglichkeiten und Einsatzgebiete) ist im MSDN dokumentiert:
Macros in X++
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.
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.
Wie schon die erste Auflage des Buches „Inside Dynamics AX“ ist dieses Buch eine sehr gute Ergänzung zu den von Microsoft angebotenen Schulungsunterlagen (Development 1-4).
Angefangen bei der Architektur, der Entwicklungsumgebung und –Tools, bis hin zu Code Upgrades beschreibt dieses Buch alle Themen die für einen AX Entwickler von Bedeutung sind.
Nicht nur alle neuen Features von Dynamics AX 2009, z.B. Dynamics AX Reporting Services oder Workflows, sondern auch ältere Features wie z.B. das Application Integration Framework (AIF), werden wesentlich detaillierter beschrieben als an anderen Stellen.
Leider gibt es auch Bereiche, die nicht so detailliert besprochen werden bzw. wo einige Fragen nicht gänzlich beantwortet werden. Ein Beispiel hierfür ist die .NET Integration. Zwar wird der Business Connector ausreichend beschrieben, aber das Thema CLR-Interoperability wird leider nur sehr knapp behandelt.
Einige Kapitel wurden im Vergleich zu der ersten Auflage des Buches gänzlich überarbeitet. Beispielhaft sei das Kapitel über Form Customizations genannt, welches komplett neu geschrieben wurde.
Leider hat dies auch zur Folge, dass einige sehr gut Beschriebene Themen, wie Beispielweise das dynamische Anpassungen von Masken mit X++, jetzt nicht mehr behandelt werden.
Was dieses Buch aber nicht beschreibt oder behandelt, sind die Klassen, Tabellen, API‘s, etc. des Microsoft Dynamics AX Standards. Dies würde allerdings auch den Rahmen des Buches mehr als sprengen.
In der Gesamtbetrachtung ist die neue Auflage von Inside Microsoft Dynamics AX eins der besten technischen Bücher über Microsoft Dynamics AX. Kein Buch geht soweit in die Tiefe wie dieses. Egal ob Anfänger oder erfahrener Entwickler, für jeden ist etwas dabei.
Auch wer schon die erste Auflage von Inside Microsoft Dynamics AX gelesen hat, wird viele neue Themen finden.
Wird die Eigenschaft(Property) “AllowEditOnCreate” eines Tabellenfeldes auf den Wert “No” gesetzt, ist es nicht möglich, Werte für dieses Tabellenfeld über das AIF (Application Integration Framework) zu schreiben (Insert-Operation).
Alle Tabellenfelder, welche diese Eigenschaft auf “No” gesetzt haben, werden durch das AIF automatisch auf deren Default-Wert gesetzt und jegliche Wert der AIF Nachricht werden ignoriert. Dies hat zur Folge, dass wenn das Tabellenfeld kein Enum ist, das Tabellenfeld immer leer ist.
Da dieser Automatismus schon vor Ausführung der AX<Table> Klasse greift, der Wert also schon beim Ausführen der entsprechenden Parm-Methode “leer” ist, kann dieses Verhalten ohne Änderung der AIF-Basis Klassen nicht geändert 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);
Die neue oder zweite Auflage von "Inside Mircosoft Dynamics AX" ist vor wenigen Tage erschienen.
Inside Mircosoft Dynamics AX 2009
Wie auch schon die erste Auflage des Buches, welche auf der Version 4.0 von Microsoft Dynamics AX basierte, ist diese Buch in erster Linie für Entwickler gedacht. Die aktuelle Auflage umfasst gut 100 Seiten mehr als die erste Auflage und ist leider auch im Preis etwas teurer.
Weitere Informationen über den Inhalt können z.B. auf den Seiten von Amazon entnommen werden.
Es ist zu hoffen, dass sich diese Auflage auf gleichem Level wie die erste Auflage bewegt und somit zu einem "Must-Have" oder "Must-Read" für AX Entwickler wird.
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)
Leider scheint es ein Problem mit duplizierten Tabellen in Dynamics AX 2009 zu geben. Unter gewissen "Umständen" ist es möglich, Quelltext der in einem andern Layer (z.B. CUS oder BUS) geschrieben wurde, in den SYS Layer zu "verschieben".
Dieser Verhalten ist sehr unschön, da viele Entwickler, z.B. für Testzwecke, mal ein Objekt duplizieren, neuen Quelltext testen und anschließen diesen auf das orginale Objekt übernehmen oder verschieben. Leider taucht genau an dieser Stelle das Problem auf (es kann sein, dass dieses "Verschieben" den Quelltext in den SYS Layer schreibt).
Das genaue Verhalten ist in einem Video von "elranu" auf YouTube beschrieben (Link zum Video):
In den Newsgroups ist auch ein entsprechender Thread zu finden: Ax 2009 bug in Sys Layer
Wie in einem Artikel auf der Microsoft Dynamics AX Webseite zu lesen ist wird der "alte" COM Business Connector nicht mehr in zukünftigen Versionen von Dynamics AX enthalten sein. The COM Business Connector will be discontinued in future releases of Microsoft Dynamics AX
Bereits in der Version 2009 von Microsoft Dynamics AX wird der COM Business Connector vom "normalen" Setup nicht mehr angeboten. Dieser muss manuell, wie im Microsoft Dynamics AX Developer Center beschrieben, nachinstalliert werden. How to: Install COM Business Connector using Command-line Options
Somit ist es an der Zeit, bestehende Lösungen welche den COM Business Connector verwenden, auf den neueren .NET Business Connector zu portieren, um diese Lösungen auch in zukünftigen Versionen verwenden zu können.
Alle benötigten Informationen über die Verwendung des .NET Business Connectors können in der Library des Microsoft Dynamics AX Developer Centers gefunden werden .NET Business Connector Overview
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(..)").
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 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.
Microsoft Dynamics AX verwendet für eindeutige Kennungswerte (Id’s) die eingebauten Nummernkreise, für welche ein Feld vom Typ „String“ benötigt wird. Dies macht auch Sinn, da Nummernkreise in Dynamics AX oft ein oder mehrere alphanumerische Zeichen enthalten. Natürlich können auch rein nummerische Nummernkreise mit diesem „Framework“ erstellt werden.
Allerdings sind die Nummernkreise im Dynamics AX Standard nicht ganz optimal bei der Verwendung von einem rein nummerischen Nummernkreisen. Dies fängt z.B. schon beim Datentyp an, der für das ID-Feld der Tabelle verwendet werden muss. Bedingt dadurch, dass ein Feld vom Typ „String“ verwendet werden muss, belegt dieses Feld unnötig viel Speicher in der Datenbank. Weiterhin gestalten sich Sortierungen, Rechenoperationen, etc. erheblich schwieriger.
Diese Probleme können umgangen werden, wenn für das ID-Feld der Datentyp „Integer“ oder „Int64“ verwendet wird. Leider kann nun nicht mehr das Nummernkreis-Framework des Dynamics AX Standards verwendet werden, da dies den Datentyp „String“ für ein ID-Feld vorschreibt.
Es muss also ein eigenes, kleines Nummernkreis-Framework oder ein eigener Nummernkreis geschrieben werden, der die Verwendung des Datentyps „Integer“ für ID-Felder ermöglicht. Dies hört sich zuerst schwierig an, da Dinge wie fortlaufende Nummernvergabe oder die Wiederverwendung von freien Nummern (Löchern im Nummernkreis) berücksichtigt werden sollten.
Es ist aber ganz und gar nicht schwierig, eine eigene Nummernkreisfunktionalität zu erstellen. Das einzige was hierfür benötigt wird ist eine entsprechen aufgebaute Select-Abfrage.
Die nächste Nummer eines Nummernkreises ist immer die zuletzt vergebenen Nummer (höchste) + 1.
Aku_TestTable t1; ; select maxof(ID) from t1; return t1.ID + 1;
Etwas schwieriger wird es, wenn auch die frei gewordenen Nummern des Nummernkreises wieder vergeben/verwendet werden sollen. Dann muss immer die kleinste Id aus der Tabelle gesucht werden, für die es keinen Datensatz in der Tabelle gibt. Gibt es kein "Nummernloch", muss die nächst höchste Nummer vergeben werden.
Aku_TestTable t1; Aku_TestTable t2; ; select minof(ID) from t1 notexists join t2 where t2.ID == (t1.ID + 1); return t1.ID + 1;
Um diese ein wenig zu verdeutlichen, kann eine beispielhafte Implementierung eines eigenen Nummernkreises in diesem Demoprojekt "SharedProject_AKU_OwnIntNumberSeq" angesehen und runter geladen werden.
Wenn bei der Entwicklung mit Microsoft Dynamics AX 4.0 die Quellcodeverwaltung mittels Visual SourceSafe 2005 eingeschaltet wurde, besteht die Möglichkeit einzelne Versionen eines Objekts miteinander zu vergleichen.
Hierbei kann es aber bei einer "ungünstigen" Konfiguration des lokal Repository-Verzeichnisses sein, dass bei einem Vergleich von zwei Objektversionen die Fehlermeldung "Fehler: Fortsetzen nicht möglich" ausgegeben wird.
Diese Fehlermeldung wird immer erzeugt, wenn sich das lokale Repository-Verzeichnis und das Verzeichnis, in dem die temporären Internetdateien (Temporary Internet Files) gespeichert werden, nicht auf der gleichen Partition (Datenträger) befinden.
Beispiel:
Ordner der Temporary Internet Files = C:\Dokumente und Einstellungen\UserXY\Lokale Einstellungen\Temporary Internet Files Ordner des lokalen Repositories = D:\VSSRepository\Test
-> Die Fehlermeldung wird ausgegeben.
Ordner der Temporary Internet Files = C:\Dokumente und Einstellungen\UserXY\Lokale Einstellungen\Temporary Internet Files Ordner des lokalen Repositories = C:\VSSRepository\Test
-> Die Fehlermeldung wird nicht ausgegeben und der Versionvergleich funktioniert problemlos.
Dieses Problem wird durch ein Update für Visual SourceSafe 2005 behoben. Es empfiehlt sich, bei Verwendung der Quellcodeverwaltung mit Visual SourceSafe 2005 als VC-System, dieses Update einzuspielen.
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.
Im Microsoft Dynamics AX Developer Center wurde für Dynamics AX Entwickler eine neue Webcast Serie gestartet.
Auszug:
"On this page you will find videos designed for all Microsoft Dynamics AX developers, from the novice to the professional. New videos are added regularly, so check back often."
Derzeit ist nur ein Webcast über "Dynamics Links between parent and child Forms" erhältlich. Gilt zu hoffen, dass in der nächsten Zeit noch weitere nützliche Webcast folgen.
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").
Das Buchen von Bestellungen in Microsoft Dynamics AX geschieht über die Klasse „PurchFormLetter“ bzw. einer ihrer konkretisierten (abgeleiteten) Klassen. Jeder Buchungstyp (z.B. Bestätigung oder Rechnung) ist durch eine eigene Klasse abgebildet, welche von der Basisklasse „PurchFormLetter“ abgeleitet ist (siehe Abbildung).

Abbildung 1 - Klassenhierarchie der Klasse „PurchFormLetter“
Vergleicht man die Klassenhierarchie der „PurchFormLetter“ Klassen mit der Klassenhierarchie der „SalesFormLetter“ Klassen, so ist zu erkennen, dass auch das Buchen von Bestellungen vom Prinzip her genau so funktioniert wie das Buchen von Aufträgen (Vergleiche hierzu: Microsoft Dynamics AX API – Teil 3 „Buchen von Aufträgen“).
Deswegen sind auch für das Buchen von Bestellungen im Wesentlichen nur zwei Schritte notwendig.
- Über die Methode „construct“ der Klasse „PurchFormLetter“ ein dem Buchungstyp einsprechendes Objekt erzeugen.
- Über den Aufruf der Methode „update“ die Bestellung buchen.
Hierzu ein Beispiel (Buchen des Lieferscheins für eine Bestellung):
static void PurchPostPackingSlip(Args _args) { PurchFormLetter purchFormLetter; PurchTable purchTable; PurchId purchId; Num packingSlipId; ; //Angabe der Bestellung, für welche der Lieferschein gebucht werden soll. purchId = "00244_049"; purchTable = PurchTable::find(purchId);
//Bestimmen des Buchungstyps durch Angabe des DocumentStatus (Lieferschein). purchFormLetter = PurchFormLetter::construct(DocumentStatus::PackingSlip);
//Festlegen der externen Lieferscheinnummer. packingSlipId = "EXT-100155L";
//Buchen des Lieferscheins. purchFormLetter.update(purchTable, packingSlipId, SystemDateGet(), PurchUpdate::All, AccountOrder::Auto, NoYes::No, NoYes::No, NoYes::No, NoYes::No); }
Einziger Unterschied zu den Auftragsbuchen ist, dass bei der Buchung einer Bestellung die „externe“ Nummer des Belegs (Lieferscheinnummer, Rechnungsnummer, etc.) angegeben werden muss.
Analog zu den Auftragsbuchen, sind auch beim Buchen von Bestellungen umfangreichere oder etwas speziellere Buchungsszenarien möglich (Vergleiche hierzu: Microsoft Dynamics AX API – Teil 3 „Buchen von Aufträgen“).
Für ein Grid-Control kann über die Einstellung "MultiSelect" gesteuert werden, od dieses Control die Auswahl von mehr als einem Datensatz erlaubt.
Gültige Einstellungen sind: Yes - Es können mehrere Datensätze ausgewählt werden. No - Es kann immer nur ein Datensatz ausgewählt werden.
Auswahl eines Datensatzes
Auswahl mehrerer Datensätze
Zugriff auf die aktuelle Selektion (einer oder mehrere) erhält man wie folgt beschrieben:
Ist nur ein Datensatz markiert, bzw. soll mit einfacher Auswahl gearbeitet werden (MultiSelect = No), kann der ausgewählte Datensatz über den aktuellen DataSource-Cursor der Grid-Control DataSource ermittelt werden. Der DataSource-Cursor steht immer auf dem zu letzt ausgewählten Datensatz eines Grid-Control's.
Beispiel:
void clicked() { //CustTable ist die DataSource des Grid-Controls ; //Datenoperationen für den Datensatz ausführen. //Do something.... info(CustTable.AccountNum);
//Angezeigte Datensätze im Grid Control aktualisieren element.lockWindowUpdate(true); CustTable_ds.research(); element.lockWindowUpdate(false); }
Soll eine Mehrfachauswahl möglich sein, reicht der Zugriff auf den aktuellen DataSource-Cursor nicht mehr aus. Um alle ausgewählten Datensätze der DataSource zu erhalten, muss diese mit einer Schleife unter Verwendung der Methoden "getFirst" und "getNext" durchlaufen werden. Hilfreich hierbei ist die Methode "anyMarked", mit welcher ermittelt werden kann ob mehrere Datensätze ausgewählt sind oder nicht.
Auch hierfür ein Beispiel:
void clicked() { CustTable selectedCustTable; Common currentRecord; ; if (CustTable_ds.anyMarked()) //Es sind meherer Datensätze selektiert. { //Ersten selektierten Datensatz ermitteln. selectedCustTable = CustTable_ds.getFirst(1);
while(selectedCustTable) { //Datenoperationen für den Datensatz ausführn. //Do something.... info(selectedCustTable.AccountNum);
//Nächsten selektieren Datensatz ermitteln. selectedCustTable = CustTable_ds.getNext(); } } else //Nur ein Datensatz ist selektiert. { //Selektierten Datensatz ermitteln. currentRecord = CustTable_ds.cursor().data(); selectedCustTable = CustTable_ds.cursor();
//Datenoperationen für den Datensatz ausführen. //Do something.... info(selectedCustTable.AccountNum); }
//Angezeigte Datensätze im Grid Control aktualisieren element.lockWindowUpdate(true); CustTable_ds.research(); CustTable_ds.findRecord(currentRecord); CustTable_ds.refresh(); element.lockWindowUpdate(false); }
Der Quellcode des Beispiels:
Form_GridSelectedRecords.xpo (6.2 KB)
Von Zeit zu Zeit ist es hilfreich sich den Abfragebefehl, der durch eine Query erzeugt wird, zur Laufzeit anzusehen.
Leider unterstützt der Debugger von Microsoft Dynamics AX das Debuggen von Querys nicht. Ein Datenbanktrace durchzuführen ist auch nicht immer sinnvoll, da man durch das Traceprotokoll keine direkte "Verbindung" von Abfragebefehl und Query einsehen kann.
Einen Workaround hierfür gibt es aber.
Die Methode "toString" einer Query DataSource gibt den Abfragbefehl, der durch die Query an die Datenbank geschickt wird, zurück. Dieser kann dann, durch die statischen Methoden des "Debug" Objekts, im Debuggers ausgegeben bzw. angezeigt werden.
Ein Beispiel:
debug::printDebug(query.dataSourceTable(tablenum(CustTable)).toString())
Weiterhin ist unter http://www.axaptapedia.com/DEV_QueryBrowser ein Tool für die Analyse von Querys erhältlich. Dieses Tool funktioniert ähnlich wie der Tablebrowser von Dynamics AX. Mit dem Tool können nicht nur der Abfragebefehl, der durch eine Query erstellt wird, sondern auch die von der Query ermittelten Daten betrachtet und ausgewertet werden.
Eine ausreichende Beschreibung zur Verwendung des Tool's ist ebenfalls auf Axaptapedia (siehe Link weiter oben) zu finden.
Einen ersten Ausblick auf die "neuen" Quellcodeverwaltungsfeatures in Microsoft Dynamics AX 5.0 zeigt der Screencast "Version control in MorphX" auf Channel9.
http://channel9.msdn.com/Showpost.aspx?postid=367024
Ich zitiere: "This screencast is a preview of the version control system integration options in the next release of MorphX - the IDE of Dynamics AX. It shows a side-by-side comparison of the integration options with Team Foundation Server, Visual Source Safe, and MorphX VCS. The latter is a simple, yet powerful alternative without any additional infrastructure requirements. The last half of the screencast gives a demonstration of MorphX VCS."
Durch die neuen Features die MorphX VCS mit sich bringt, sowie die Möglichkeit Visual Studio Team System, oder genauer der Team Foundation Server, (nicht nur) als Quellcodeverwaltung zu verwenden, sollte nun für jeden ein "passendes" Quellcodeverwaltungsystem bereit stehen.
Vielen Dank an dieser Stelle an Michael Fruergaard Pontoppidan (http://blogs.msdn.com/mfp/default.aspx) für diesen und die bisherigen Screencasts über Microsoft Dynamics AX.
Die Version 4.0 von Microsoft Dynamics AX enthält eine neue Funktion ("Nach Raster filtern" oder "Filter by Grid") mit der die Datenfilterung direkt in einem Grid ermöglicht wird.

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

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

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

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

Die Drag & Drop Funktion kann nur von der Artikeltabelle zu den Auftragsposition durchgeführt werden. Umgekehrt funktioniert das nicht, was man auch optisch sehen kann. (Screenshots haben aus irgendwelchen Gründen nicht funktioniert) Ein weiterer Vorteil bei der Drag & Drop Funktion ist, das Sie auch Maskenübergreifend funktioniert. Hierfür sind gar keine weiteren Änderungen notwendig. Es muss in der Drop Methode, wenn dort Überprüfungen stattfinden, nur der Ursprung auch erlaubt, bzw. mit berücksichtigt wurden sein.
Die gerade erstellte Maske lässt sich schon jetzt zweimal öffnen um dort von der einen zu der anderen Maske Daten mittels Drag & Drop zu übertragen (Inventtable -> SalesLine)
Durch kleine Änderungen kann diese Funktion auch aus der Artikelmaske ausgeführt werden.
Hierzu muss in der Artikelmaske einfach auf dem Grid DragDrop auf Manual gesetzt werden und die Drop Funktion in der neu erstellten Maske leicht angepasst werden.
Beispielprojekte für das einfache Drag&Drop innerhalb einer Maske und die kleine Erweiterung für das Drag&Drop aus der Artikelmaske herraus gibts auch wieder.
In der Maske "BOMDesigner" kann die Drag&Drop Funktion noch anhand einer Baumstruktur (FormTreeControl) als Quelle bewundert werden.
Beispiel SimpleDragAndDrop Form_SimpleDragDrop.rar (1,99 KB) Beispiel Projekt für Drag&Drop aus Artikelmaske in die Maske SimpleDragAndDrop SharedProject_DragAndDropInventTable.rar (11,71 KB)
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.
Eine Bestellung umfasst in Microsoft Dynamics AX immer einen Datensatz der Tabelle „PurchTable“ und wenn die Bestellung einen Artikel enthält, auch einen Datensatz in der Tabelle „PurchLine“. Zusätzlich werden in Abhängigkeit von den Daten der Bestellung (Einmallieferant: Ja/Nein, Intercompany: Ja/Nein, etc.) zusätzliche Datensätze in anderen Tabellen erzeugt bzw. geändert. Beispielhaft sei hier die Tabelle „VendTable“ genannt. In dieser wird ein neuer Lieferant erstellt, wenn beim Erstellen der Bestellung angegeben wurde, dass es sich um einen Einmallieferanten handelt. Ein weiteres Beispiel wäre die Tabelle „MarkupTrans“ in der in Abhängigkeit von den Einstellungen für sonstige Zuschläge ebenfalls weitere Datensätze erzeugt werden.
Die Logik, die das Erstellen der einzelnen Datensätze der verschiedenen Tabellen steuert wird in Microsoft Dynamics AX durch die Klassen „PurchTableType“ (Abbildung 1) und „PurchLineType“ (Abbildung 2), sowie deren abgeleiteten Klassen abgebildet. Diese Klassen steuern das Verhalten bei Anlage, Änderung und Löschung einer Bestellung. Dies beinhaltet auch, welche Werte ein Feld bei welchem Bestellungstyp annehmen darf, was geschieht wenn ein Feld geändert wird, was wird wie gebucht und so weiter.


Diese Klassen werden von überschriebenen Methoden der Tabellen „PurchTable“ und „PurchLine“ aufgerufen. So ruft zum Beispiel die Methode „Insert“ der Tabelle „PurchTable“, die Methode „Insert“ der Klasse „PurchTableType“ auf. Abhängig vom Bestellungstyp wird über die Methode „construct“ bei der Initialisierung eines „PurchTableType“ Objekts gesteuert, welches konkrete Objekt erzeugt wird („PurchTableType_Purch“, „PurchTableType_ReturnItem“, etc.). Dies erfolgt in der Methode „type“ der Tabelle „PurchTable“ oder „PurchLine“. Unter anderem sind weiterhin die Methoden „Update“, „Delete“, „InitValue“, „ValidateField“ und „Delete“ auf die gleiche Weise überschrieben. Ein Blick in die Methoden der Tabelle „PurchTable“ sollte dies verdeutlichen.
Somit ist die Logik, die für die Steuerung von Bestellungen in Microsoft Dynamics AX verantwortlich ist, vom Prinzip her vergleichbar mit der Logik welche die Aufträge „steuert“ (vergleiche hierzu: Microsoft Dynamics AX API – Teil 1 „Erstellen von Aufträgen“).
Deswegen ist das Erstellen einer Bestellung genau so einfach wie das Erstellen eines Auftrags. Um eine neue Bestellung zu erstellen muss im Wesentlichen nur:
- Eine neue Nummer des entsprechenden Nummernkreises gezogen werden.
- Die Methode „InitValue“ der Tabelle „PurchTable“ aufgerufen werden.
- Die Methode „InitFromVendTable“ der Tabelle „PurchTable“ mit Angabe des Lieferanten Datensatzes aufgerufen werden.
- Die Methode „Insert“ der Tabelle „PurchTable“ aufgerufen werden.
Soll für diese gerade erzeugte Bestellung nun noch eine Artikelposition erzeugt werden, muss im Wesentlichen nur die Methode „CreateLine“ der Tabelle „PurchLine“, mit vorheriger Definition von Bestellungsnummer („PurchLine.PurchId“) und Artikelnummer („PurchLine.ItemId“), aufgerufen werden.
Hierzu ein Beispiel:
void createPurchTableAndLine() { VendAccount vendAccount = "<yourVendAccount>"; ItemId itemId = "<yourItemId>";
PurchTable purchTable; PurchLine purchLine; NumberSeq numberSeq; InventTable inventTable; ; //Bestellungskopf (PurchTable) //Neue Bestellungsnummer aus Nummernkreis erzeugen NumberSeq = NumberSeq::newGetNumFromCode( PurchParameters::numRefPurchId().numberSequence); purchTable.PurchId = NumberSeq.num();
//Bestellungskopf initialisieren purchTable.initValue();
//Initialisierung der lieferantenspezifischen Bestellungsdaten purchTable.initFromVendTable(VendTable::find(vendAccount));
//Bestellungskopf erstellen purchTable.insert();
//Bestellungsposition (PurchLine) purchLine.clear();
//Zuweisen von Bestellungsnummer und Artikelnummer purchLine.purchId = purchTable.PurchId; purchLine.ItemId = itemId;
//Bestellungsposition erstellen (ruft PurchLine.insert auf) purchLine.createLine(NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes); }
Ein Auftrag umfasst in Microsoft Dynamics AX immer einen Datensatz in der Tabelle „SalesTable“ (Auftragskopf) und wenn der Auftrag einen Artikel enthält (Auftragsposition), auch einen Datensatz in der Tabelle „SalesLine“. Zusätzlich werden in Abhängigkeit von den Daten des Auftrags (Einmalkunde: Ja/Nein, Intercompany: Ja/Nein, etc.) zusätzliche Datensätze in anderen Tabellen erzeugt bzw. geändert. Beispielhaft sei hier die Tabelle „CustTable“ genannt. In dieser wird ein neuer Kunde erstellt, wenn beim Erstellen des Auftrags angegeben wurde, dass es sich um einen Einmalkunden handelt. Ein weiteres Beispiel wäre die Tabelle „MarkupTrans“ in der in Abhängigkeit von den Einstellungen für Sonstige Zuschläge ebenfalls weitere Datensätze erzeugt werden.
Die Logik, die das Erstellen der einzelnen Datensätze der verschiedenen Tabellen steuert (die so genannte Geschäftslogik) wird in Microsoft Dynamics AX durch die Klassen „SalesTableType“ (Abbildung 1) und „SalesLineType“ (Abbildung 2), sowie deren abgeleiteten Klassen abgebildet. Diese Klassen steuern das Verhalten bei Anlage, Änderung und Löschung eines Auftrags. Dies beinhaltet auch, welche Werte ein Feld bei welchem Auftragstyp annehmen darf, was geschieht wenn ein Feld geändert wird, was wird wie gebucht und so weiter.


Diese Klassen werden von überschriebenen Methoden der Tabellen „SalesTable“ und „SalesLine“ aufgerufen. So ruft zum Beispiel die Methode „Insert“ der Tabelle „SalesTable“, die Methode „Insert“ der Klasse „SalesTableType“ auf. Abhängig vom Auftragstyp wird über die Methode „construct“ bei der Initialisierung eines „SalesTableType“ Objekts gesteuert, welches konkrete Objekt erzeugt wird („SalesTableType_Sales“, „SalesTableType_ItemReq“, etc.). Unter anderem sind weiterhin die Methoden „Update“, „Delete“, „InitValue“, „ValidateField“ und „Delete“ auf die gleiche Weise überschrieben. Ein Blick in die Methoden der Tabelle „SalesTable“ oder „SalesLine“ sollte dies verdeutlichen.
Somit gestaltet sich das Erstellen eines neuen Auftrags sehr einfach, da die gesamte Geschäftslogik die hinter einem Auftrag steht, automatisch aufgerufen wird.
Um einen neuen Auftrag zu erstellen muss im Wesentlichen nur
- Eine neue Nummer des entsprechenden Nummernkreises gezogen werden.
- Die Methode „InitValue“ der Tabelle „SalesTable“ aufgerufen werden.
- Die Kundennummer zugewiesen werden.
- Die Methode „InitFromCustAccount“ der Tabelle „SalesTable“ aufgerufen werden.
- Die Methode „Insert“ der Tabelle „SalesTable“ aufgerufen werden.
Soll für diesem gerade erzeugten Auftrag nun noch eine Artikelposition erzeugt werden, muss im Wesentlichen nur die Methode „CreateLine“ der Tabelle „SalesLine“, mit vorheriger Definition von Auftragsnummer („SalesLine.SalesId“) und Artikelnummer („SalesLine.ItemId“), aufgerufen werden.
Hierzu ein Beispiel:
void createSalesTableAndLine() { AccountNum custAccount = <yourCustAccount>; ItemId itemId = <yourItemId>;
SalesTable salesTable; SalesLine salesLine; NumberSeq NumberSeq; ; //Auftragskopf (SalesTable) //Neue Auftragsnummer aus Nummernkreis erzeugen NumberSeq = NumberSeq::newGetNumFromCode( SalesParameters::numRefSalesId().numberSequence); salesTable.SalesId = NumberSeq.num();
//Auftragskopf initialisieren salesTable.initValue(); salesTable.CustAccount = custAccount;
//Initialisierung der kundenspezifischen Auftragsdaten salesTable.initFromCustTable();
//Auftragskopf erstellen salesTable.insert();
//Auftragsposition (SalesLine) salesLine.clear();
//Zuweisen von Auftragsnummer und Artikelnummer salesLine.SalesId = salesTable.SalesId; salesLine.ItemId = itemId;
//Auftragsposition erstellen (ruft SalesLine.insert auf) salesLine.createLine(NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes); }
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.
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
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)
In Microsoft Dynamics AX werden alle Benutzerberechtigungen über SecurityKeys gesteuert. SecurityKeys können für Forms, Formcontrols, Tables, Tablefields, MenuItems, etc. in deren Eigenschaften hinterlegt werden.
Für Klassen ist dies zwar mit Hilfe eines MenuItems und der Implementation der Methode "static void Main(Args _args)" ebenfalls möglich, doch kann für eine einzelne Methode kein SecurityKey vergeben werden.
Es gibt allerdings Situationen wo die Codeausführung, abhängig von der jeweiligen Berechtigung des Benutzers, gesteuert werden soll/muss. In einem solchen Fall muss im X++ Code eine Überprüfung der Berechtigungen des Benutzers durchgeführt werden.
Möchte man prüfen, ob ein Benutzer Zugriff auf einen SecurityKey hat, kann dies mit der Methode hasSecurityAccess erfolgen.
if ( hasSecurityKeyAccess(securitykeyNum(CustSetup), AccessType::View) ) { //Code ausführen, wenn entsprechende Berechtigung vorhanden ist. }
Möchte man prüfen, ob ein Benutzer Zugriff auf eine Tabelle hat, geht dies mit der Methode hasTableAccess.
if ( hasTableAccess(tablenum(CustTable), AccessType::Edit) ) { //Code ausführen, wenn entsprechende Berechtigung vorhanden ist. }
Muss nicht nur die Tabelle, sondern auch ein einzelnes Feld überprüft werden, kann dies mit der Methode hasFieldAccess gemacht werden.
if ( hasFieldAccess(tablenum(CustTable), fieldnum(CustTable, AccountNum), AccessType::Delete) ) { //Code ausführen, wenn entsprechende Berechtigung vorhanden ist. }
Der Parameter AccessType bestimmt hierbei auf welche Berechtigung das jeweilige Element gepürft wird (Kein Zugriff, Anzeigen, Bearbeiten, Erstellen, Vollständige Kontrolle).
Alle Methoden (hasSecurityAccess, hasTableAccess, hasFieldAccess) sind globale Methoden, die in der Klasse Global definiert sind. Somit können diese Methoden an jeder beliebigen Stelle im Quellcode verwendet werden. Es ist egal ob es sich um die Methode einer Form, Datasource, Klasse oder Tabelle handelt.
Nachdem nun in diesem Artikel schon die ersten Schritte unternommen und vorhandene Elemente manipuliert wurden, werden nun auch neue Elemente hinzuzugefügt.
Einfügen einer neuen Formcontrol im Design.
FormRun fr; Args args = new ARgs(); FormStringControl fsc; ; args.name(formstr(InventTable)); fr = new FormRun(args); fr.init(); // Neue Control (Tabellenfeld "AltItemID") im Design anfügen fsc = fr.design().addDataField(fr.dataSource(1).id(),fieldnum(InventTAble, altitemid)); fr.run(); fr.wait();
In dem o.g. Beispiel wird das Feld Ersatz-Artikelnummer direkt im Design der Form eingefügt. Bei einem Datenfeld ist es wichtig das als Datasource immer die ID der FormDatasource übergeben wird um die richtige Referenz zu erhalten. Anstelle eines Datenfeldes kann natürlich auch jedes andere Formcontrol eingefügt werden, was wiederrum anderen Möglichkeiten der weiteren Verwendung bieten kann. Möchte man kein Datenfeld erzugen kann mit
.addControl(... auch ein beliebiges FormControl hinzugefügt werden. Über den Enum FormControlType wird die Art des Controls in Methode addControl als erstes Parameter festgelegt.
element.design().addControl(FormControltype::String, "Neue Control");
Um das Feld Ersatz-Artikelnummer Beispieltsweise im Überblick - Grid anzuzeigen muss die Erzeugung der FormControl auch auf dem Grid geschehen.
Hierzu wird die FormGridControl „Grid“ aus der Maske gesucht und dort das neue Feld hinzugefügt.
FormRun fr; Args args = new ARgs(); FormGridControl fgc; ; args.name(formstr(InventTable)); fr = new FormRun(args); fr.init(); // Zugriff auf FormControl "Grid" aus dem Design fgc = fr.design().controlName("Grid"); // Dem Grid ein neues Control hinzufügen fgc.addDataField(fr.dataSource(1).id(),fieldnum(InventTAble, altitemid)); fr.run(); fr.wait();
Eine neues FormControl wird also immer vom Vater (Übergeordneten) Control aus erzeugt, im zweifelsfall also mindestens auf dem Design der Form.
parentControl.addControl(…
es muss immer die FormControl gefunden werden auf dem die FormControl erzeugt werden soll. Neue FormControls können nur auf Container FormControls, wie Grid, TabPage, Group usw. erzeugt werden.
Über X++ können alle Eigenschaften der FormControls manipuliert werden. Je nach ControlTyp stehen unterschiedliche Eigenschaften zur Verfügung.
FormGroupControl groupCtrl; FormStringControl stringCtrl; ; //Neue Gruppe im Design einfügen groupCtrl = element.design().addControl(FormControlType::Group, "Gruppe"); //Neue Zeichenfolge der Gruppehinzufügen stringCtrl = groupCtrl.addControl(FormControlType::String, "ZeichenFolge"); //Zuweisung der DataSource stringCtrl.dataSource(element.dataSource().name()); //Zuweisung des Feldes // stringCtrl.dataField(fieldnum(InventtAble, ItemID)); //Zuweisung einer Methode der Tabelle stringCtrl.dataMethod("ItemName");
stringCtrl.enabled(false); stringCtrl.visible(true);
In dem Beispiel wird eine Gruppe direkt im Design erzeugt und in dieser Gruppe wird ein Zeichenfolge eingefügt, die über die Methode ItemName auf der InventTable Daten anzeigt.
Überschreiben der Standardevents ausserhalb der Form
Auch Events wie Modified oder Validate können ausserhalb der Form überschrieben werden. Hierzu muss der aufgerufenden Form mitgeteilt werden, das dieses nicht mehr in der Maske passieren soll, sondern in dem aufrufenden Element (zb. In der Class).
// Das Überschreiben der Methoden erlauben fR.controlMethodOverload(true); // Das Objekt festlegen in der die Methoden definiert sind fR.controlMethodOverloadObject(this);
Nun kann man nicht pauschal für alle Controls eine Methode definieren, sondern muss für jede Control eine eigende Definition erzeugen. Die Syntax ist aber immer die gleiche und fängt mit der Benamung der Methode an. D.h. wurden 10 FormControls zur Laufzeit eingefügt müssen für jeden Event der verändert werden soll eine neue Methode erzeugt werden. Eine Ausnahme, die aber oft zur Regel werden kann, ist das nur für jeden unterschiedliche Namen der FormControls zusätzliche Methode erzeugt werden müssen.
ControlName_MethodenName
Als Beispiel kann die Control ctrlInventTable_ItemId (Artikelnummer im Reiter Übersicht) wie folgt aussehen um die Änderung auf diesem Feld abzufragen
Public boolean ctrlInventTable_ItemID_modified() { FormStringControl c = formrun.controlCallingMethod(); // Die FormControl, von der der Aufruf erfolgt boolean ret; ; ret = c.modified(); // Super() der aktuellen FormControl aufrufen ->modified
return ret; }
Oben genannte Methode entspricht der Standard Methode "modified" auf der FormControl "ctrlInventTable_ItemID"
public boolean modified() { boolean ret;
ret = super();
return ret; } In den og. Fall hätte man sich das Überschreiben der Methode Modified sparen können, da nichts gemacht wird als die Standardfunktion aus Dynamics AX aufzurufen. Es werden aber alle wichtigen Elemente dargestellt um einen ordnungsgemäße Funktionalität zu gewährleisten.
Achtung: Gibt es mehrere Controls in der Form die dieselben Namen haben, werden auch die überschriebenen Methoden von all diesen Controls benutzt! Somit muss die Unterschreidung welche Control das Event gerade ausgelöst hat hier abgefragt werden. Es können aber nur Controls die zur Laufzeit per X++ erzeugt wurden den gleichen Namen erhalten. Beim "normalen" Erstellen einer Form kann das nicht vorkommen...
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(); }
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(); }
Wer auf der Suche nach Dokumentation zu / über Dynamics Ax 4.0 ist, sollte mal einen Blick auf diese Seite werfen: Using Microsoft Dynamics AX
Mit dieser Property lässt sich bestimmen, ob ein Feld ein Eingabe- oder Suchfeld sein soll. Dies ist gerade bei der Verwendung von Gridcontrols hilfreich, da keine zusätzlichen Filter- oder Suchfunktionen eingebaut werden müssen.
Die Property kann folgende Werte annehmen:
Eingaben in diesem Feld ist möglich. Das Feld ist somit ein Eingabe und kein Suchfeld
Das Feld ist ein Suchfeld. Eingaben in die Datenbank sind über dieses Feld nicht mehr möglich.
Wird in dem Feld eine Eingabe vorgenommen, wird nach dem Verlassen des Datensatzes eine Abfrage der Datenbank durchgeführt. Es werden der Feldeingabe entsprechende Datensätze angezeigt. Es wird auf den eingegebenen Wert gefiltert.
Das Feld ist ein Suchfeld. Eingaben in die Datenbank sind über dieses Feld nicht mehr möglich.
Sobald in diesem Feld eine Eingabe erfolgt, wird automatisch zu dem der Eingabe entsprechenden Datensatz gesprungen. Gleiches verhalten wie im AOT.
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.
Langsam ist der Zeitpunkt gekommen, zu dem sich auch ein Dynamics Ax Entwickler mit dem Thema .NET beschäftigen sollte.
Ich stimme fast jedem zu der sagt, dass man bei Dynamics Ax 3.0 eigentlich keinerlei .NET Kenntisse benötigt. Allerdings wird sich das mit Dynamics Ax 4.0, durch die erweiterte Integration zwischen Dynamics Ax und .NET, ändern.
Deshalb sollte man nicht auf den Release von Dynamics Ax 4.0 warten, sondern sein Wissen schon jetzt erweitern.
Einen guten Einstieg in die .NET Welt bietet die MSDN Webcast-Serie von Bernd Marquardt, Get Sharper Now! - C# für Einsteiger und Umsteiger (ich gehe davon aus, dass die Sprache C# die erste Wahl sein wird, jedenfalls für die meisten Dynamics Ax Entwickler).
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";
|