<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/css" href="https://www.schweda.net/style_feed.css" ?>
<rss version="2.0" 
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
    xmlns:atom="http://www.w3.org/2005/Atom"	
	xmlns:dc="http://purl.org/dc/elements/1.1/" > 
<channel>
    <title>schweda.net - Blog</title>
    <link>https://www.schweda.net/</link>
    <description>schweda.net - Blog - Blog-Beitraege</description>
    <language>de-at</language>
    <copyright>Copyright 2006-2026</copyright>
    <generator>schweda.net</generator>
    <managingEditor>heinz.schweda@schweda.net (Heinz Schweda)</managingEditor>
    <webMaster>heinz.schweda@schweda.net (Heinz Schweda)</webMaster>
    <category>Blog</category>
	<atom:link href="https://schweda.net/blog_rss.php?bid=444" rel="self" type="application/rss+xml" />
<item>
<title><![CDATA[AX 2012: Beispiel für die Verwendung des SysOperation-Frameworks]]></title>
<description><![CDATA[
<p>Das <strong>SysOperation-Framework </strong>ist eine Neuerung in Dynamics AX 2012 und soll das RunBasebatch-Framework ersetzen. In der MSDN sind zahlreiche Dokumentationen und <a href="http://technet.microsoft.com/en-us/library/hh881828.aspx" target="_blank" title="Whitepaper">Whitepaper</a> &uuml;ber dieses Framework zu finden, welche ich unbedingt empfehle zu lesen.
</p>


<p>Der folgende Beitrag enth&auml;lt ein bewusst einfach gehaltenes Beispiel, wie man in AX&nbsp;2012 Programmlogik auf Basis dieses Framework einsetzen kann.
</p>


<p>Das Beispiel besteht dabei aus insgesamt vier Klassen:
</p>


<ul>
	
<li>Data Contract Class
</li>
	
<li>Service Class
</li>
	
<li>UIBuilder Class
</li>
	
<li>Controller Class
</li>

</ul>


<p>&nbsp;
</p>


<h2>Data Contract Class
</h2>


<p>Eine Data Contract Class besteht im Grunde genommen lediglich aus Accessor-Methoden (parm...-Methoden) und ist dadurch gekennzeichnet, da&szlig; in der <em>classDeclaration() </em>das Attribute <em>DataContractAttribute </em>verwendet wird.
</p>


<p>F&uuml;r alle Accessor-Methoden des Data Contracts werden vom SysOperation-Framework beim Aufruf entsprechende Dialogfelder generiert.
</p>


<p>DIe weiteren im Beispiel verwendeten Attribute <em>SysOperationContractProcessingAttribute </em>bzw. <em>SysOperationGroupAttribute </em>verkn&uuml;pfen zum Einen den Data Contract mit einem UI Builder und zum Anderen wird - neben der standardm&auml;ssig immer generierten Feldgruppe namens <em>Parameter </em>- eine weitere Feldgruppe mit dem internen Namen <em>DemoGroup</em>.
</p>


<pre class="pre_blog_axcode">
[
    DataContractAttribute
    ,SysOperationContractProcessingAttribute(classStr(TutorialSysOperationUIBuilder))
    ,SysOperationGroupAttribute(&#39;DemoGroup&#39;, &#39;For demonstration purpose only&#39;, &#39;2&#39;)
]
class TutorialSysOperationDataContract
    implements SysOperationValidatable,
               SysOperationInitializable
{
    str             qryStr;
    FilenameSave    filenameSave;
    TransDate       dialogDate;
    CustAccount     custAccount;
}
</pre>


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

<pre class="pre_blog_axcode">
[
    DataMemberAttribute
    ,SysOperationDisplayOrderAttribute(&#39;2&#39;)
    ,SysOperationGroupMemberAttribute(&#39;DemoGroup&#39;)
]
public CustAccount parmCustAccount(CustAccount _custAccount = custAccount)
{
    custAccount = _custAccount;
    return custAccount;
}
</pre>


<p>Die Accessor-Methode f&uuml;r das Datum unterscheidet sich im groben nur durch ein weiteres verwendetes Attribute <em>SysOperationLabelAttribute </em>mit Hilfe dessen das Label des Dialogfeldes festgelegt wird.
</p>


<p>Das hier anzugebende Datum ist im &uuml;brigen lediglich zu Demonstrationszwecken implementiert und hat keinen erw&auml;hnenswerten Einfluss auf die Programmlogik. &nbsp;
</p>


<pre class="pre_blog_axcode">
[
    DataMemberAttribute
    ,SysOperationDisplayOrderAttribute(&#39;3&#39;)
    ,SysOperationGroupMemberAttribute(&#39;DemoGroup&#39;)
    ,SysOperationLabelAttribute(literalStr(&quot;@SYS128676&quot;))
]
public TransDate parmDialogDate(TransDate _dialogDate = dialogDate)
{
    dialogDate = _dialogDate;
    return dialogDate;
}
</pre>


<p>In der&nbsp;Accessor-Methode f&uuml;r den&nbsp;Dateinamen&nbsp;wird &uuml;ber das Attribute <em>SysOperationDisplayOrderAttribute </em>festgelegt, da&szlig; das Feld das Erste im Dialog sein soll.
</p>


<pre class="pre_blog_axcode">
[
    DataMemberAttribute
    ,SysOperationDisplayOrderAttribute(&#39;1&#39;)
]
public FilenameSave parmFilenameSave(FilenameSave _filenameSave = filenameSave)
{
    filenameSave = _filenameSave;
    return filenameSave;
}
</pre>


<p>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&uuml;gung stehender Datentyp verwendet werden kann, muss ein Query beispielsweise in einen String umgewandelt und &uuml;bergeben werden. Das SysOperation-Framework stellt daf&uuml;r eigene Methoden zur Verf&uuml;gung.
</p>


<pre class="pre_blog_axcode">
[
    DataMemberAttribute
    ,AifQueryTypeAttribute(&#39;_qryStr&#39;, &#39;&#39;)
]
public str parmQuery(str _qryStr = qryStr)
{
    qryStr = _qryStr;
    return qryStr;
} 
</pre>


<p>Die Methode<em> validate() </em>steht nur zur Verf&uuml;gung, wenn in der <em>classDeclaration() </em>des Data Contracts die Klasse <em>SysOperationValidatable </em>eingebunden wurde. Dann funktioniert sie &auml;hnlich, wie im RunBaseBatch-Framework.
</p>


<pre class="pre_blog_axcode">
public boolean validate()
{
    boolean ret;
 

    ret = true;

    // Simple validation example
    if(this.parmDialogDate() &amp;&amp; this.parmDialogDate() &lt; systemDateGet())
    {
        ret = checkFailed(strFmt(&quot;&#39;%1&#39; has to be greater/equal than %2.&quot;, &quot;@SYS128676&quot;, systemDateGet()));
    }

    if(!this.parmFilenameSave())
    {
        ret = checkFailed(strFmt(&quot;Field &#39;%1&#39; must be filled in.&quot;,
                    extendedTypeId2pname(extendedTypeName2Id(identifierStr(FileNameSave)))
                    ));
    }
    return ret;
}
</pre>


<p>Die Methode<em> initialize() </em>steht nur zur Verf&uuml;gung, wenn in der <em>classDeclaration() </em>des Data Contracts die Klasse <em>SysOperationInitalize </em>eingebunden wurde. Diese Methode wird nur aufgerufen, wenn keine Nutzungsdaten vorhanden oder verwendet werden.
</p>


<pre class="pre_blog_axcode">
public void initialize()
{
    dialogDate = systemDateGet() - 365;
}
</pre>


<p>&nbsp;
</p>


<h2>Service Class
</h2>


<p>Diese - von <em>SysOperationServicebase </em>abgeleitete - Klasse, enth&auml;lt die eigentliche Logik des Beispiels.
</p>


<pre class="pre_blog_axcode">
class TutorialSysOperationService 
    extends SysOperationServiceBase
{
}
</pre>


<p>Genauergesagt ist die Logik in der Methode <em>runService() </em>enthalten. Die Besonderheit dieser Methode ist, da&szlig; sie als einzigen Parameter den Data Contract erh&auml;lt und da&szlig; das Attribute <em>SysEntryPointAttribute </em>gesetzt ist. Der Name der Methode hingegen bleibt dem Entwickler &uuml;berlassen, die Methode wird &uuml;ber den Namen in der Methode <em>newFromArgs() </em>des Service Controllers referenziert.
</p>


<p>Die im Beispiel enthaltene Methode enth&auml;lt keine aufregende Logik, sie erstellt ledigich eine einfache Text-Datei und schreibt in diese einige Werte aus den &uuml;ber den Query &uuml;bergebenen Ausgangsrechnungen.
</p>


<pre class="pre_blog_axcode">
[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(&#39;File creation failed.&#39;);
        }

        textIo.write(strFmt(&quot;--- %1 ---&quot;, _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(&quot;@SYS326740&quot;);
    }

    queryRun = new QueryRun(query);
    while(queryRun.next())
    {
        custInvoiceJour = queryRun.get(tableNum(CustInvoiceJour));

        if(textIo)
        {
            textIo.write(strFmt(&quot;Invoice: %1;CustAccount: %2;Currency: %3&quot;, custInvoiceJour.InvoiceId, custInvoiceJour.OrderAccount, custInvoiceJour.custTable_OrderAccount().Currency));
        }
    }
}
</pre>


<p>&nbsp;
</p>


<h2>Controller Class
</h2>


<p>Eine Controller Class muss von <em>SysOperationServiceController </em>abgeleitet sein.
</p>


<pre class="pre_blog_axcode">
class TutorialSysOperationServiceController 
    extends SysOperationServiceController
{
}
</pre>


<p>Die Methode <em>newFromArgs() </em>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 &uuml;ber die Methode <em>parmArgs() </em>des Frameworks der Aufrufer f&uuml;r einen evtl. sp&auml;teren notwendigen Zugriff gespeichert.
</p>


<pre class="pre_blog_axcode">
public static TutorialSysOperationServiceController newFromArgs(Args _args)
{
    TutorialSysOperationServiceController controller;

    controller = new TutorialSysOperationServiceController(classStr(TutorialSysOperationService), methodStr(TutorialSysOperationService, runService));
    controller.parmArgs(_args);

    return controller;
}
</pre>


<p>Die<em> initQuery() </em>dient dazu, den Query f&uuml;r den Dialog zu initalisieren. Diese Methode wird sp&auml;ter von der Methode<em> initializeServiceParameter() </em>aufgerufen.
</p>


<p>Erw&auml;hnenswert ist in Verbindung mit dem Query die Klasse <em>SysOperationHelper</em>, die u.a. Methoden zur Verf&uuml;gung stellt, um einen Query in einen String zu konvertieren und umgekehrt. Dies ist notwendig, da der Data Contract nur mit &quot;einfachen&quot; Datentypen arbeiten kann.
</p>


<pre class="pre_blog_axcode">
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(&quot;01.01.2010..&quot;);
    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;
}
</pre>


<p>Wie zuvor angek&uuml;ndigt, verwende ich die Methode <em>initializeServiceParameter()</em> um den Query des Dialoges &uuml;ber die Methode <em>initQuery()</em> zu initalisieren. Weiters wird an dieser Stelle ein weiterer Parameter mit Werten bef&uuml;llt.
</p>


<pre class="pre_blog_axcode">
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;
}
</pre>


<p>Die <em>main()</em>-Methode ist bereits aus dem RunBaseBatch-Framework hinreichend bekannt. Sie wird immer dann aufgerufen, wenn die Controller Class &uuml;ber ein MenuItem aufgerufen wird.
</p>


<p>In dieser Methode wird - &uuml;ber die oben erw&auml;hnte Methode <em>newFromArgs() </em>- die Klasse instanziiert. Weiters wird &uuml;ber <em>parmExecutionMode() </em>der Ausf&uuml;hrungsmodus festgelegt und schlie&szlig;lich &uuml;ber <em>startOperation() </em>der eigentliche Aufruf des Service gestartet.
</p>


<p>Diese Methode retouniert &uuml;brigens einen Enum-Wert vom Typ <em>SysOperationStartResult </em>mit Hilfe dessen man abfragen kann, ob das Service beispielsweise sofort oder im Stapel gestartet wurde oder ob der Dialog vom Benutzer abgebrochen wurde.
</p>


<pre class="pre_blog_axcode">
public static void main(Args _args)
{
    TutorialSysOperationServiceController   controller;
    TutorialSysOperationDataContract        dataContract;
    SysOperationStartResult                 sysOperationStartResult;

    if (!_args)
    {
        throw error(&quot;@SYS25407&quot;);
    }

    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(&quot;File %1 sucessfully written.&quot;, dataContract.parmFilenameSave()));
        }
    }
}
</pre>


<p>&nbsp;
</p>


<h2>UI Builder Class&nbsp;
</h2>


<p>Eine User Interface Builder Class ist dadurch gekennzeichnet, da&szlig; sie von <em>SysOperationAutomaticUIBuilder </em>abgeleitet ist. Eine solche Klasse kann dazu verwendet werden, den vom Framework automatisch generierten Dialog um eigene Logik zu erweitern.
</p>


<p>Die Verkn&uuml;pfung zwischen Data Contract und UI Builder erfolgt dabei &uuml;ber das Attribute <em>SysOperationContractProcessingAttribute </em>in der <em>classDeclaration() </em>des Data Contracts.
</p>


<pre class="pre_blog_axcode">
class TutorialSysOperationUIBuilder 
    extends SysOperationAutomaticUIBuilder
{
    DialogField df_custAccount;
}
</pre>


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

<pre class="pre_blog_axcode">
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();
}
</pre>


<p>Die Methode <em>postBuild() </em>ist ein idealer Platz, um die in der <em>classDeclaration() </em>deklarierte Variable vom Typ DialogField &uuml;ber <em>bindInfo().getDialogField() </em>mit Leben zu bef&uuml;llen.
</p>


<p>Auch ist die Methode gut dazu geeignet, die Eigenschaften von Dialogfelder abh&auml;ngig vom Aufrufer zu &uuml;bersteuern. Im Beispiel wird dazu die Methode<em> parmArgs() </em>des Service Controllers abgefragt.
</p>


<pre class="pre_blog_axcode">
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()          &amp;&amp;
       this.controller().parmArgs().record() &amp;&amp;
       this.controller().parmArgs().dataset() == tableNum(CustTable))
    {
        df_custAccount.value(this.controller().parmArgs().record().(fieldNum(CustTable, AccountNum)));
        df_custAccount.allowEdit(false);
    }
}
</pre>


<p>In der Methode <em>postRun() </em>kann man nun das Dialogfeld mit der oben beschriebenen Lookup-Methode verbinden.
</p>


<pre class="pre_blog_axcode">
public void postRun()
{
    super();

    // override methods
    df_custAccount.registerOverrideMethod(  methodStr(FormStringControl, lookup),
                                            methodStr(TutorialSysOperationUIBuilder, custAccount_lookup), this);
}
</pre>


<p>&nbsp;
</p>


<h2>Menu Item
</h2>


<p>Als letzten Schrittt gilt es nun ein MenuItem f&uuml;r die Service Controller Class zu erstellen.&nbsp;Ruft&nbsp;man dieses&nbsp;MenuItem auf, so sollte sich das oben beschriebene Beispiel wie folgt im Client darstellen:
</p>


<p><a href="http://www.schweda.net/pictures/blogpics/ax2012_tutorialSysOperationDialog.jpg" rel="lightbox" target="_blank"><img alt="Screenshot" height="218" src="http://www.schweda.net/pictures/blogpics/tb_ax2012_tutorialSysOperationDialog.jpg" title="Screenshot" width="465" /></a>
</p>]]></description>
<category>Microsoft Dynamics AX (Axapta)</category>
<pubDate>Sun, 04 Nov 2012 18:48:00 +0100</pubDate>
<link>https://www.schweda.net/blog_ax.php?bid=444</link>
<comments>https://www.schweda.net/blog_ax.php?bid=444</comments>
<guid isPermaLink="true">https://www.schweda.net/blog_ax.php?bid=444</guid>
<author>heinz.schweda@schweda.net (Heinz Schweda)</author>
<wfw:commentRss>https://www.schweda.net/blog_ax.php?bid=444</wfw:commentRss>
</item>
<item>
<title>Kommentar von Martin Pfister</title>
<description><![CDATA[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?]]></description>
<category>Microsoft Dynamics AX (Axapta)</category>
<pubDate>Sat, 13 Apr 2013 09:32:00 +0200</pubDate>
<link>https://www.schweda.net/blog_ax.php?bid=444</link>
<guid isPermaLink="true">https://www.schweda.net/blog_ax.php?bid=444</guid>
<author>Martin Pfister</author>
<dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Heinz Schweda</dc:creator>
<wfw:commentRss>https://www.schweda.net/blog_ax.php?bid=444</wfw:commentRss>
</item>
<item>
<title>Kommentar von Heinz Schweda</title>
<description><![CDATA[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.]]></description>
<category></category>
<pubDate>Tue, 16 Apr 2013 21:00:00 +0200</pubDate>
<link>https://www.schweda.net/blog.php?bid=444</link>
<guid isPermaLink="true">https://www.schweda.net/blog.php?bid=444</guid>
<author>Heinz Schweda</author>
<dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Heinz Schweda</dc:creator>
<wfw:commentRss>https://www.schweda.net/blog.php?bid=444</wfw:commentRss>
</item>
</channel>
</rss>	
