AX 2012: Beispiel für die Verwendung des SysOperation-Frameworks

Das SysOperation-Framework ist eine Neuerung in Dynamics AX 2012 und soll das RunBasebatch-Framework ersetzen. In der MSDN sind zahlreiche Dokumentationen und Whitepaper über dieses Framework zu finden, welche ich unbedingt empfehle zu lesen.

Der folgende Beitrag enthält ein bewusst einfach gehaltenes Beispiel, wie man in AX 2012 Programmlogik auf Basis dieses Framework einsetzen kann.

Das Beispiel besteht dabei aus insgesamt vier Klassen:

  • Data Contract Class
  • Service Class
  • UIBuilder Class
  • Controller Class

 

Data Contract Class

Eine Data Contract Class besteht im Grunde genommen lediglich aus Accessor-Methoden (parm...-Methoden) und ist dadurch gekennzeichnet, daß in der classDeclaration() das Attribute DataContractAttribute verwendet wird.

Für alle Accessor-Methoden des Data Contracts werden vom SysOperation-Framework beim Aufruf entsprechende Dialogfelder generiert.

DIe weiteren im Beispiel verwendeten Attribute SysOperationContractProcessingAttribute bzw. SysOperationGroupAttribute verknüpfen zum Einen den Data Contract mit einem UI Builder und zum Anderen wird - neben der standardmässig immer generierten Feldgruppe namens Parameter - eine weitere Feldgruppe mit dem internen Namen DemoGroup.

[
    DataContractAttribute
    ,SysOperationContractProcessingAttribute(classStr(TutorialSysOperationUIBuilder))
    ,SysOperationGroupAttribute('DemoGroup', 'For demonstration purpose only', '2')
]
class TutorialSysOperationDataContract
    implements SysOperationValidatable,
               SysOperationInitializable
{
    str             qryStr;
    FilenameSave    filenameSave;
    TransDate       dialogDate;
    CustAccount     custAccount;
}

Accessor-Methoden eines Data Contracts müssen immer über das Attribute DataMemberAttribute gekennzeichnet werden. Das Attribute SysOperationDisplayOrderAttribute legt die Reihenfolge der Felder im Dialog fest und mit Hilfe des Attributes SysOperationGroupMemberAttribute wird gesteuert, daß das Feld innerhalb der in der classDeclaration() definierten Feldgruppe eingeordnet wird.

[
    DataMemberAttribute
    ,SysOperationDisplayOrderAttribute('2')
    ,SysOperationGroupMemberAttribute('DemoGroup')
]
public CustAccount parmCustAccount(CustAccount _custAccount = custAccount)
{
    custAccount = _custAccount;
    return custAccount;
}

Die Accessor-Methode für das Datum unterscheidet sich im groben nur durch ein weiteres verwendetes Attribute SysOperationLabelAttribute mit Hilfe dessen das Label des Dialogfeldes festgelegt wird.

Das hier anzugebende Datum ist im übrigen lediglich zu Demonstrationszwecken implementiert und hat keinen erwähnenswerten Einfluss auf die Programmlogik.  

[
    DataMemberAttribute
    ,SysOperationDisplayOrderAttribute('3')
    ,SysOperationGroupMemberAttribute('DemoGroup')
    ,SysOperationLabelAttribute(literalStr("@SYS128676"))
]
public TransDate parmDialogDate(TransDate _dialogDate = dialogDate)
{
    dialogDate = _dialogDate;
    return dialogDate;
}

In der Accessor-Methode für den Dateinamen wird über das Attribute SysOperationDisplayOrderAttribute festgelegt, daß das Feld das Erste im Dialog sein soll.

[
    DataMemberAttribute
    ,SysOperationDisplayOrderAttribute('1')
]
public FilenameSave parmFilenameSave(FilenameSave _filenameSave = filenameSave)
{
    filenameSave = _filenameSave;
    return filenameSave;
}

Eine Besonderheit unter den im Beispiel verwendeten parm-Methoden stellt die folgende Methode dar. Weil innerhalb von Accessor-Methoden nicht jeder in AX zur Verfügung stehender Datentyp verwendet werden kann, muss ein Query beispielsweise in einen String umgewandelt und übergeben werden. Das SysOperation-Framework stellt dafür eigene Methoden zur Verfügung.

[
    DataMemberAttribute
    ,AifQueryTypeAttribute('_qryStr', '')
]
public str parmQuery(str _qryStr = qryStr)
{
    qryStr = _qryStr;
    return qryStr;
} 

Die Methode validate() steht nur zur Verfügung, wenn in der classDeclaration() des Data Contracts die Klasse SysOperationValidatable eingebunden wurde. Dann funktioniert sie ähnlich, wie im RunBaseBatch-Framework.

public boolean validate()
{
    boolean ret;
 

    ret = true;

    // Simple validation example
    if(this.parmDialogDate() && this.parmDialogDate() < systemDateGet())
    {
        ret = checkFailed(strFmt("'%1' has to be greater/equal than %2.", "@SYS128676", systemDateGet()));
    }

    if(!this.parmFilenameSave())
    {
        ret = checkFailed(strFmt("Field '%1' must be filled in.",
                    extendedTypeId2pname(extendedTypeName2Id(identifierStr(FileNameSave)))
                    ));
    }
    return ret;
}

Die Methode initialize() steht nur zur Verfügung, wenn in der classDeclaration() des Data Contracts die Klasse SysOperationInitalize eingebunden wurde. Diese Methode wird nur aufgerufen, wenn keine Nutzungsdaten vorhanden oder verwendet werden.

public void initialize()
{
    dialogDate = systemDateGet() - 365;
}

 

Service Class

Diese - von SysOperationServicebase abgeleitete - Klasse, enthält die eigentliche Logik des Beispiels.

class TutorialSysOperationService 
    extends SysOperationServiceBase
{
}

Genauergesagt ist die Logik in der Methode runService() enthalten. Die Besonderheit dieser Methode ist, daß sie als einzigen Parameter den Data Contract erhält und daß das Attribute SysEntryPointAttribute gesetzt ist. Der Name der Methode hingegen bleibt dem Entwickler überlassen, die Methode wird über den Namen in der Methode newFromArgs() des Service Controllers referenziert.

Die im Beispiel enthaltene Methode enthält keine aufregende Logik, sie erstellt ledigich eine einfache Text-Datei und schreibt in diese einige Werte aus den über den Query übergebenen Ausgangsrechnungen.

[SysEntryPointAttribute(true)]
public void runService(TutorialSysOperationDataContract _dataContract)
{
    Query               query = new Query(SysOperationHelper::base64Decode(_dataContract.parmQuery()));
    QueryRun            queryRun;
    CustInvoiceJour     custInvoiceJour;
    TextIo              textIo;
    FileIOPermission    fileIOPermission;
    #file

    if(_dataContract.parmFilenameSave())
    {
        // Assert permission.
        fileIOPermission = new FileIOPermission(_dataContract.parmFilenameSave() , #io_write);
        fileIOPermission.assert();

        // If the test file already exists, delete it.
        if (WinAPIServer::fileExists(_dataContract.parmFilenameSave()))
        {
            WinAPIServer::deleteFile(_dataContract.parmFilenameSave());
        }
        textIo = new TextIo(_dataContract.parmFilenameSave(), #io_write);

        if( !textIo)
        {
            throw error('File creation failed.');
        }

        textIo.write(strFmt("--- %1 ---", _dataContract.parmDialogDate()));
    }

    // Do service really need to call validate one more time? framework should do it instead!
    if (!_dataContract.validate())
    {
        // Service should always revalidate parameters
        throw error("@SYS326740");
    }

    queryRun = new QueryRun(query);
    while(queryRun.next())
    {
        custInvoiceJour = queryRun.get(tableNum(CustInvoiceJour));

        if(textIo)
        {
            textIo.write(strFmt("Invoice: %1;CustAccount: %2;Currency: %3", custInvoiceJour.InvoiceId, custInvoiceJour.OrderAccount, custInvoiceJour.custTable_OrderAccount().Currency));
        }
    }
}

 

Controller Class

Eine Controller Class muss von SysOperationServiceController abgeleitet sein.

class TutorialSysOperationServiceController 
    extends SysOperationServiceController
{
}

Die Methode newFromArgs() ist eine selbst erstellte Methode, die dazu dient, die Klasse zu instanziieren. Dabei wird die Klasse und die als Service aufzurufende Methode festgelegt. Gleichzeitig wird über die Methode parmArgs() des Frameworks der Aufrufer für einen evtl. späteren notwendigen Zugriff gespeichert.

public static TutorialSysOperationServiceController newFromArgs(Args _args)
{
    TutorialSysOperationServiceController controller;

    controller = new TutorialSysOperationServiceController(classStr(TutorialSysOperationService), methodStr(TutorialSysOperationService, runService));
    controller.parmArgs(_args);

    return controller;
}

Die initQuery() dient dazu, den Query für den Dialog zu initalisieren. Diese Methode wird später von der Methode initializeServiceParameter() aufgerufen.

Erwähnenswert ist in Verbindung mit dem Query die Klasse SysOperationHelper, die u.a. Methoden zur Verfügung stellt, um einen Query in einen String zu konvertieren und umgekehrt. Dies ist notwendig, da der Data Contract nur mit "einfachen" Datentypen arbeiten kann.

public static Query initQuery(TutorialSysOperationDataContract _dataContract)
{
    Query                query;
    QueryBuildDataSource queryBuildDataSource;
    QueryBuildRange      queryBuildRangeInvoiceDate;
    ;
    query = new Query();

    queryBuildDataSource = query.addDataSource(tableNum(CustInvoiceJour));

    // Build ranges for SELECT-Button
    SysQuery::findOrCreateRange(queryBuildDataSource, fieldNum(CustInvoiceJour, OrderAccount));
    SysQuery::findOrCreateRange(queryBuildDataSource, fieldNum(CustInvoiceJour, InvoiceAccount));

    // Add locked Range
    queryBuildRangeInvoiceDate = query.dataSourceTable(tableNum(CustInvoiceJour)).addRange(fieldNum(CustInvoiceJour, InvoiceDate));
    queryBuildRangeInvoiceDate.value("01.01.2010..");
    queryBuildRangeInvoiceDate.status(RangeStatus::Locked);

    // Add sort fields
    query.dataSourceTable(tableNum(CustInvoiceJour)).addSortField(fieldNum(CustInvoiceJour, InvoiceDate), SortOrder::Ascending);

    _dataContract.parmQuery(SysOperationHelper::base64Encode(query.pack()));

    return query;
}

Wie zuvor angekündigt, verwende ich die Methode initializeServiceParameter() um den Query des Dialoges über die Methode initQuery() zu initalisieren. Weiters wird an dieser Stelle ein weiterer Parameter mit Werten befüllt.

protected Object initializeServiceParameter(DictMethod dictMethod, int parameterIndex)
{
    Object                              ret;
    TutorialSysOperationDataContract    tutorialSysOperationDataContract;

    ret = super(dictMethod, parameterIndex);

    if(ret is TutorialSysOperationDataContract)
    {
        tutorialSysOperationDataContract = ret;
        TutorialSysOperationServiceController::initQuery(ret);

        tutorialSysOperationDataContract.parmDialogDate(systemDateGet());   // Overridden by SysLastValue, if exists
    }

    return ret;
}

Die main()-Methode ist bereits aus dem RunBaseBatch-Framework hinreichend bekannt. Sie wird immer dann aufgerufen, wenn die Controller Class über ein MenuItem aufgerufen wird.

In dieser Methode wird - über die oben erwähnte Methode newFromArgs() - die Klasse instanziiert. Weiters wird über parmExecutionMode() der Ausführungsmodus festgelegt und schließlich über startOperation() der eigentliche Aufruf des Service gestartet.

Diese Methode retouniert übrigens einen Enum-Wert vom Typ SysOperationStartResult mit Hilfe dessen man abfragen kann, ob das Service beispielsweise sofort oder im Stapel gestartet wurde oder ob der Dialog vom Benutzer abgebrochen wurde.

public static void main(Args _args)
{
    TutorialSysOperationServiceController   controller;
    TutorialSysOperationDataContract        dataContract;
    SysOperationStartResult                 sysOperationStartResult;

    if (!_args)
    {
        throw error("@SYS25407");
    }

    controller = TutorialSysOperationServiceController::newFromArgs(_args);
    controller.parmExecutionMode(SysOperationExecutionMode::Synchronous);

    setPrefix(controller.caption());

    sysOperationStartResult =
        controller.startOperation();

    if(sysOperationStartResult == SysOperationStartResult::Started)
    {
        dataContract = controller.getDataContractObject();
        if(dataContract is TutorialSysOperationDataContract)
        {
            info(strFmt("File %1 sucessfully written.", dataContract.parmFilenameSave()));
        }
    }
}

 

UI Builder Class 

Eine User Interface Builder Class ist dadurch gekennzeichnet, daß sie von SysOperationAutomaticUIBuilder abgeleitet ist. Eine solche Klasse kann dazu verwendet werden, den vom Framework automatisch generierten Dialog um eigene Logik zu erweitern.

Die Verknüpfung zwischen Data Contract und UI Builder erfolgt dabei über das Attribute SysOperationContractProcessingAttribute in der classDeclaration() des Data Contracts.

class TutorialSysOperationUIBuilder 
    extends SysOperationAutomaticUIBuilder
{
    DialogField df_custAccount;
}

Ein Ziel unserer UI Builder Class ist, das Lookup-Formular des Feldes für das Debitorenkonto zu übersteuern. Dazu erstellen wird eine neue Methode beliebigen Namens - im Beispiel custAccount_lookup() - und befüllen diese mit dem gewünschten Programmcode. Im Grunde genommen unterscheidet sich die Methode nicht von anderen, ähnlichen Lookup-Methoden. Das einzige worauf man achten muß ist, daß die Methode die gleichen Parameter enthält, wie die gleichwertige Methode eines Formulares.

public void custAccount_lookup(FormStringControl _control)
{
    SysTableLookup          sysTableLookup;
    Query                   query = new Query();

    query.addDataSource(tableNum(CustTable));

    SysQuery::findOrCreateRange(query.dataSourceTable(tableNum(CustTable)), fieldNum(CustTable, Currency)).value(queryValue(CompanyInfo::standardCurrency()));

    sysTableLookup = SysTableLookup::newParameters(tableNum(CustTable),_control);
    sysTableLookup.addLookupfield(fieldNum(CustTable,AccountNum),true);
    sysTableLookup.addLookupMethod(tableMethodStr(CustTable, name));
    sysTableLookup.addLookupfield(fieldNum(CustTable,Currency));

    sysTableLookup.parmQuery(query);
    sysTableLookup.performFormLookup();
}

Die Methode postBuild() ist ein idealer Platz, um die in der classDeclaration() deklarierte Variable vom Typ DialogField über bindInfo().getDialogField() mit Leben zu befüllen.

Auch ist die Methode gut dazu geeignet, die Eigenschaften von Dialogfelder abhängig vom Aufrufer zu übersteuern. Im Beispiel wird dazu die Methode parmArgs() des Service Controllers abgefragt.

public void postBuild()
{
    super();

    // get references to dialog controls after creation
    df_custAccount = this.bindInfo().getDialogField(this.dataContractObject(),
                                                    methodStr(TutorialSysOperationDataContract, parmCustAccount));

    // Initalize dialog field from calling args
    if(this.controller().parmArgs()          &&
       this.controller().parmArgs().record() &&
       this.controller().parmArgs().dataset() == tableNum(CustTable))
    {
        df_custAccount.value(this.controller().parmArgs().record().(fieldNum(CustTable, AccountNum)));
        df_custAccount.allowEdit(false);
    }
}

In der Methode postRun() kann man nun das Dialogfeld mit der oben beschriebenen Lookup-Methode verbinden.

public void postRun()
{
    super();

    // override methods
    df_custAccount.registerOverrideMethod(  methodStr(FormStringControl, lookup),
                                            methodStr(TutorialSysOperationUIBuilder, custAccount_lookup), this);
}

 

Menu Item

Als letzten Schrittt gilt es nun ein MenuItem für die Service Controller Class zu erstellen. Ruft man dieses MenuItem auf, so sollte sich das oben beschriebene Beispiel wie folgt im Client darstellen:

Screenshot

Dieser Beitrag bezieht sich auf die Version:
Dynamics AX 2012

Martin Pfister 13.04.2013 09:32 | #1

Super Beitrag, herzlichen Dank! Eine Frage bezüglich der gespeicherten Datei. In einer 3-Tier Architektur wird das File auf dem AOS Server abgelegt. Wie öffnet der User die Datei wenn er nur Zugriff auf AX über Remote App oder RDS Server hat?

Heinz Schweda 16.04.2013 21:00 | #2

Wenn man nur Zugriff auf AX über Remote App hat, befürchte ich hat man schlechte Karten.
Mein Beispiel geht davon aus, daß der Benutzer die Datei an einer Stelle ablegt auf die er auch Zugriff hat.

Kommentar hinzufügen
 
 

 

 
 
 
Beiträge des aktuellen Monats
März 2024
MoDiMiDoFrSaSo
 123
45678910
11121314151617
18192021222324
25262728293031
 
© 2006-2024 Heinz Schweda | Impressum | Kontakt | English version | Mobile Version
Diese Webseite verwendet Cookies, um Benutzern einen besseren Service anzubieten. Wenn Sie weiterhin auf der Seite bleiben, stimmen Sie der Verwendung von Cookies zu.  Mehr dazu