howto

HOWTO: Crear controles y eventos dinámicamente en formularios AX

En este HOWTO me gustaría explicar el proceso completo para crear dinámicamente controles en un formulario existente (aunque también podría no existir, pero ya sería complicar demasiado) y además manejar los eventos generados por ese control tanto dentro como fuera del formulario en sí.

Lo haremos paso a paso, de lo mas facil a lo mas complicado:

Empezaremos con un formulario nuevo de AX totalmente estándar sin ninguna propiedad modificada:

Dynamic Runtime Forms | 1

El primer paso será generar controles en este formulario de forma dinámica, es decir, mediante código en tiempo de ejecución. Para ello modificaremos el método estandar run() del formulario de la siguiente manera:

public void run()
{
    FormButtonControl   fButtonCtr;
    ;
   
    super();
   
    element.lock();

    fButtonCtr = element.design().addControl(FormControlType::Button, "buttonName");
    fButtonCtr.text("Ejemplo");

    element.unLock();
}

Con lo que habremos añadido al diseño del formulario, un boton generado en tiempo de ejecución:

Dynamic Runtime Forms | 2

Añadimos otro control al formulario y establecemos alguna propiedad a los controles. Llevo todo este código a un nuevo método del formulario para mantener cierto orden:

public void run()
{
    super();

    element.drawControls();
}

private void drawControls()
{
    FormButtonControl   fButtonCtr;
    FormStringControl   fStringCtr;
    ;

    element.lock();

    // Control de texto (StringEdit), en negrita, ancho total
    fStringCtr = element.design().addControl(FormControlType::String, 'stringEditTexto');
    fStringCtr.bold(10);
    fStringCtr.widthMode(FormWidth::ColumnWidth);
    fStringCtr.text("Ejemplo");

    // Botón, con imagen incrustada y texto, ancho total
    fButtonCtr = element.design().addControl(FormControlType::Button, 'buttonUp');
    fButtonCtr.widthMode(FormWidth::ColumnWidth);
    fButtonCtr.buttonDisplay(FormButtonDisplay::TextAndImageLeft);
    fButtonCtr.normalResource(1067);
    fButtonCtr.text("Subir");

    // Botón, con imagen incrustada y texto, ancho total
    fButtonCtr = element.design().addControl(FormControlType::Button, 'buttonDown');
    fButtonCtr.widthMode(FormWidth::ColumnWidth);
    fButtonCtr.buttonDisplay(FormButtonDisplay::TextAndImageLeft);
    fButtonCtr.normalResource(1068);
    fButtonCtr.text("Bajar");

    element.unLock();
}

Esto es otra cosa!:

Dynamic Runtime Forms | 3

Bien, hasta aquí la parte fácil. Crear controles en runtime es bastante sencillo pero ahora, ¿qué hago con ellos?. Lo primero, tendremos que indicar al formulario donde puede encontrar el código manejador de eventos de los nuevos controles, primero la maner mas sencilla:

Lo primero que hay que hacer en cualquier caso es indicar a AX que tenemos métodos controladores de eventos propios, es decir, que estamos sobrecargando sus propios métodos controladores, igual que hacemos cuando "anulamos" métodos en tiempo de diseño (la traducción no es muy acertada):

public void run()
{
    // Sobrecargo los métodos de control
    element.controlMethodOverload(true);
    // ¿Dónde están los métodos de control?: Aquí mismo (this)
    element.controlMethodOverloadObject(this);

    super();

    element.drawControls();
}

Con el método controlMethodOverload indicamos que tenemos algún método sobrecargado, es decir, que busque primero nuestros métodos antes de ejecutar los suyos propios. El método controlMethodOverloadObject indica a AX dónde, en qué objeto, puede encontrar los métodos sobrecargados.

Hay que destacar dos puntos importantes:

  • El método se debe declarar siempre como public void
  • El nombre del método siempre ha de ser nombreObjeto_evento()

public void buttonUp_clicked()
{
    ;

    info(element.design().controlName('stringEditTexto').valueStr());
}

Funciona! Pero hay un problema. ¿Qué sentido tiene crear controles en tiempo de ejecución de una manera dinámica -por ejemplo, basandonos en opciones configurables en una tabla- si el código asociado a ellos debe permanecer estáticamente en el formulario? No tiene ningún sentido.

Es por esto que se hace necesaria la segunda opción, llevar tódo el código fuera del formulario, por ejemplo, a una clase, como es el caso. El formulario pués se libera del código y queda de una manera muy sencilla (e independiente totalmente de los controles añadidos en runtime):

public class FormRun extends ObjectRun
{
    // Clase manejadora de eventos
    JAEBaseForm_Mngr    baseFormMngr;
}

public void init()
{
    super();

    // Construyo una instancia de la clase manejadora asociada a
    // la instancia actual de este formulario
    baseFormMngr = JAEBaseForm_Mngr::construct(this);
}

public void run()
{
    // Sobrecargo los métödos de control
    element.controlMethodOverload(true);

    // ¿Dónde están los métödos de control?: Aquí mismo (this)
    element.controlMethodOverloadObject(baseFormMngr);

    super();

    // Crear los controles
    baseFormMngr.drawControls();
}

Y es la clase la que contiene todo el codigo dinámico, de manera que es posible reutilizarla en diferentes formularios:

class JAEBaseForm_Mngr
{
    FormRun     element;
}

private void new(FormRun _formRun)
{
    element = _formRun;
}

public static JAEBaseForm_Mngr construct(FormRun _element)
{
    return new JAEBaseForm_Mngr(_element);
}

public void drawControls()
{
    FormButtonControl   fButtonCtr;
    FormStringControl   fStringCtr;
    ;

    element.lock();

    // Control de texto (StringEdit), en negrita, ancho total
    fStringCtr = element.design().addControl(FormControlType::String, 'stringEditTexto');
    fStringCtr.bold(10);
    fStringCtr.widthMode(FormWidth::ColumnWidth);
    fStringCtr.text("Ejemplo");

    // Botón, con imagen incrustada y texto, ancho total
    fButtonCtr = element.design().addControl(FormControlType::Button, 'buttonUp');
    fButtonCtr.widthMode(FormWidth::ColumnWidth);
    fButtonCtr.buttonDisplay(FormButtonDisplay::TextAndImageLeft);
    fButtonCtr.normalResource(1067);
    fButtonCtr.text("Subir");

    // Botón, con imagen incrustada y texto, ancho total
    fButtonCtr = element.design().addControl(FormControlType::Button, 'buttonDown');
    fButtonCtr.widthMode(FormWidth::ColumnWidth);
    fButtonCtr.buttonDisplay(FormButtonDisplay::TextAndImageLeft);
    fButtonCtr.normalResource(1068);
    fButtonCtr.text("Bajar");

    element.unLock();
}

public void buttonUp_clicked()
{
    FormStringControl  fStringCtr = element.design().controlName('stringEditTexto');
    ;

    fStringCtr.text("Arriba!");
}

public void buttonDown_clicked()
{
    FormStringControl  fStringCtr = element.design().controlName('stringEditTexto');
    ;

    fStringCtr.text("Abajo!");
}

De esta manera conseguimos el objetivo. El objeto Form es estático y contendrá su propia funcionalidad que no cambiará en runtime. Sin embargo ejecuta métodos de la clase controladora que se encarga de toda la parte dinámica, tanto generar los controles, como manejar sus eventos.

Las posiblidades son enormes. Desde generar pequeños formularios a modo de diálogos, a construir formularios dinámicos basados en datos de tablas en la base de datos, igual que el estándar hace con el Configurador de Productos. De hecho las clases que generan el formulario del modelo de producto (las que empiezan por PBA*) son un buen banco de ejemplos sobre este tema.

Adjunto un proyecto con los dos objetos por si pudiera ser de utilidad.

Descargar objetos

HOWTO: Imprimir PDF desde AX mediante código

Habitualmente se presenta la necesidad de imprimir documentos desde Dynamics AX en formato PDF. Esta es una funcionalidad, presente en el estándar, se puede replicar por código según las necesidades.

El siguiente Job (basado en otro original de Giridhar Raj's Blog) indica como hacerlo para imprimir una factura pero es fácilmente extensible a otro tipo de documentos:


Job | Imprimir a PDF

HOWTO: Usar y crear Mapas (Maps) en Dynamics AX

Los mapas (Maps) son elementos particulares en Dynamics AX a los que vale la pena dedicar tiempo y aprender a utilizarlos, ya que resultan indispensables para ahorrar tiempo en modificaciones y desarrollos nuevos.

Para ver la utilidad de los Mapas pensemos un caso habitual: A una instalación estándar de AX se le añaden una serie de campos nuevos referentes a artículos (medidas, pesos, redondeos, precios, tallas, ...) particulares de esta instalación, con los cuales se tiene que trabajar en nuevos desarrollos para calculo de precios, tarifas, cantidades, ...

Lo tradicional con la mayoría de tecnologías sería añadir esos campos a las tablas necesarias (utilizando EDT's para mantener una coherencia de tipos), que podrían ser líneas de pedidos, facturas, compras, inventario, proyectos, producción ... (en resumen, muchas tablas), escribir el código necesario para la lógica requerida e implementarla en cada tabla, o utilizando clases, o ambas cosas.

Dynamics AX nos ofrece un objeto que encapsula esos campos, con su coherencia de tipos junto a la lógica que los maneja, de manera que se pueda aplicar toda esa información a las tablas necesarias de manera fácil (muy facil).

Lo vemos con un ejemplo, para el cual utilizo un mapa utilizado en muchísimas partes de la aplicación estándar y que encapsula la lógica y los campos del manejo de direcciones en AX, el mapa AddressMap.

Lo primero que haremos será crear una tabla nueva (se puede usar una existente) y buscar en el AOT el mapa estándar AddressMap (Data Dictionary > Maps > AddressMap). Desde el nodo del mapa en el AOT se pueden seleccionar los Fields necesarios y arrastrarlos a la tabla para crear los  campos.

jaestevan.com | Mapas 1

Bien, ya podemos guardar la tabla. El siguiente paso es "mapear" los campos de la nueva tabla, con el mapa, demanera que le digamos qué campo del mapa es qué campo en la tabla (el nombre no tiene por qué ser el mismo, aunque es recomenable para facilitar su comprensión).

Para ello vamos al nodo AddressMap en el AOT y añadimos un nuevo elemento a su sub-nodo Mappings. Al hacerlo nos aparecen todos los campos del mapa, para asignarlos a los campos necesarios de la tabla. Si algún campo del mapa no existe en la tabla puede dejarse sin asignar.

jaestevan.com | Mapas 2

Guardamos el mapa y con ésto ya tenemos los campos de nuestra tabla asignados al mapa estándar, vamos a probar este enlace para aplicar la lógica de direcciones a nuestra tabla, para lo cual anulamos el método modifiedField de la misma y añadimos una nueva línea:

jaestevan.com | Maps 3

Y con esto tenemos la lógica de direcciones implementada en nuestra tabla, de una manera totalmente optimizada ante futuros cambios del estándar, coherente con los tipos y las relaciones de los campos, evitando duplicar código, ... Se puede probar insertando un nuevo registro y modificando el código postal, por ejemplo.

Comentarios serán bien recibidos, los mapas es una cosa que cuanto mas se utiliza, más util parece.

HOWTO: AX - Distribuir ficheros con SysFileDeployment

A veces es util poder distribuir a los clientes de AX ficheros auxiliares que van a necesitar para la ejecución del programa, por ejemplo, controles ocx, librerias dll, iconos, imágenes, etc.

Gracias a André Arnaud, que amablemente respondió a una duda mía en los foros de la Microsoft Dynamics Community he descubierto el framework de AX SysFileDeployment que viene perfecto para esta tarea.

La idea de este framework es heredar clases para indicar a AX qué ficheros deben copiarse a la parte cliente cada vez que se realiza una nueva instalación de AX. Este framework tiene bastantes limitaciones pero la ventaja es que en su código se gestiona bastante bien el tema de permisos para copiar ficheros situados en el servidor hacia el cliente.

Comparto las pruebas que yo he realizado por si a alguien le pudieran servir. En primer lugar, hay que generar una nueva clase heredada de SysFileDeploymentFile indicando a AX qué fichero hay que copiar, dónde esta y hacia donde copiarlo. Esta es la estructura básica de la clase heredada, que esta bien autodocumentada por si se necesita mas personalización:

[snippet|type=xpp|csid=13|tagstyle=block|lineNumbering=|numberStart=]

Una de las limitaciones de este framework es que sólo se ejecuta la primera vez que se ejecuta el cliente en un equipo determinado. Por tanto para distribuir nuevos ficheros o para hacer pruebas, como es mi caso, habría que modificar el método applBuildNo() de la clase ApplicationVersion, para forzar al sistema a actualizar la versión. Haciéndolo de este modo, AX detectará que tenemos ficheros pendientes de actualizar en el servidor y nos mostrará el siguiente diálogo:

SysFileDeploymentFile

Como no me apetecía andar modificando la build de mi entorno de pruebas, he hecho un pequeño Job que simula el código que se ejecutaría automáticamente de manera normal:

[snippet|type=xpp|csid=12|tagstyle=block|lineNumbering=|numberStart=]

Otra de las desventajas es que parece que esta clase no funciona demasiado bien en AX 4. Yo las pruebas que acabo de hacer en 2009 SP1 sí han funcionado, salvo un pequeño problema con las rutas de origen/destino, supongo que por ejecutar todo el entorno en la misma máquina (cliente, aplicación, AOS, ..).

A partir de aquí que cada uno investigue lo que necesite.

HOWTO: AX - Expandir LMAT por código

AX nos deja expandir, o explotar, una línea de lista de materiales (L.MAT, o BOM en inglés) de manera que podamos manejar sus elementos al mismo nivel que el artículo L.MAT. La funcionalidad se llama Expandir L.MAT y la podemos ver, por ejemplo, en las líneas de un pedido:

Explode BOM | Expandir L.MAT

Esta función nos presenta un formulario para elegir las líneas que queremos "Explotar" fuera de la lista de materiales para su tratamiento:

Explode BOM | Expandir L.MAT

El código que este formulario ejecuta para realizar la expansión de la L.MAT se encuentra, en el caso de líneas de pedido de venta, en la tabla SalesLine) concretamente en el método: SalesLine.expandBOM(), por tanto para simular esta funcionalidad en X++, el mínimo código que podemos utilizar es el siguiente:

[snippet|type=xpp|csid=10|tagstyle=block|lineNumbering=|numberStart=]

y digo mímino porque la línea se insertará con la mayoría de sus opciones por defecto, por lo que es seguro que habra que añadir código extra para completar los datos necesarios según las necesidades.

HOWTO: AX - Modificar el menu contextual de un objeto

Al hacer "click derecho" sobre un objeto en Dynamics Axapta se ejecuta el metodo showContextMenu, el cual como siempre, podemos sobrecargar. Esta es la pinta que tiene inicialmente:

 

[snippet|type=xpp|csid=4|tagstyle=block|lineNumbering=|numberStart=]

 

No tiene ningún misterio, es como la mayoría de métodos sobrecargados. Vamos a sustituirlo por este otro código:

 

[snippet|type=xpp|csid=5|tagstyle=block|lineNumbering=|numberStart=]

 

Como se puede ver en el ejemplo, podemos añadir por código un nuevo MenuItem al menu contextual y a la vez, capturar la pulsación del mismo para poder ejecutar nuestro propio código de respuesta.

HOWTO: AX - Ventana de progreso para procesos largos

Me gustaría empezar a colgar pequeños "trucos" o trozos de código útiles, por un lado con un fin divulgativo para que si a alguien le sirve, lo utilice; y por otro lado con un fin documental para hacer una pequeña biblioteca de código y poder consultarla en caso necesario. Sugerencias seran bien recibidas, como siempre. Como es el primero, empezare por uno muy sencillo:

Se trata de utilizar la clase SysOperationProgress para mostrar el diálogo estandar de progreso, que podemos (y debemos) utilizar en procesos que se puedan alargar mas de unos pocos segundos.

 

[snippet|type=xpp|csid=6|tagstyle=block|lineNumbering=|numberStart=]

 

Su utilización es bien sencilla y se puede complicar, por ejemplo para añadir varias barras de progreso en el mismo diálogo, jugando, por ejemplo, con los parámetros del constructor de la clase.