Bojensen Blogs

RunBase Framework Extension – Inside Microsoft Dynamics Ax 4

 

RunBase Framework Extension

Use the RunBase framework throughout Dynamics AX whenever you must execute a business transaction job. Extending the RunBase framework allows you to implement business operations that do not have default support in the Dynamics AX application. The RunBase framework supplies many features, including dialog boxes, query windows, validation-before-execution windows, the progress bar, client/server optimization, pack-unpack with versioning, and optional scheduled batch execution at a given date and time.

Inheritance in the RunBase Framework

Classes that use the RunBase framework must inherit from either the RunBase class or the RunBaseBatch class. If the class extends RunBaseBatch, it can be enabled for scheduled execution in batch mode.

In a good inheritance model, each class has a public construction mechanism, unless the class is abstract. If initialization of the class is not required, use a static construct method. Because X++ does not support method name overloading, you should use a static new method if the class must be initialized further upon instantiation. Static new methods have the following characteristics:

  • They are public and static.

  • Their names are prefixed with new.

  • They are named logically or with the arguments that they take. Examples include newInventTrans and newInventMovement.

  • They usually take non-default parameters only.

  • They always return a valid object of the class type, instantiated and initialized, or throw an error.

Note

A class can have several new methods with different parameter profiles. The NumberSeq class is an example of a class with multiple new methods.

The default constructor (the new method) should be protected to force users of the class to instantiate and initialize it with the static construct or new method. If new has some extra initialization logic that is always executed, you should place it in a separate init method.

To ease the task of writing customizations, the best practice is to add construction functionality for new subclasses (in higher layers) without mixing code with the construct method in the original layer.

The Property Method Pattern

To allow other business operations to run your new business operation, you might want to run it without presenting the user with any dialog boxes. If you do this, you will need an alternative to dialog box to set the values of the necessary member variables of your business operation class.

In Dynamics AX classes, member variables are always protected. In other words, they cannot be accessed outside of the class; they can be accessed only from within objects of the class itself or its subclasses. To access member variables from outside of the class, you must write accessor methods. The accessor methods can get, set, or both get and set member variable values.

A Dynamics AX best practice is to not use separate get and set accessor methods. The accessor methods are combined into a single accessor method, handling both get and set, in a pattern called the property method pattern. Accessor methods should have the same name as the member variable that they access, prefixed with parm.

The following is an example of what a method implementing the property method pattern could look like.

[View full width]

public NoYesId parmCreateServiceOrders(NoYesId _createServiceOrders = createServiceOrders) { ; createServiceOrders = _createServiceOrders; return createServiceOrders; }

If you want the method to work only as a get method, change it to something such as this.

 public NoYesId parmCreateServiceOrders()
{
;
    return createServiceOrders;
}

And if you want the method to work only as a set method, change it to this.

[View full width]

public void parmCreateServiceOrders(NoYesId _createServiceOrders = createServiceOrders) { ; createServiceOrders = _createServiceOrders; }

When member variables contain huge amounts of data (such as large containers or memo fields), the technique in the following example is recommended. This technique determines whether the parameter is changed. The disadvantage of using this technique in all cases is the overhead of an additional method call.

public container parmCode(container _code = conNull())
{
;
    if (!prmIsDefault(_code)
    {
        code = _code;
    }

    return code;
}

Tip

From the X++ editor window, you can access a template script to help you create parm methods. Right-click the editor window, point to Scripts, point to Template, point to Method, and then click Parm. A dialog box appears in which you must enter the variable type and name of the member variable that you want the parm method to give access to. You can also access the script by pressing Shift+F10 in the editor window and then selecting Scripts.

The Pack-Unpack Pattern

When you want to save the state of an object with the option to reinstantiate the same object later, you must use the pack-unpack pattern. The RunBase framework requires that you implement this pattern to switch the class between client and server (for client/server optimization) and to present the user with a dialog box that states the choices made at the last execution of the class.

The pattern consists of a pack method and an unpack method. These methods are used by the SysLastValue framework, which stores and retrieves user settings or usage data values that persist between processes.

Note

A reinstantiated object is not the same object as the saved object. It is a copy of the object with the same values as the packed and unpacked member variables.

The pack and unpack Methods

The pack method must be able to read the state of the object and return it in a container. Reading the state of the object involves reading the values of the variables needed to hydrate and dehydrate the object. Variables used at execution time that are declared as member variables do not have to be included in the pack method. The first entry in the container must be a version number that identifies the version of the saved structure. The following is an example of the pack method.

container pack()
{
;
    return [#CurrentVersion, #CurrentList];
}

The macros must be defined in the class declaration. CurrentList is a macro defined in the ClassDeclaration holding a list of the member variables to pack. If the variables in the CurrentList macro are changed, the version number should also be changed to allow safe and versioned unpacking. The unpack method can support unpacking previous versions of the class, as shown in the following example.

[View full width]

class InventCostClosing extends RunBaseBatch { #define.maxCommitCount(25) // Parameters TransDate transDate; InventAdjustmentSpec specification; NoYes prodJournal; NoYes updateLedger; NoYes cancelRecalculation; NoYes runRecalculation; FreeTxt freeTxt; Integer maxIterations; CostAmount minTransferValue; InventAdjustmentType adjustmentType; InventCostMinSettlePct minSettlePct; InventCostMinSettleValue minSettleValue; ... #DEFINE.CurrentVersion(2) #LOCALMACRO.CurrentList TransDate, Specification, ProdJournal, UpdateLedger, FreeTxt, MaxIterations, MinTransferValue, adjustmentType, minSettlePct, minSettleValue, cancelRecalculation, runRecalculation, collapseGroups #ENDMACRO } public boolean unpack(container packedClass) { #LOCALMACRO.Version1List TransDate, Specification, ProdJournal, UpdateLedger, FreeTxt, MaxIterations, MinTransferValue, adjustmentType, minSettlePct, minSettleValue #ENDMACRO boolean _ret; Integer _version = conpeek (packedClass,1); switch (_version) { case #CurrentVersion: [_version, #CurrentList] = packedClass; _ret = true; break; case 1: [_version, #Version1List] = packedClass; cancelRecalculation = NoYes::Yes; runRecalculation = NoYes::No; _ret = true; break; default: _ret = false; } return _ret; }

If any member variable is not packable, the class cannot be packed and reinstantiated to the same state. If any of the members are other classes, records, cursors, or temporary tables, they must also be made packable. Other classes that do not extend RunBase may implement the pack and unpack methods by implementing the SysPackable interface.

When the object is reinstantiated, it must be possible to call the unpack method, which reads the saved state and reapplies the values of the member variables. The unpack method can reapply the correct set of member variables according to the saved version number, as shown in this example.

public boolean unpack(container _packedClass)
{
    Version    version = conpeek(_packedClass, 1);
;
    switch (version)
    {
        case #CurrentVersion:
            [version, #CurrentList] = _packedClass;
            break;

        default:
            return false;
    }
    return true;
}

The unpack method returns a Boolean value that indicates whether the initialization was a success.

Bike-Tuning Service Offers Example

In this section, you will create an extension of the RunBase class to send bike-tuning service offers to customers via e-mail. Each bike-tuning offer could result in the creation of a service order transaction. To follow this example, you must have created an extended data type and a number sequence for bike-tuning service orders, as described in Chapter 6, "Customizing Microsoft Dynamics AX."

Note

To send e-mail messages, you must first set up the e-mail parameters in Dynamics AX. You access the e-mail parameters from AdministrationSetupE-Mail Parameters. To run the example without sending e-mail messages, omit the bits that use the SysMailer class.

Creating the Labels

Start by creating the labels that you need. Open the Label Editor from ToolsDevelopment ToolsLabelLabel Editor. The label numbers that appear in the Label Editor depend on your existing labels and the choice of label file. This example refers to the labels as @USR9, @USR10, and @USR11. Press Ctrl+N to create the labels shown in Table 7-2.

Table 7-2. Bike-Tuning Label Numbers and Text

Label number

Text

@USR9

Bike-tuning offers.

@USR10

Create bike-tuning offers

@USR11

Send bike-tuning offers to existing customers via e-mail.

Take note of the label numbers you are given so that you can use them in your code if you have label numbers other than those listed in the table.

Creating the Table

To store information about the generated service orders, a simple table with only two fields must be created. If you are not confident in your ability to create new tables, the Microsoft Dynamics AX SDK offers detailed table creation information.

The table must be created with the following properties.

Name

BikeServiceOrderTable

Label

@SYS79051 The label reads "Service Orders."

Add two fields to identify the service order and the customer. The fields must have the following properties.

Name

CustAccount

ExtendedDataType

CustAccount

Name

BikeServiceOrderId

ExtendedDataType

BikeServiceOrderId

Finally, add an index with the following properties to the table.

Name

ServiceOrderIdx

AllowDuplicates

No

DataField

BikeServiceOrderId

Creating the Class

Now you can begin to create the business transaction class itself. Create a new class that extends the RunBase class, as shown in this example.

public class BikeTuningOffers extends RunBase
{
}

Implement the two abstract pack and unpack methods of RunBase. For now, you will make a very simple implementation to be able to compile the class. You will make the final implementation with the correct class members later. Insert to-do comments in the code, as shown in the following example, so that compile log messages will remind you to revisit the methods.

public container pack()
{
;
    //TODO Make the final implementation.
    return conNull();
}

public boolean unpack(container _packedClass)
{
;
    //TODO Make the final implementation.
    return true;
}

To enable the example for execution, you must implement the run method. Because it is too early to add the business operation, you will implement an empty method, as shown here.

public void run()
{

}

Implementing the Class Description

You must implement a static method that returns a description of what the class does. This method sets the title of the dialog box, and it can also be used for different kinds of user interface presentations on the class. The description method must effectively be executed on the tier from which it is called, so define it as client server. Use one of the labels created earlier, as shown in this example.

client server static ClassDescription description()
{
;
    return "@USR9";
}

Implementing Constructors

Next, you create a custom static constructor as shown here.

public static BikeTuningOffers construct()
{
    BikeTuningOffers    bikeTuningOffers;
    ;
    bikeTuningOffers = new BikeTuningOffers();

    return bikeTuningOffers;
}

To force users of the class to use your constructor, rather than the default constructor (new), make the default constructor protected. Right-click the class, point to Override Method, click new, and change the method as shown here.

protected void new()
{
;
    super();
}

To enable your job to run from a menu item, you must create the static constructor that is called by the menu item that you will eventually create. This is the method with the name main, and it should look like this.

public static void main(Args args)
{
    BikeTuningOffers    bikeTuningOffers;
    ;

    bikeTuningOffers = BikeTuningOffers::construct();

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

In the main method, you call the prompt method of the framework. This method opens the user dialog box. It returns true if the user clicks OK and the values entered are free of errors. The run method of the framework starts the actual job.

Implementing a User Dialog Box

The user dialog box should allow the user to choose whether to create service orders automatically for each bike-tuning offer sent to customers via e-mail. To make this option available, you must have two global member variables in the class declaration. One is the dialog box field object shown in the dialog box, and the other is a variable used to store the value entered in the dialog box field. The changed class declaration looks like this.

public class BikeTuningOffers extends RunBase
{
    DialogField dialogCreateServiceOrders;

    NoYesId     createServiceOrders;
}

The RunBase framework sets up the basic dialog box by using the dialog framework, so you must add your dialog box field to the dialog box by overriding the dialog method. The following code sample displays what the system gives you when you override the dialog method.

[View full width]

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

Rewrite this code as shown here so that it is more readable and follows the general pattern for the method.

protected Object dialog()
{
    DialogRunBase   dialog;
    ;

    dialog = super();

    return dialog;
}

Now add your field to the dialog box, as shown in the following code. Dialog box fields are objects of the DialogField class.

[View full width]

protected Object dialog() { DialogRunBase dialog; ; dialog = super(); dialogCreateServiceOrders = dialog.addField (typeId(NoYesId), "@SYS79091", "@SYS84386"); return dialog; }

To use the values entered in the dialog box, you must retrieve them from the dialog box fields and store them in member variables. When the user clicks OK or Cancel, the framework calls the getFromDialog method to retrieve and save the values. Implement an override of this method as follows.

[View full width]

public boolean getFromDialog() { boolean ret; ; ret = super(); createServiceOrders = dialogCreateServiceOrders.value(); return ret; }

When the user clicks OK, the framework calls the validate method. Although further validation is not necessary for this example, the following code shows how to implement an override that prevents the user from running the job without selecting the Create Service Orders check box.

[View full width]

public boolean validate() { boolean ret; ; ret = super(); if (ret && createServiceOrders == NoYes::No) { ret = checkFailed("You cannot run the job without creating service orders."); } return ret; }

You can view the user dialog box, shown in Figure 7-12, by opening the class. Right-click the class in the AOT, and then click Open.

Figure 7-12. The Create Bike-Tuning Offers dialog box.

Implementing the run Method

You can now write the sendOffers method that contains your business operation as follows.

[View full width]

private void sendOffers() { CustTable custTable; BikeServiceOrderId bikeServiceOrderId; BikeServiceOrderTable bikeServiceOrderTable; SysMailer sysMailer; ; sysMailer = new SysMailer(); ttsBegin; while select custTable { if (createServiceOrders) { bikeServiceOrderId = NumberSeq::newGetNum(SalesParameters: :numRefBikeServiceOrderId()).num(); bikeServiceOrderTable .BikeServiceOrderId = bikeServiceOrderId; bikeServiceOrderTable.CustAccount = custTable.AccountNum; bikeServiceOrderTable.insert(); } sysMailer.quickSend(CompanyInfo::find().Email, custTable.Email, "Tune your bike", strFmt("Hi %1,nnIt's time to tune your bike...", custTable.name)); } ttsCommit; }

To call the method, you must add it to the run method, which, as you might remember, is called from the value main if the user clicks OK in the dialog box and the values pass validation. The run method follows a specific pattern, as shown here.

[View full width]

public void run() { #OCCRetryCount ; if (! this.validate()) throw error(""); try { ttsbegin; // Place the code that carries out the actual business transaction here. ttscommit; } catch (Exception::Deadlock) { retry; } catch (Exception::UpdateConflict) { if (appl.ttsLevel() == 0) { if (xSession::currentRetryCount() >= #RetryNum) { throw Exception: :UpdateConflictNotRecovered; } else { retry; } } else { throw Exception::UpdateConflict; } } }

This pattern ensures that the transaction is carried out within the scope of a database transaction and that the execution can recover from a deadlock or update conflict in the database. The run method calls validation again because someone may call run without showing the dialog box. In run, an error is thrown to completely stop the execution if validation fails. (Using the class without showing the dialog box is discussed later in this section.) When you add the call to the sendOffers method that holds your business operation, the run method looks like this.

[View full width]

public void run() { #OCCRetryCount ; if (! this.validate()) throw error(""); try { ttsbegin; this.sendOffers(); ttscommit; } catch (Exception::Deadlock) { retry; } catch (Exception::UpdateConflict) { if (appl.ttsLevel() == 0) { if (xSession::currentRetryCount() >= #RetryNum) { throw Exception: :UpdateConflictNotRecovered; } else { retry; } } else { throw Exception::UpdateConflict; } } }

Implementing the pack and unpack Methods

Now is a good time to revisit the pack and unpack methods. Start in the class declaration by setting up the member variables that you want to store. In this example, you store the createServiceOrders variable. State the version number of the current set of member variables. The version number allows you to add new member variables later and still retrieve the old settings from the last execution of the operation. Also, you can specify the version number to be treated as the first version of the member variable list in the #Version1 declaration. This allows you to treat another version as the first version, which you might choose to do if you simply want to ignore a range of older versions. The first version is typically version 1.

public class BikeTuningOffers extends RunBase
{
    DialogField dialogCreateServiceOrders;

    NoYesId     createServiceOrders;

    #define.CurrentVersion(1)
    #define.version1(1)
    #localmacro.CurrentList
        createServiceOrders
    #endmacro
}

When more variables are stored in the #CurrentList macro, separate each variable by a comma.

The pack method must be changed to follow this specific pattern.

public container pack()
{
;
    return [#CurrentVersion, #CurrentList];
}

The unpack method must be changed to follow this pattern.

[View full width]

public boolean unpack(container _packedClass) { Version version = runbase::getVersion (_packedClass); ; switch (version) { case #CurrentVersion: [version, #CurrentList] = _packedClass; break; default: return false; } return true; }

You must also make the following change to your implementation of the dialog method to show the old values in the dialog box fields.

[View full width]

protected Object dialog() { DialogRunBase dialog; ; dialog = super(); dialogCreateServiceOrders = dialog .addFieldValue(typeId(NoYesId), createServiceOrders, "@SYS79091", "@SYS84386"); return dialog; }

Notice that you call the addFieldValue method rather than the addField method. The addFieldValue method allows you to pass a default value to the dialog box field. The RunBase framework ensures that the variable is set to the value saved in the SysLastValue framework at this point in time.

Creating a Menu Item

To make the operation available from the main menu and the Navigation Pane, you must create a menu item for the operation. The menu item must be attached to a configuration key and a security key.

To create a new configuration key, open the AOT and expand Data Dictionary, right-click Configuration Keys, and then select New Configuration Key. Right-click the new configuration key and select Properties to open the property sheet. Change the name to BikeTuningOffers, and add the label number @USR9 to the Label field. The label should read "Bike-tuning offers." If you want to make the configuration dependent on another configuration key, you should fill in the ParentKey property. For this example, make the configuration key dependent on the Quotation configuration key by entering QuotationBasic in the ParentKey property field.

The security key property for the menu item should be chosen from the existing security keys. The chosen security key must match the position of the menu item on the main menu or in the Navigation Pane. For example, if you wanted to put your menu item under Accounts ReceivablePeriodic, the security key must be CustPeriodic.

With the configuration and security keys in place, you are ready to create the menu item. In the AOT, expand Menu Items, right-click Action, and then select New Menu Item. Right-click the new menu item, and then select Properties. Fill out the properties as described in the Table 7-3.

Table 7-3. Bike-Tuning Menu Item Properties

Property

Value

Explanation

Name

BikeTuningOffers

This is the name of the menu item as it appears in the AOT.

Label

@USR10

The label should read, "Create bike-tuning offers."

HelpText

@USR11

The label should read, "Send bike-tuning offers to existing customers via e-mail."

ObjectType

Class

This is the type of object opened by the menu item.

Object

BikeTuningOffers

This is the name of the object opened by the object.

RunOn

Server

Execute the job on the server tier.

ConfigurationKey

BikeTuningOffers

This is the new configuration key that you just created.

SecurityKey

CustPeriodic

This is the security key chosen according to the position of the menu item on the main menu or in the Navigation Pane.

Tip

You can drag the class node in the AOT onto the Action node under Menu Items to create a new menu item with the same name as the class and the ObjectType and Object properties already defined.

Now add the menu item to the Accounts Receivable submenu. In the AOT, expand Menus, right-click Cust, point to New, and then click Menu Item. Right-click the new menu item, and then select the Properties tab. Change Name to BikeTuningOffers. Change MenuItemType to Action and MenuItemName to BikeTuningOffers. Finally, move the menu item to the Periodic folder of the menu. Save the menu, and then restart the Dynamics AX client to make the new menu item appear in the Navigation Pane and on the Dynamics AX main menu.

Adding Property Methods

Suppose that you want to run the Bike-Tuning Offers business operation directly from another piece of code without presenting the user with a dialog box. To do so, you must implement property methods according to the property method pattern. This pattern allows you to set and get the properties that would otherwise be inaccessible because member variables in Dynamics AX are protected.

Start by writing a parm method for the property as follows.

[View full width]

public NoYesId parmCreateServiceOrders(NoYesId _createServiceOrders = createServiceOrders) { ; createServiceOrders = _createServiceOrders; return createServiceOrders; }

This job demonstrates how you can run the operation without showing the dialog box.

[View full width]

static void createBikeTuningOffersJob(Args _args) { BikeTuningOffers bikeTuningOffers; ; bikeTuningOffers = BikeTuningOffers::construct(); bikeTuningOffers.parmCreateServiceOrders(NoYes ::Yes); bikeTuningOffers.run(); }

Adding Constructors

As mentioned earlier in this chapter, X++ does not support method name overloading, and you should avoid using default parameters on constructors. You must create individually named new methods with different parameter profiles instead.

In the preceding example, you created an instance of the class and set the necessary parameters. Imagine that there is one more parameter in your class that indicates a certain customer account number for creating bike offers. Add a new member variable to the class declaration, and then add the new parameter method, like this.

[View full width]

public class BikeTuningOffers extends RunBase { DialogField dialogCreateServiceOrders; NoYesId createServiceOrders; CustAccount custAccount; #define.CurrentVersion(1) #define.version1(1) #localmacro.CurrentList createServiceOrders #endmacro } public CustAccount parmCustAccount(CustAccount _custAccount = custAccount) { ; custAccount = _custAccount; return custAccount; }

Suppose that the customer record contained information about the option to create service orders with bike offers. For example, imagine that offers are not sent to the customer if the customer has been stopped for new transactions. Because you want to avoid using default parameters in the construct method, you must call both of these parm methods when you create an instance based on a customer record.

Running the business operation from a job with a specific customer would look like this.

[View full width]

server static void createBikeTuningOffersJobCustomer(Args _args) { CustTable custTable = CustTable: :find('4001'); BikeTuningOffers bikeTuningOffers; ; bikeTuningOffers = BikeTuningOffers::construct(); bikeTuningOffers.initParmDefault(); bikeTuningOffers.parmCustAccount(custTable .accountNum); bikeTuningOffers.parmCreateServiceOrders (custTable.blocked == CustVendorBlocked::No); bikeTuningOffers.run(); }

This code is a good candidate for the static new pattern, so implement a static newCustTable method on the BikeTuningOffers class to create an instance based on a customer record, as shown here.

[View full width]

server static public BikeTuningOffers newCustTable (CustTable _custTable) { BikeTuningOffers bikeTuningOffers; ; bikeTuningOffers = BikeTuningOffers::construct(); bikeTuningOffers.initParmDefault(); bikeTuningOffers.parmCustAccount(_custTable .accountNum); bikeTuningOffers.parmCreateServiceOrders (_custTable.blocked == CustVendorBlocked:: No); return biketuningOffers; }

Now change your job to a simpler version to be assured that the class gets properly instantiated and initialized.

[View full width]

server static void createBikeTuningOffersJobCustomer(Args _args) { CustTable custTable = CustTable: :find('4001'); BikeTuningOffers bikeTuningOffers; ; bikeTuningOffers = BikeTuningOffers: :newCustTable(custTable); bikeTuningOffers.run(); }

Adding a Query

Adding a query to the business operation class allows the user to select a range of targets to apply the operation to, such as sending bike-tuning offers to selected customers. To use the query, you must be able to create an instance of QueryRun. Start by adding QueryRun as a member variable, as shown here.

[View full width]

public class BikeTuningOffers extends RunBase { DialogField dialogCreateServiceOrders; NoYesId createServiceOrders; CustAccount custAccount; // This member won't be used with the query. QueryRun queryRun; #define.CurrentVersion(2) #define.version1(1) #localmacro.CurrentList createServiceOrders #endmacro }

To initialize the QueryRun object, override the initParmDefault method, as shown in the following code. This method is called by the RunBase framework if no saved object state is found by the SysLastValue framework via the unpack method.

public void initParmDefault()
{
    Query   query;
    ;

    super();

    query = new Query();
    query.addDataSource(tableNum(CustTable));

    queryRun = new QueryRun(query);
}

You must modify the pack method, as shown in the following example, so that you can save the state of the QueryRun object.

[View full width]

public container pack() { ; return [#CurrentVersion, #CurrentList, queryRun.pack()]; }

Consequently, you must also modify the unpack method to reinstantiate the QueryRun object, as shown here.

[View full width]

public boolean unpack(container _packedClass) { Version version = runbase::getVersion (_packedClass); Container packedQuery; ; switch (version) { case #CurrentVersion: [version, #CurrentList, packedQuery] = _packedClass; if (packedQuery) queryRun = new QueryRun(packedQuery); break; default: return false; } return true; }

To make the QueryRun object available for presentation in the dialog box, override the queryRun method to return your QueryRun object, as shown in the following code.

public QueryRun queryRun()
{
;
    return queryRun;
}

To actually show the query in the dialog box, you must override the showQueryValues method to return the value true, as follows.

boolean showQueryValues()
{
;
    return true;
}

If you open the class now, you can see that the query is embedded in the dialog box, as shown in Figure 7-13.

Figure 7-13. The Create Bike-Tuning Offers dialog box with an embedded query.

[View full size image]

Finally, you must change your business logic method, sendOffers, so that it uses the QueryRun object, as shown here.

[View full width]

private void sendOffers() { CustTable custTable; BikeServiceOrderId bikeServiceOrderId; BikeServiceOrderTable bikeServiceOrderTable; SysMailer sysMailer; ; sysMailer = new SysMailer(); ttsBegin; while (queryRun.next()) { custTable = queryRun.get(tableNum(CustTable)); if (createServiceOrders) { bikeServiceOrderId = NumberSeq: :newGetNum(SalesParameters:: numRefBikeServiceOrderId()).num(); bikeServiceOrderTable .BikeServiceOrderId = bikeServiceOrderId; bikeServiceOrderTable.CustAccount = custTable.AccountNum; bikeServiceOrderTable.insert(); } sysMailer.quickSend(CompanyInfo::find().Email, custTable.Email, "Tune your bike", strFmt("Hi %1,nnIt's time to tune your bike...", custTable.name)); } ttsCommit; }

Client/Server Considerations

Typically, you will want to execute business operation jobs on the server tier, because these jobs almost always involve several database transactions. However, you want the user dialog box to be executed on the client to minimize client/server calls from the server tier. Fortunately, the RunBase framework can help you run the dialog box on the client and the business operation on the server tier.

To run the business operation job on the server and push the dialog box to the client, you should be aware of two settings. On the menu item that calls the job, ypou must set the RunOn property to Server; on the class, you must set the RunOn property to Called from. Figure 7-14 shows where to set the RunOn property of a class.

Figure 7-14. The execution tier of the class set to Called from.

When the job is initiated, it starts on the server, and the RunBase framework packs the internal member variables and creates a new instance on the client, which then unpacks the internal member variables and runs the dialog box. When the user clicks OK in the dialog box, RunBase packs the internal member variables of the client instance and unpacks them again in the server instance.

RunBase Framework Extension –Inside Microsoft Dynamics Ax 4 在线阅读-秋毫ERP咨询论坛

Comments are closed.