Praktisches Beispiel für die Verwendung des runBaseBatch-Frameworks

Vor einiger Zeit habe ich schon einmal einen Eintrag über das runBaseBatch-Framework geschrieben, damals ging's hauptsächlich um die Aufrufreihenfolge der einzelnen Methoden und wofür einige von diesen Methoden verwendet werden können bzw. verwendet werden sollten.

Nun habe ich mir basierend auf diesem Eintrag und vor allem aufgrund von Erfahrungswerten eine eigene Klasse namens tutorial_ClassWithQueryRun geschrieben, die einige oft benötigte Anforderungen vereinigt.

Diese Klasse vereint folgende Fähigkeiten:

  • Stapelfähigkeit
  • Arbeiten mit einem Query
  • Arbeiten mit den Nutzungsdaten
  • Schreiben einer Datei via textIo
  • Arbeiten mit Fortschrittsanzeigen (progress)

Screenshot

Die erste Anforderung an meine Klasse war, daß sie stapelfähig sein musste. Dazu muss sie in erster Linie von runBaseBatch abgeleitet sein, und einige Methoden wie pack, unPack, canGoBatch müssen entsprechend überschrieben werden.

Die nächste Anforderung war, daß der Benutzer über einen Dialog Parameter festlegen kann. Dafür verantwortlich sind u.a. dialog, getFromDialog, putToDialog und natürlich die main-Methode.

Weiters sollte über diesen Dialog der Benutzer die Möglichkeit erhalten, einen definierten Query durch Filter und drgl. zu erweitern. Dazu mussten folgende Methoden angepasst/erstellt werden: initQuery, showQueryValues und showQuerySelectButton.

Die Methoden filenameLookupFilter, filenameLookupInitialPath und filenameLookupTitle wurden dazu verwendet, dem Benutzer im Dialog die Eingabe des Dateinamens zu erleichtern.

Starten wir also mit der classDeclaration. Hier erfolgt die Ableitung von runBaseBatch, weiters werden sämtliche Objektvariablen deklariert, die innerhalb der Methoden benötigt werden. Außerdem wird hier über das Makro currentList festgelegt, welche dieser Variablen in den Nutzungsdaten gespeichert werden.

class tutorial_ClassWithQueryRun extends RunBaseBatch
{
    FilenameSave    filenameSave;
    DialogField     df_FileNameSave;
    DialogField     df_DialogDate;
    date            dialogDate;
    SysQueryRun     queryrun;
    TextIo          textIo;
    #file
    #define.CurrentVersion(2)
    #define.Version1(1)
    #localmacro.CurrentList
        fileNameSave,
        dialogDate,
        queryRun
    #endmacro
}

In der pack-Methode werden nun über einen Container jene Variablen in die Nutzungsdaten geschrieben, die z.B. für die korrekte Abarbeitung innerhalb der Stapelverarbeitung von Nöten sind.

public container pack()
{
    ;
    return [ #CurrentVersion,
             filenameSave,
             dialogDate,
             queryrun.pack() ];
}

Die Methode unPack ist das Gegenstück zur pack-Methode, ist sie doch für das Auslesen der Werte aus den Nutzungsdaten zuständig. Wichtig hierbei ist natürlich, daß die Werte innerhalb des Containers in der gleichen Reihenfolge ausgelesen werden, wie sie geschrieben wurden. Weiters ist hier dafür zu sorgen, daß unterschiedliche Nutzungsdaten der Klasse über die entsprechende Versionsinfo korrekt berücksichtigt werden. Wird zum Beispiel die Klasse insofern angepasst, daß entweder die Reihenfolge der Variablen im Macro currentList geändert wird, oder - und das ist der wohl üblichere Fall - daß neue Variablen in die Nutzungsdaten aufgenommen werden,  ist das Makro currentVersion anzupassen.

In nachstehender Methode wurde beispielsweise berücksichtigt, daß in der ersten ausgelieferten Version der Klasse die Variable dialogDate noch nicht in den Nutzungsdaten gespeichert wurde. Erst mit Version 2 (=aktuelle Version) wurde auch diese Variable über die pack-Methode in die Nutzungsdaten geschrieben.

public boolean unpack(container packedClass)
{
    Integer         version     = conpeek(packedClass,1);
    PackedQueryRun  packedQueryRun;
    ;
    switch (version)
    {
        case(#CurrentVersion) :
            [version,filenameSave,dialogDate,packedQueryRun] = packedClass;

            if (queryIsPackedOk(packedQueryRun))
            {
                queryrun = new QueryRun(packedQueryRun);

                if(!queryrun)
                {
                    this.initQuery();
                }
            }
            break;
        case(#Version1) :
            // New dialog-field "date" was added in classversion 2
            [version,filenameSave,packedQueryRun] = packedClass;

            if (queryIsPackedOk(packedQueryRun))
            {
                queryrun = new QueryRun(packedQueryRun);

                if(!queryrun)
                {
                    this.initQuery();
                }
            }

            dialogDate = systemdateget();

            break;
        default :
            return false;
    }

    return true;
}

Über die Methode allowSaveLast kann gesteuert werden, ob Werte aus den Nutzungsdaten oder die Standardwerte aus der Methode initParmDefault verwendet werden sollen.

public boolean allowSaveLast()
{
    boolean ret;
    ;
    // true  = Gets the last choice stored in the last value table
    // false = call initParmDefault()

    ret = super();

    return ret;
}

In der Methode canGoBatch legt man fest, ob eine Klasse im Stapel ausgeführt werden können soll.

public boolean canGoBatch()
{
    boolean ret;
    ;
    // Enable/disable batch
    ret = true;

    return ret;
}

Die Methode runImpersonated steuert (unter AX 2009), ob die Klasse, sofern sie im Stapel ausgeführt wird, am AOS ausgeführt wird, oder über die manuell vom Benutzer zu startende Stapelverarbeitung.

public boolean runsImpersonated()
{
    boolean ret;

    ret = super();

    // ret = TRUE:  Runs on server (default)
    // ret = FALSE: Runs only if started through Basic > Periodic > Batch > Processing

    return ret;
}

Die main-Methode wird aufgerufen, wenn die Klasse über eine Schaltfläche in einem Formular, einen Eintrag im Menu (beides setzt sein entsprechendes MenuItem voraus) oder aber direkt aus dem AOT aufgerufen wird. Über den Aufruf von prompt wird der Dialog der Klasse geöffnet.

public static void main(Args args)
{
    Tutorial_ClassWithQueryRun tutorial_ClassWithQueryRun = Tutorial_ClassWithQueryRun::construct();
    ;

    if(tutorial_ClassWithQueryRun.prompt())
    {
        tutorial_ClassWithQueryRun.run();
    }
}

Die dialog-Methode dient dazu, dem Benutzer die Möglichkeit zu geben, bestimmte Parameter und dergleichen festzulegen. Dazu werden dem Dialog die gewünschten Dialog-Felder hinzugefügt.

protected Object dialog(DialogRunbase dialog, boolean forceOnClient)
{
    DialogRunbase ret;
    ;
    ret = super(dialog, forceOnClient);

    // Note: Don't use addFieldValue when using Default-Button

    df_FileNameSave = ret.addField(typeid(FilenameSave));
    df_DialogDate   = ret.addField(typeid(TransDate));

    df_FileNameSave.value(this.parmFilename());
    df_DialogDate.value(  this.parmDialogDate());

    // Add helptext
    df_FileNameSave.helpText('Enter filename');
    df_DialogDate.helpText('Field is not used. Demonstration purpose only');

    ret.filenameLookupFilter(this.filenameLookupFilter());
    ret.filenameLookupInitialPath(this.filenameLookupInitialPath());
    ret.filenameLookupTitle(this.filenameLookupTitle());

    return ret;
}

GetFromDialog wird aufgerufen, wenn der Benutzer den Dialog mit OK bestätigt. Die Methode dient dazu, die eingetragenen Werte/Parameter auszulesen und wie im Beispiel über entsprechende parm-Methoden der Instanz zu übergeben. 

public boolean getFromDialog()
{
    boolean ret;
    ;
    ret = super(); 
    this.parmFilename(df_FileNameSave.value());
    this.parmDialogDate(df_DialogDate.value());

    return ret;
}

Die Methode putToDialog dient dazu, um die Dialogfelder des Dialogs mit Werten zu befüllen. Diese Methode wird unter anderem beim Betätigen der Schaltflächen Löschen und Standard aufgerufen.

protected void putToDialog()
{
    ;
    df_FileNameSave.value(this.parmFilename());
    df_DialogDate.value(this.parmDialogDate());

    super();
}

Die Methode dialogClear kann dazu verwendet werden, um Dialogfelder bzw. deren Inhalten zurückzusetzen. Diese Methode zu überschreiben macht natürlich nur dann Sinn, wenn die Methode showClearButton entsprechend true retourniert und somit die Schaltfläche Löschen im Dialog angezeigt wird.

public void dialogClear()
{
    ;
    super();
    // Rebuild query/queryRun
    this.initQuery();

    // Clear parameters
    filenameSave = "";
    dialogDate   = dateNull();
}

Die folgenden drei Methoden werden dazu verwendet, den Benutzer bei der Eingabe des Speicherortes für die durch die Klasse zu erstellende Datei zu unterstützen. So werden der auszuwählende Dateityp genauso gesteuert, wie der Initial-Ablagepfad und der Titel des "Speichern untern"-Windows-Dialoges.

FilenameFilter filenameLookupFilter()
{
    return ["Tab-Separated Files;Text-Files",'*.tsv;*.txt'];
}

 

Filename filenameLookupInitialPath()
{
    #WinApi
    ;
    return winApi::getFolderPath(#CSIDL_DESKTOP);
}

 

str filenameLookupTitle()
{
    return "@SYS63229";
}

Die Schaltfläche Löschen im Dialog kann über die Methode showClearButton gesteuert werden. Wird die Schaltfläche eingeblendet, sollte die Methode dialogClear entsprechend überschrieben worden sein.

protected boolean showClearButton()
{
    boolean ret;
    ;
    // Enable/disable Clear-Button
    ret = true;

    return ret;
}

Soll im Dialog die Schaltfläche Standard angezeigt werden, so muss die Methode showDefaultButton entsprechend übersteuert werden. Diese Schaltfläche setzt die Felder des Dialoges auf die Standard-Werte zurück. In diesem Fall sollte aber auch die Methode initParmDefault entsprechenden Code enthalten.

public boolean showDefaultButton()
{
    boolean ret;
    ;
    // Enable/disable Default-Button
    ret = true;

    return ret;
}

Ob man dem Benutzer die Möglichkeit gibt, den Query mit Filterkriterien, Sortierung usw. zu erweitern kann man über die Methode showQuerySelectButton steuern. Diese legt fest, ob die Schaltfläche Auswählen im Dialog angezeigt wird.

boolean showQuerySelectButton()
{
    boolean ret;
    ;
    // User can change query
    ret = true;

    return ret;
}

In dieser Methode wird festgelegt, ob die Auswählen-Schaltfläche im Dialog angezeigt wird. Soll diese angezeigt werden, so muss übrigens auch die Methode queryRun überschrieben sein

public boolean showQueryValues()
{
    boolean ret;
    ;
    // Determines whether a Select button is to be added to the dialog. If you change the return value to true, the button will be added.
    ret = true;

    return ret;
}

Bestätigt der Benutzer den Dialog mit OK, so wird die validate-Methode aufgerufen. Hier können Dialogfelder bzw. deren Inhalt geprüft werden. Bevorzugterweise sollte man jedoch die Variablen der Instanz prüfen und nicht die Felder selbst. Auf diese Art und Weise kann man - so wie ich dies beinahe immer mache - die validate-Methode nochmals manuell in der run-Methode aufrufen, um vor der Abarbeitung der Klasse zu prüfen, ob alle notwendigen Parameter übergeben würden. Unabhängig davon, ob die Klasse über den Dialog (also über ein MenuItem) oder aus X++-Code heraus aufgerufen wurde.

public boolean validate(Object calledFrom)
{
    boolean ret;
    ;
    // Method is called from dialog, and manually called in run-method

    ret = super(calledFrom);

    if(ret && !this.parmFilename())
    {
        ret = checkFailed(strfmt("@SYS89866", "@SYS16423"));
    }

    if(ret && !this.queryRun())
    {
        ret = checkFailed(strfmt("@SYS89866", "@SYS25531"));
    }

    return ret;
}

Die construct-Methode muss überschrieben werden, um eine Instanz der Klasse bilden zu können.

public static Tutorial_ClassWithQueryRun construct()
{
    ;
    return new Tutorial_ClassWithQueryRun();
} 

Die Methode initParmDefault wird u.a. ausgeführt, wenn die Klasse erstmalig über ein MenuItem aufgerufen wird und es somit noch keine Nutzungsdaten gibt. Außerdem wird sie aufgerufen, wenn der Benutzer die Schaltfläche Standard betätigt.

Im Beispiel werden hier all jene Variablen, die der Benutzer auch im Dialog ggf. ändern kann, durch Standard-Werte befüllt. Wichtig ist der Aufruf der initQuery, die den zu verwendenden Query erstmalig initialisiert.

public void initParmDefault()
{
    #WINAPI
    ;
    super();

    filenameSave = WinAPI::getFolderPath(#CSIDL_DESKTOP) + #FilePathDelimiter + "exportFile.tsv";
    dialogDate   = systemdateget();

    // Build query/queryrun
    this.initQuery();
}

Die initQuery wird u.a. beim erstmaligen Aufruf der Klasse über ein Menuitem von der initParmDefault aufgerufen und generiert ein Query- sowie ein QueryRun-Objekt. Hier werden im Beispiel diverse QueryBuildRanges (Filter) aufgebaut. Auch die Sortierung des Queries kann hier festgelegt werden.

public Query initQuery()
{
    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);

    queryrun = new SysQueryRun(query);

    return query;
}

Die parm-Methoden werden u.a. dazu verwendet, um innerhalb einer Instanz der Klasse die Variablen abzufragen bzw. zu setzen.

public TransDate parmDialogDate(TransDate _dialogDate = dialogDate)
{
    ;
    dialogDate = _dialogDate;

    return dialogDate;
}
Filename parmFilename(FilenameSave _filename = filenameSave)
{
    ;
    filenameSave = _filename;

    return filenameSave;
}

  

In der Methode queryRun wird das in der initQuery aufgebaute queryRun-Objekt zurückgegeben.

public QueryRun queryRun()
{
    QueryRun ret;
    ;
    ret = queryrun;

    return ret;
}

Das eigentliche Herzstück der Klasse ist natürlich die run-Methode. Hier ist die eigentliche Logik zu implementieren. Im konkreten Fall wird nach der Prüfung der Parameter eine Datei erstellt. Danach wird der Query durchlaufen und bestimmte Werte werden in die Datei geschrieben. Der Benutzer wird währenddessen über eine Fortschrittsanzeige informiert, wie weit der Vorgang fortgeschritten ist.

public void run()
{
    CustInvoiceJour custInvoiceJour;
    #AviFiles
    ;
    super();

    setprefix(Tutorial_ClassWithQueryRun::description());

    // Check Parameters
    if(!this.validate(this))
    {
        return;
    }

    // Create/Open File
    if(this.isInBatch())
    {
        textIo = Tutorial_ClassWithQueryRun::createFileServer(this.parmFilename());
    }
    else
    {
        textIo = Tutorial_ClassWithQueryRun::createFileClient(this.parmFilename());
    } 

    // Lets go
    try
    {
        // Init progress
        this.progressInit(this.caption(),SysQuery::countTotal(this.queryRun()), #AviTransfer);

        while(this.queryRun().next())
        {
            custInvoiceJour = queryrun.get(tablenum(CustInvoiceJour));

            // Update progress
            this.parmProgress().incCount();

            // Write file
            textIo.write(custInvoiceJour.InvoiceAccount + #delimiterTab +
                         custInvoiceJour.InvoiceId      + #delimiterTab);
        }
    }
    catch (Exception::Error)
    {
        this.closeFile();
    }

    this.closeFile();

    info("@SYS92126");
}

Da es das Ziel der Klasse ist, eine Datei auf Basis eines Queries zu erstellen, muss diese Datei zu einem bestimmten Zeitpunkt erstellt werden. Dafür sind die Methoden createFileServer bzw. createFileClient zuständig.

public static server textio createFileServer(fileName _fileNameSave)
{
    fileIOPermission    fileIOPermission;
    textio              textio;
    #file
    ;
    fileIOPermission = new fileIOPermission(_fileNameSave, #io_write);
    fileIOPermission.assert();

    textIo = new TextIo(_fileNameSave, #io_write);
    if( !textIo)
    {
        throw error(strfmt("@SYS72245", _fileNameSave));
    }

    return textIo;
}
public static client textio createFileClient(fileName _fileNameSave)
{
    fileIOPermission    fileIOPermission;
    textio              textio;
    #file
    ;
    textIo = new TextIo(_fileNameSave, #io_write);
    if( !textIo)
    {
        throw error(strfmt("@SYS72245", _fileNameSave));
    }

    return textIo;
} 

Die Methode closeFile wird lediglich dazu verwendet, die Instanz des textIO zu entfernen.

private void closeFile()
{
    ;
    textIo = null;
}

Welcher Titel im Dialog angezeigt werden, kann mit Hilfe der Methode caption gesteuert werden.

public ClassDescription caption()
{
    ClassDescription ret;
    ;
    // Here goes the caption of the class (used ex. for dialog-title)
    ret = "Tutorial-Class";

    return ret;
}

Die Methode description sollte dazu verwendet werden, um dem Benutzer im Formular Stapelverarbeitungsliste eine Beschreibung des Stapelverarbeitungsauftrages zur Verfügung zu stellen.

static ClassDescription description()
{
    ;
    // Here goes a description of the class
    return "Tutorial for classes with queryRun and file-export";
}

Die Methode howToUseClass wird zu keinem Zeitpunkt automatisch aufgerufen, sie dient dem Entwickler lediglich als Information, wie die Klasse über X++ aufzurufen ist.

public static str howToUseClass()
{
    Tutorial_ClassWithQueryRun tutorial_ClassWithQueryRun;
    ;
    tutorial_ClassWithQueryRun = Tutorial_ClassWithQueryRun::construct();
    tutorial_ClassWithQueryRun.parmFilename(@'c:\temp\temp_123.tsv');
    tutorial_ClassWithQueryRun.parmDialogDate(systemdateget());
    tutorial_ClassWithQueryRun.initQuery();
    tutorial_ClassWithQueryRun.run();
}

Die oben beschriebene Klasse wurde in Dynamics AX 2009 entwickelt, sie läuft aber auch in Dynamics AX 4.0. 
Die Klasse steht hier als XPO-File zum Download zur Verfügung, Verwendung natürlich auf eigene Gefahr!

 

Dieser Beitrag bezieht sich auf die Versionen:
Dynamics AX 4.0, Dynamics AX 2009

 
 

 

 
 
 
Beiträge des aktuellen Monats
November 2024
MoDiMiDoFrSaSo
 123
45678910
11121314151617
18192021222324
252627282930 
 
© 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