Añadir lookup multiselección en un diálogo de SysOperation Framework

¡Primer post del 2020! ¡Feliz año nuevo! Sí, ya se que estamos a pasado mediados de enero…

Cuando añades un campo en un Data Contract de la SysOperation Framework el lookup que genera el framework (si el EDT lo tiene) es un lookup simple, de selección única. Vamos a ver cómo crear un lookup que permita la selección múltiple en MSDyn365FO.

El SysOperation Framework y MVC

Pero antes un poco de introducción. El SysOperation Framework se introdució en Dynamics AX 2012 para sustituir al RunBase Framework, y se usa para crear procesos que se ejecutarán por lotes. Las clases de RunBase siguen estando en Dynamics 365 for Finance and Operations. Algunos procesos estándar las siguen usando, mientras otras las usan para luego llamar a clases del SysOperations Framework.

Aunque podemos seguir usando las clases del RunBase Framework recomendaría usar las SysOperations ya que tienen mejor rendimiento y usan un patrón de software moderno como el MVC (aunque de moderno tiene poco porque existe desde los 70…).

Para hacer esto necesitamos crear una clase de tipo Data Contract para nuestro modelo de datos, una Controller que define como se ejecuta el proceso, una Service con la lógica (que será un DataProvider en el caso de reports) y una clase UIBuilder para modificar el comportamiento del diálogo. En este ejemplo el lookup mostrará las empresas disponibles y ejecutará un report.

Data Contract

La clase Data Contract es nuestro modelo de datos y tenemos que etiquetarla con el atributo DataContract. Para crear el lookup de selección múltiple vamos a declarar una List en vez de una variable del tipo del EDT que tiene la relación con la tabla que queremos seleccionar. El método parm tiene que ir decorado con los atributos DataMemeber y AifCollectionType.

Nuestro Data Contract va a ser así:

[
    DataContract,
    SysOperationContractProcessing(classStr(AASSysOpsReportUIBuilder))
]
class AASSysOpsReportDC
{    
    List selectedEntities;

    [
        DataMember('selectedEntities'),
        SysOperationLabel("@SYS303247"),
        AifCollectionType('return', Types::String)
    ]
    public List parmSelectedLegalEntities(List _selectedEntities = selectedEntities)
    {
        selectedEntities = _selectedEntities;

        return selectedEntities;
    }

}

El SysOperation Framework creará la UI automáticamente si los miembros están marcados con el atributo DataMember, otra ventaja respecto el RunBase.

Clase Service (Data Provider)

La clase Service es en la que desarrollaremos la lógica. Como dije esta clase es distinta dependiendo de si queremos ejecutar un report o un proceso por lotes.

Data Provider

Para un report crearemos una clase Data Provider (DP) en la que rellenaremos la tabla temporal con la que alimentamos el report:

[SRSReportParameterAttribute(classStr(AASSysOpsReportDC))]
class AASSysOpsReportDP extends SrsReportDataProviderPreProcess
{
    AASSysOpsReportTableTmp reportTmp;

    [SRSReportDataSet(tablestr(AASSysOpsReportTableTmp ))]
    public AASSysOpsReportTableTmp getTmpTable()
    {
        select reportTmp;

        return reportTmp;
    }
}

Service

Para una clase que ejecute lógica de negocio hacemos lo siguiente, extendiendo una clase distinta:

class AASSysOpsBatchService extends SysOperationServiceBase
{
    AASSysOpsBatchDC         contract;
    
    public void executeOperation(AASSysOpsBatchDC _contract)
    {
        // do things
    }
}

Controller

El controller es donde definimos como queremos que sea la clase, síncrona o asíncrona, es bastante sencillo:

class AASSysOpsReportController extends SrsReportRunController
{
    public void execute(Args _args)
    {
        this.parmReportName(ssrsReportStr(AASTestReport, Report));
        this.parmArgs(_args);
        this.startOperation();
    }

    static void main(Args _args)
    {
        new AASSysOpsReportController().execute(_args);
    }

}

Como estoy ejecutando un report no defino cómo se ejecutará la clase. Si creo una clase para ejecutar en Batch el controller debería ser así (ojo que, como el servicio, también extiende otra clase distinta):

class AASBatchClassController extends SysOperationServiceController
{
    public static void main(Args _args)
    {
        AASBatchClassController controller      = AASBatchClassController::construct();

        controller.startOperation();
    }

    public static AASBatchClassController construct(SysOperationExecutionMode _executionMode = SysOperationExecutionMode::Asynchronous)
    {
        return new AASBatchClassController(identifierStr(AASSysOpsBatchService), methodStr(AASSysOpsBatchService, executeOperation), _executionMode);
    }

}

Aquí es donde le decimos a la clase como ejecutarse, en este ejemplo de forma síncrona con el parámetro _executionMode. Podemos usar 4 formas distintas de proceso:

  • Synchronous: ejecuta el proceso i deja la UI congelada hasta que termina el proceso.
  • Asynchronous: ejecuta el proceso por lotes.
  • Reliable asynchronous: ejecuta el proceso por lotes nada más darle al OK. Mientras se ejecuta aparece en la lista de lotes pero desaparece al terminar.
  • Scheduled batch: igual que el Reliable asynchronous pero el job no se borra de la lista al terminar.

Clase UIBuilder

La clase UIBuilder nos permite cambiar la forma en la que la clase DC genera la UI o también mejorar la funcionalidad de un campo, que es lo que queremos nosotros:

class AASSysOpsReportUIBuilder extends SrsReportDataContractUIBuilder
{
    AASSysOpsReportDC   contract;
    DialogField		availableCompanies;
    

    public void postBuild()
    {
        contract		= this.dataContractObject() as AASSysOpsReportDC;
        availableCompanies	= this.bindInfo().getDialogField(contract, methodStr(AASSysOpsReportDC, parmSelectedLegalEntities));

        availableCompanies.lookupButton(FormLookupButton::Always);
    }

    public void postRun()
    {
        Query		        query		    = new Query();
        QueryBuildDataSource    qbdsLegalEntity     = query.addDataSourceTable(tablenum(OMLegalEntity));

        qbdsLegalEntity.fields().addField(fieldNum(OMLegalEntity, LegalEntityId));
        qbdsLegalEntity.fields().addField(fieldNum(OMLegalEntity, Name));

        container selectedFields = [tableNum(OMLegalEntity), fieldNum(OMLegalEntity, LegalEntityId)];

        SysLookupMultiSelectCtrl::constructWithQuery(this.dialog().dialogForm().formRun(), availableCompanies.control(), query, false, selectedFields);
    }
}

Creamos un dialog field y lo inicializamos con el método parm de nuestra lista en el DC. Después en el método postRun llamamos al método SysLookupMultiSelectCtrl::constructWithQuery. Le podemos pasar una query que ya exista o crear una con X++ como he hecho yo. Atención, atentos a que no se llama al super en ninguno de los dos métodos, de otro modo lo que pasaría es que el SysOperation Framework crearía los campos y tendríamos un error en tiempo de ejecución.

El resultado

Con todas las clases creadas ahora podemos ejecutar la clase Controller y ver cómo se ve el lookup.

Los registros se pueden seleccionar, ¡tantos como queramos!

Y ahora podemos recuperar los datos usando el método parm en nuestra clase Service o Data Provider y usar los valores. ¡Genial!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.