Commonly Implemented Features in SysOperation and RunBase in Ax 2012

Description:- SysOperation is a framework in Microsoft Dynamics® AX 2012 that allows application logic to be written in a way that supports running operations interactively or via the Microsoft Dynamics AX batch server. The framework provides capabilities that are very similar to the RunBase framework that came before it. The batch framework has very specific requirements for defining operations:

1.       The operation must support parameter serialization so that its parameters can be saved to the batch table.
2.       The operation must have a way to display a user interface that can be launched from the batch job configuration user interface.
3.       The operation must implement the interfaces needed for integration with the batch server runtime.

The RunBase framework defines coding patterns that implement these requirements. The SysOperation framework provides base implementations for many of the patterns defined by the RunBase framework.
SysOperation and RunBase are frameworks geared toward building operations that can run via the batch server or interactively. In order for an operation to run via the batch server, it must meet these requirements:

1.       It must support parameter serialization via the SysPackable interface.
2.       It must support the standard run () method defined in the BatchRunable interface.
3.       It must support the batch server integration methods found in the Batchable interface.
4.       It must provide a mechanism to show input parameters with a user interface.

Currently in Microsoft Dynamics AX, all operations that must run via the batch server must derive from either the SysOperationController or the RunBaseBatch base class.
The following two samples illustrate the basic capabilities provided by the two frameworks:

1.       SysOpSampleSimpleRunbaseBatch
2.       SysOpSampleSimpleController

The simplest operation based on the RunBaseBatch base class has to implement 12 overrides. The purpose of this sample is simply to compare the RunBase and SysOperation frameworks. For full details of the RunBase framework, see the following article on MSDN: RunBaseBatch FrameWork

There are four classes: the controller, the service operation, the data contract, and the user interface builder.

let’s Start to Create Class in Ax Classes node and Name it “SysOpSampleSimpleRunbaseBatch”.

ClassDeclaration
  1. Derives from RunBaseBatch.
  2. Declares variables for operation input parameters.
  3. Declares variables for dialog box controls.
  4. Declares a macro defining a list of variables that need to be serialized.
class SysOpSampleSimpleRunbaseBatch extends RunBaseBatch
{
    str text;
    int number;
    DialogRunbase       dialog;

    DialogField numberField;
    DialogField textField;

    #define.lookupAlways(2)
    #define.CurrentVersion(1)

    #LOCALMACRO.CurrentList
        text,
        number
    #ENDMACRO
}

protected Object dialog()
{
    dialog = super();
    textField = dialog.addFieldValue(IdentifierStr(Description255), text, 'Text Property','Type some text here');
    textField.lookupButton(#lookupAlways);

    numberField = dialog.addFieldValue(IdentifierStr(Counter), text, 'Number Property','Type some number here');
    return dialog;
}

public void dialogPostRun(DialogRunbase _dialog)
{
    FormControl control;
    super(_dialog);

    // register overrides for form control events
    numberField.registerOverrideMethod(methodstr(FormIntControl, validate), methodstr(SysOpSampleSimpleRunbaseBatch, numberFieldValidate), this);
    textField.registerOverrideMethod(methodstr(FormStringControl, lookup), methodstr(SysOpSampleSimpleRunbaseBatch, textFieldLookup), this);

}

public boolean getFromDialog()
{
    text = textField.value();
    number = numberField.value();

    return super();
}

public boolean numberFieldValidate(FormIntControl _control)
{
    if (_control.value() < 0)
    {
        error('Please type a number >= 0');
        return false;
    }
    return true;
}

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

public int parmNumber(int _number = number)
{
    number = _number;
    return number;
}

public str parmText(str _text = text)
{
    text = _text;
    return text;
}

protected void putToDialog()
{
    super();
    textField.value(text);
    numberField.value(number);
}

public void run()
{
    // marshall call to server
    SysOpSampleSimpleRunbaseBatch::showTextInInfolog(this.pack());
}

public void  textFieldLookup(FormStringControl _control)
{
    FormStringControl       companyControl;
    SysTableLookup          tableLookup;
    Query                   query = new Query();

    companyControl = _control;
    tableLookup = SysTableLookup::newParameters(tablenum(DataArea),companyControl);
    tableLookup.addLookupfield(fieldnum(DataArea,Id),true);
    tableLookup.addLookupfield(fieldnum(DataArea,Name),false);

    query.addDataSource(tablenum(DataArea));
    tableLookup.parmQuery(query);
    tableLookup.performFormLookup();
}

public boolean unpack(container packedClass)
{
    Integer         version         = conPeek(packedClass,1);

    switch (version)
    {
        case #CurrentVersion:
            [version,#CurrentList] = packedClass;
            break;
        default:
            return false;
    }
    return true;
}

public static ClassDescription description()
{
    return 'Intermediate RunBaseBatch Sample';
}

public static void main(Args args)
{
    SysOpSampleSimpleRunbaseBatch operation;

    operation = new SysOpSampleSimpleRunbaseBatch();
    if (operation.prompt())
    {
        operation.run();
    }
}

private static server void showTextInInfolog(container packedRunBase)
{
    SysOpSampleSimpleRunbaseBatch thisClass;

    // If not a CLR session then marshall over. If already in a CLR session
    // then execute the logic for the operation
    if (!xSession::isCLRSession())
    {
        new XppILExecutePermission().assert();
        SysDictClass::invokeStaticMethodIL(classStr(SysOpSampleSimpleRunbaseBatch),
                                           staticMethodStr(SysOpSampleSimpleRunbaseBatch, showTextInInfolog),
                                           packedRunBase);
        // exit call executed in CLR session.
        return;
    }

    thisClass = new SysOpSampleSimpleRunbaseBatch();
    if (!thisClass.unpack(packedRunBase))
    {
        throw AifFault::fault('SysOpSampleSimpleRunbaseBatch unpack error', 'unpackError');
    }

    if (xSession::isCLRSession())
    {
        info('Running in a CLR session.');
    }
    else
    {
        info('Running in an interpreter session.');
        if (isRunningOnServer())
        {
            info('Running on the AOS.');
        }
        else
        {
            info('Running on the Client.');
        }
    }

    info(strFmt('SysOpSampleSimpleRunbaseBatch: %1, %2', thisClass.parmNumber(), thisClass.parmText()));
}

Create Class in Classes node and Name it SysOpSampleSimpleDataContract.

[DataContractAttribute,
SysOperationContractProcessingAttribute(classStr(SysOpSampleSimpleUserInterfaceBuilder))]
class SysOpSampleSimpleDataContract
{
    str text;
    int number;
}

[DataMemberAttribute,
SysOperationLabelAttribute('Number Property'),
SysOperationHelpTextAttribute('Type some number >= 0'),
SysOperationDisplayOrderAttribute('2')]
public int parmNumber(int _number = number)
{
    number = _number;
    return number;
}

[DataMemberAttribute,
SysOperationLabelAttribute('Text Property'),
SysOperationHelpTextAttribute('Type some text'),
SysOperationDisplayOrderAttribute('1')]
public Description255 parmText(str _text = text)
{
    text = _text;
    return text;
}

Create Class in Classes node and Name it SysOpSampleSimpleController.

class SysOpSampleSimpleController extends SysOpSampleBaseController
{
}

public ClassDescription caption()
{
    return 'Intermediate SysOperation Sample';
}

void new()
{
    super();

    this.parmClassName(classStr(SysOpSampleSimpleService));
    this.parmMethodName(methodStr(SysOpSampleSimpleService, showTextInInfolog));
}

public static void main(Args args)
{
    SysOpSampleSimpleController operation;

    operation = new SysOpSampleSimpleController();
    operation.startOperation();
}

Create Class in Classes node and Name it SysOpSampleSimpleService.

class SysOpSampleSimpleService extends SysOperationServiceBase
{
}

public void showTextInInfolog(SysOpSampleSimpleDataContract data)
{
    if (xSession::isCLRSession())
    {
        info('Running in a CLR session.');
    }
    else
    {
        info('Running in an interpreter session.');
        if (isRunningOnServer())
        {
            info('Running on the AOS.');
        }
        else
        {
            info('Running on the Client.');
        }
    }


    info(strFmt('SysOpSampleSimpleService: %1, %2', data.parmNumber(), data.parmText()));
}

Create Class in Classes node and Name it SysOpSampleSimpleUserInterfaceBuilder.

class SysOpSampleSimpleUserInterfaceBuilder extends SysOperationAutomaticUIBuilder
{
    #define.lookupAlways(2)

    DialogField numberField;
    DialogField textField;
}

public boolean numberFieldValidate(FormIntControl _control)
{
    if (_control.value() < 0)
    {
        error('Please type a number >= 0');
        return false;
    }
    return true;
}

public void postBuild()
{
    super();

    // get references to dialog controls after creation
    numberField = this.bindInfo().getDialogField(this.dataContractObject(), methodStr(SysOpSampleSimpleDataContract, parmNumber));
    textField = this.bindInfo().getDialogField(this.dataContractObject(), methodStr(SysOpSampleSimpleDataContract, parmText));
    // change text field metadata to add lookup
    textField.lookupButton(#lookupAlways);

}

public void postRun()
{
    super();

    // register overrides for form control events
    numberField.registerOverrideMethod(methodstr(FormIntControl, validate), methodstr(SysOpSampleSimpleUserInterfaceBuilder, numberFieldValidate), this);
    textField.registerOverrideMethod(methodstr(FormStringControl, lookup), methodstr(SysOpSampleSimpleUserInterfaceBuilder, textFieldLookup), this);

}

public void  textFieldLookup(FormStringControl _control)
{
    FormStringControl       companyControl;
    SysTableLookup          tableLookup;
    Query                   query = new Query();

    companyControl = _control;
    tableLookup = SysTableLookup::newParameters(tablenum(DataArea),companyControl);
    tableLookup.addLookupfield(fieldnum(DataArea,Id),true);
    tableLookup.addLookupfield(fieldnum(DataArea,Name),false);

    query.addDataSource(tablenum(DataArea));
    tableLookup.parmQuery(query);
    tableLookup.performFormLookup();
}
The service and data contract classes define the operation. The derived controller class provides the main entry point and overrides the new() method to associate the operation classes with the controller. The base controller reflects on the operation and constructs metadata classes that define the operation. The base class SysOperationAutomaticUIBuilder uses the metadata derived from the operation to create the user interface. In the sample, there is a derived user interface builder called SysOpSampleSimpleUserInterfaceBuilder. This overrides the postBuild() and postRun() overrides on the base builder to subscribe to form control events related to validation and lookup.

The system uses SysOperationContractProcessingAttribute to associate the custom user interface builder with the data contract.

[DataContractAttribute,
SysOperationContractProcessingAttribute(classStr(SysOpSampleSimpleUserInterfaceBuilder))]
class SysOpSampleSimpleDataContract
{
      str text;
      int number;
}

If this attribute is not present, the default builder, SysOperationAutomaticUIBuilder, is used. As an experiment, comment out the attribute in the preceding code, and then run the operation to see the differences.

The postBuild() override in the custom user interface builder is where the form control metadata needs to be modified before the controls are instantiated. The framework maintains an association between controls and data contracts in a map that can be accessed via the this.bindInfo() method. The map is keyed by the name of the property in the data contract.

public void postBuild()
{
      super();
      // get references to dialog controls after creation
      numberField = this.bindInfo().getDialogField(this.dataContractObject(),
            methodStr(SysOpSampleSimpleDataContract, parmNumber));
      textField = this.bindInfo().getDialogField(this.dataContractObject(),
            methodStr(SysOpSampleSimpleDataContract, parmText));
      // change text field metadata to add lookup
      textField.lookupButton(#lookupAlways);
}

The postRun() override in the custom user interface builder is where the form control events are subscribed to. The subscriptions must be added to the controls after they have been instantiated.

public void postRun()
{
      super(); // register overrides for form control events
      numberField.registerOverrideMethod(methodstr(FormIntControl, validate),
            methodstr(SysOpSampleSimpleUserInterfaceBuilder, numberFieldValidate), this);
      textField.registerOverrideMethod(methodstr(FormStringControl, lookup),
            methodstr(SysOpSampleSimpleUserInterfaceBuilder, textFieldLookup), this);
}

The registerOverRideMethod method on the controls is a run-time equivalent to the control overrides used in normal forms. If you use an override method in a standard Microsoft® MorphX® form, you can use the same method override in a dynamic form by using this mechanism. Note that both the RunBase and SysOperation frameworks allow the use of modeled forms as the operation user interface.

The SysOperation framework provides the override SysOperationController.templateForm() for that purpose, however, this topic is outside the scope of this white paper.

The samples in this section show how the user interface for the operation can use many of the same features that are available in the normal form programming model. Control overrides fire run-time events that can be subscribed to. The SysOperation version of the sample shows how the different aspects of the operation can be factored into separate classes.

To show that everything is possible with code, the RunBase sample is modified so that it marshals its interactive execution into a CLR session, in the same way that the SysOperation framework does. This illustrates the design principle that drove the SysOperation framework: move as much of the boilerplate code as possible into the base classes.

private static server void showTextInInfolog(container packedRunBase)
{
      SysOpSampleSimpleRunbaseBatch thisClass;
      // If not in a CLR session then marshal over. If already in a CLR session
      // then execute the logic for the operation
      if (!xSession::isCLRSession())
      {
new XppILExecutePermission().assert();
SysDictClass::invokeStaticMethodIL (classStr (SysOpSampleSimpleRunbaseBatch),
StaticMethodStr (SysOpSampleSimpleRunbaseBatch,
showTextInInfolog),
packedRunBase);
// exit call executed in CLR session.
return;
      }
      thisClass = new SysOpSampleSimpleRunbaseBatch();
      if (!thisClass.unpack(packedRunBase))
      {
throw AifFault::fault('SysOpSampleSimpleRunbaseBatch unpack error', 'unpackError');
      }
      if (xSession::isCLRSession())
      {
info('Running in a CLR session.');
      }
      else
      {
info('Running in an interpreter session.');
if (isRunningOnServer())
{
info('Running on the AOS.');
}
else
{
info('Running on the Client.');
}
      }
      info(strFmt('SysOpSampleSimpleRunbaseBatch: %1, %2',
      thisClass.parmNumber(), thisClass.parmText()));
}
Now Generate CIL and run your Controller Class.

Related Posts

Previous
Next Post »

Thanks for comments.....