6 de marzo de 2021

Enlazar eventos para obtener mejores aplicaciones - Parte 1 de 4

Artículo original: Bind Events for Better Applications
(Bind Events for Better Applications 2017.pdf)
Autor: Tamar E. Granor
Traducido por: Luis María Guayán


A primera vista, la función BindEvents() puede parecer innecesaria. Después de todo, ¿por qué vincularse a un evento cuando solo puede escribir código en el método del evento?

En esta sesión ya popular, veremos por qué BindEvents() y sus primos, los métodos Access y Assign, son tan valiosos. Usando ejemplos extraídos de aplicaciones reales, veremos cómo la vinculación de eventos nos permite hacer cosas que de otra manera no podríamos hacer y simplifica el código para otras tareas. Los ejemplos incluyen el seguimiento de los cambios del usuario en un formulario, el seguimiento de la actividad del usuario, ToolTips dinámicos y más. También hablaremos sobre las dificultades involucradas en la depuración de código que usa el enlace de eventos.

Cuando se agregó la función BindEvent() en Visual FoxPro 8, me costó entender por qué debería importarme. A diferencia de la función EventHandler() que permite que mi código responda a eventos para otros servidores, BindEvent() funcionó solo para eventos disparados en código VFP. ¿Por qué necesitaría enlazarme a tales eventos? ¿Por qué no podría simplemente poner el código necesario en los correspondientes métodos de los eventos?

Con bastante rapidez, me di cuenta de que BindEvent() sería bastante útil cuando se trata de código de caja negra que no se puede modificar o subclasear (como herramientas de terceros). Pero me tomó mucho más tiempo antes de que realmente viera el potencial de BindEvent() y comenzara a usarlo ampliamente. Por otro lado, con la introducción de los métodos de Access y Assign en Visual FoxPro 6, inmediatamente vi que nos brindaban la oportunidad de crear nuestros propios eventos personalizados y rápidamente comenzamos a encontrar usos para ellos. A medida que crecía mi nivel de comodidad con BindEvent(), vi que las dos capacidades estaban realmente relacionadas. Tanto los métodos Access como Assign y BindEvent nos dan más control sobre lo que sucede cuando los usuarios trabajan con nuestras aplicaciones. Además, nos permiten construir más capacidades en nuestras bibliotecas de clases, por lo que podemos usarlas en nuevos formularios y aplicaciones.

En este artículo, mostraré cómo funciona la vinculación de eventos y los métodos Access/Assign, y examinaré algunas de las formas en que estas capacidades han mejorado las aplicaciones que desarrollé. Además, echaré un breve vistazo a los desafíos que ofrecen para la depuración. Los materiales para esta sesión incluyen una (simple) aplicación que demuestra muchas de las técnicas discutidas. La aplicación está diseñada para una biblioteca de préstamos, para manejar libros que se prestan y registran, rastrear miembros y mantener el catálogo de libros.

Esta aplicación se creó originalmente como una demostración de una variedad de prácticas de interfaz de usuario; como resultado, parte de su interfaz de usuario es un poco diferente a las aplicaciones estándar de Windows.

En particular, fue diseñada para usar códigos de barras para especificar un miembro o un libro, incluso cuando no hay formularios abiertos. Para simular códigos de barras escaneados, escriba el valor deseado con un asterisco ("*") en cada extremo. (algunos estándares de códigos de barras, como el Código 39, usan un asterisco en cada extremo como código de inicio y finalización). Por ejemplo, para especificar el miembro cuyo código de barras es "7160769048", debe escribir "*7160769048*" (sin las comillas). Sin embargo, en un campo de código de barras, puede omitir los asteriscos.

A lo largo de este artículo, usaré los términos "nivel superior" y "base" indistintamente para referirme a las subclases de primer nivel de las clases base de VFP.

Antecedentes de BindEvent()

Antes de profundizar en los ejemplos, comencemos con un poco de teoría y terminología. La función BindEvent() crea una conexión entre métodos de dos objetos. Específicamente, un evento de un objeto -el origen del evento- es controlado por otro método -el método delegado o delegado- de otro objeto -el controlador de eventos-. La sintaxis de BindEvent() se muestra en el listado

1. Dice que siempre que se activa el método cMethod de oEventSource, también se ejecutará el método cDelegateMethod de oEventHandler.

Listado 1. La función BindEvent() le permite vincular un método a otro.

BINDEVENT(oEventSource, cMethod, oEventHandler, cDelegateMethod, nFlags)

De forma predeterminada, el método delegado se ejecuta primero, pero puede cambiarlo con el parámetro nFlags. Es decir, se ejecuta el código en ambos métodos, pero puede determinar el orden.

nFlags también controla si el enlace solo se aplica cuando cMethod se activa como un evento, o también cuando se llama a cMethod mediante programación.

VFP tiene un ejemplo integrado de enlace de eventos (aunque no usa BindEvent()). Cuando establece la propiedad KeyPreview de un formulario en .T., Cada vez que se presiona una tecla en cualquier control del formulario, se activa el método KeyPress del formulario, seguido del método KeyPress del propio control. Es como si hubiera emitido BindEvent(This, "KeyPress", ThisForm, "KeyPress") para cada control del formulario.

La aplicación de biblioteca de ejemplo utiliza esta capacidad para manejar códigos de barras. Todos los formularios de entrada de datos se derivan de una clase (frmBarCodeEnabled) que tiene KeyPreview establecido en .T. y codifique en KeyPress para determinar si los datos que se ingresaron son códigos de barras.

Hay tres funciones adicionales que se ocupan del enlace de eventos. UnbindEvents() le permite desactivar el enlace de eventos. Puede desactivar todos los enlaces para un objeto en particular o solo un enlace específico. Los enlaces se desactivan automáticamente cuando cualquiera de los objetos sale del alcance, por lo que solo necesita usar UnbindEvents() si necesita eliminar un enlace mientras ambos objetos todavía están disponibles.

AEvents() le permite averiguar qué eventos están vinculados actualmente. Es más útil en el método delegado para averiguar qué objeto y evento activó el método; hay algunos ejemplos más adelante en este documento.

La última función de enlace de eventos es RaiseEvent(); le permite disparar un evento mediante programación. Se diferencia de simplemente llamar al método de evento en que garantiza que se llamen a los métodos delegados, sin importar qué parámetros haya especificado en BindEvent()

Vinculación a eventos de Windows

En Visual FoxPro 9, también puede vincularse a eventos de Windows, como cambiar aplicaciones o cambiar el esquema de color. La sintaxis en ese caso es un poco diferente; entre otras cosas, le permite especificar para qué ventana desea capturar el evento de Windows especificado. El Listado 2 muestra la estructura de la llamada.

Listado 2. Cuando se vincula a eventos de Windows, los parámetros de BindEvent() cambian.

BindEvent(hWnd | 0, nMessage, oEventHandler, cDelegateMethod [, nFlags])

El término oficial de Windows para estos eventos es "messages". Hay docenas de mensajes a los que puede responder; cada uno tiene un código numérico único. Desafortunadamente, la lista no está en la documentación de VFP. La lista completa tampoco parece estar en MSDN, pero este sitio parece tener una lista completa: http://wiki.winehq.org/List_Of_Windows_Messages.

Pase el identificador de ventana (hWnd) de la ventana cuyos mensajes desea capturar o pase 0 para capturar todos los mensajes de Windows. Mi experiencia es que, por lo general, pasar _VFP.hWnd me da lo que quiero. El controlador de eventos y los parámetros del método delegado son los mismos que cuando se vinculan eventos VFP. Aunque puede pasarlo sin errores, el parámetro nFlags se ignora al vincular un mensaje de Windows. En el método delegado, si desea que el evento de Windows se produzca como de costumbre, debe incluir código para transmitirlo. El código que necesita se muestra en "Actualización de colores cuando cambia el tema de color" más adelante en este documento.

Poner a funcionar a BindEvent()

Ahora que hemos cubierto los conceptos básicos, pasemos a algunos ejemplos que muestran cómo BindEvent() puede mejorar sus aplicaciones.

Gestión de menús contextuales

El primer lugar donde me quedó clara la utilidad real de la vinculación de eventos fue para el manejo de menús contextuales (también conocidos como menús de botón derecho). Aunque puede administrarlos en el nivel de control, generalmente encuentro que quiero hacerlo en el nivel de formulario. Es decir, quiero utilizar un método de formulario único para evaluar la situación actual y completar y mostrar un menú contextual. Hago esto emitiendo el comando en el Listado 3, en el método Init de todas mis clases de control de nivel superior.

Listado 3. Poner este comando en la inicialización de todas mis clases de control me da un manejo central de los Clics con el botón derecho.

BINDEVENT(Esto, "RightClick", ThisForm, "RightClick")

Es razonable preguntarse por qué esto es mejor que poner ThisForm.RightClick en el método RightClick de cada clase de control de nivel superior. La razón principal es que, con el enlace de eventos, puedo averiguar en el método RightClick del formulario en qué control se hizo clic con el botón derecho; No tengo que pasar el control como parámetro y recordar recibir el parámetro en el RightClick del formulario. Además, el enlace de eventos no puede ser anulado por código personalizado en el método RightClick del control (aunque, por supuesto, puede ser por código personalizado en el método Init, pero la mayoría de los desarrolladores tienen cuidado de emitir DoDefault() en el método Init.).

En cuanto a la construcción de los menús de acceso directo, mi enfoque se basa en uno que Doug Hennig publicó en FoxTalk (hace mucho tiempo en septiembre de 1997). El método RightClick de mi clase base Formulario llama a un método ShowMenu personalizado. Ese método determina la persona que llama, crea un menú emergente, llama a un método personalizado (llamado ShortcutMenu, es abstracto en la clase base Formulario) que llena la ventana emergente según quién lo llamó y otros factores, y activa la ventana emergente. El código se muestra en el Listado 4.

Listado 4. El método ShowMenu personalizado de mi clase base Formulario configura y muestra un menú contextual.

LOCAL aEventInfo[1], oObject
* Find out who called
IF AEVENTS(aEventInfo, 0) = 0
 * Called from form
 oObject = This
ELSE
 oObject = aEventInfo[1]
ENDIF

* Define menu
RELEASE POPUPS ShortCut
DEFINE POPUP ShortCut FROM MROW(), MCOL() SHORTCUT
ON SELECTION POPUP ShortCut WAIT WINDOW "Under construction." NOWAIT
* Populate menu
This.ShortcutMenu(m.oObject)
* Activate menu
IF CNTBAR("ShortCut") > 0
 ACTIVATE POPUP Shortcut
ENDIF
RELEASE POPUPS ShortCut
RETURN

ShowMenu usa AEvents() para identificar el control en el que el usuario hizo clic derecho; cuando pasa 0 como segundo parámetro a AEvents(), llena la matriz especificada (su primer parámetro) con información sobre el enlace que condujo a la rutina actual; el primer elemento de la matriz es la fuente del evento. Si la función devuelve 0, significa que este código no fue activado por un evento vinculado; en ese caso, sabemos que el usuario hizo clic con el botón derecho en el formulario.

Para un formulario en particular, todo lo que tengo que hacer es poner código en el método ShortcutMenu para crear las barras de menú apropiadas, según el objeto que recibe como parámetro. El Listado 5 muestra el código en el método ShorcutMenu del formulario CheckOut en la aplicación Biblioteca. La Figura 1 muestra el menú contextual cuando se muestra un miembro en el formulario y hace clic en cualquier lugar excepto sobre un libro en la grilla; La Figura 2 muestra el menú contextual cuando hace clic derecho sobre un libro en la grilla.

Listado 5. Vincular todos los clics con el botón derecho del ratón al formulario le permite centralizar el manejo de los menús contextuales.

LPARAMETERS oObject
* oObject = object actually right-clicked
* Build the shortcut menu for this form
LOCAL nNextBar
nNextBar = 1

* If we have a borrower, provide access to borrower form
IF NOT EMPTY(ThisForm.cCurrentMemberNum)
 DEFINE BAR m.nNextBar OF Shortcut PROMPT "Show this member's record"
 LOCAL cMemberNum
 cMemberNum = ThisForm.cCurrentMemberNum
 ON SELECTION BAR m.nNextBar OF Shortcut do form Borrowers with "&cMemberNum"
 nNextBar = m.nNextBar + 1
ENDIF
* If we're over a book in the grid, offer to open the catalog pointing to it.
* If the control that got us here is the grid itself, we're not over a record.
LOCAL lInGrid, oCheckObj, cBookBarCode
lInGrid = .F.
IF NOT INLIST(UPPER(oObject.BaseClass), "GRID", "FORM")
 oCheckObj = m.oObject
 DO WHILE NOT m.lInGrid AND NOT ISNULL(oCheckObj.Parent)
 oCheckObj = oCheckObj.Parent
 DO CASE
 CASE UPPER(oCheckObj.BaseClass) = "GRID"
 lInGrid = .T.
 CASE UPPER(oCheckObj.BaseClass) = "FORM"
 * If we get to a form, we're not in a grid. Get out of here.
 EXIT
 ENDCASE
 ENDDO
ENDIF
IF m.lInGrid
 DEFINE BAR m.nNextBar OF Shortcut PROMPT "Show book in catalog"
 cBookBarCode = CheckOutList.cBarCode
 ON SELECTION BAR m.nNextBar OF Shortcut do form Catalog with "C", "&cBookBarCode"
ENDIF

Figura 1. Cuando aparece un miembro en el formulario de pago, al hacer clic con el botón derecho en la mayoría de los lugares se ofrece una única opción: Mostrar el registro de este miembro.

Figura 2. Cuando hace clic con el botón derecho del ratón sobre un libro de la lista que desea sacar, tiene la opción de ver ese libro en el catálogo.

Manejo de eventos dentro de un contenedor

No es raro querer que todos los controles dentro de un contenedor deleguen el comportamiento al contenedor. Esto es especialmente cierto cuando se usa un contenedor para representar objetos gráficos como en la Figura 3 (que proviene de una aplicación cliente), donde se usan muchas capas de contenedores para representar un objeto físico. Las acciones deben tener lugar en el nivel del objeto físico, no en los controles a partir de los cuales se construye. Por ejemplo, cada una de las casillas verdes debajo de las etiquetas "Interfaz n" es un objeto contenedor, que contiene una etiqueta y cuatro contenedores adicionales. (De hecho, aunque la figura no lo muestra, la cantidad de contenedores dentro puede ser 2, 4 u 8.) Cada uno de esos contenedores adicionales contiene una etiqueta. En ésta aplicación, un doble clic en cualquier lugar dentro de uno de los cuadros verdes debería abrir otro formulario. El enlace de eventos hace que esto sea sencillo.

Figura 3. En este formulario, los contenedores contienen otros contenedores, así como etiquetas, formas y otros controles. A menudo, un clic o un doble clic deben interpretarse en el contexto del contenedor, no en el control que recibe la acción.

Mi clase Contenedor de nivel superior, cntBase, tiene propiedades personalizadas, lBindClick, lBindDblClick y lBindMouseDown, así como los métodos personalizados BindClick, BindDblClick y BindMouseDown. Los tres métodos son todos bastante similares. El Listado 6 muestra el método BindClick.

Listado 6. El método BindClick de la clase Contenedor de nivel superior profundiza a través de todos los objetos en el contenedor y vincula sus métodos Click al método Click del contenedor. Utiliza la recursividad para profundizar.

LPARAMETERS oContainer
* Bind all contents to start drag
FOR EACH oObject IN oContainer.Objects FOXOBJECT
 IF PEMSTATUS(oObject, "Click", 5)
 BINDEVENT(oObject, "Click", This, "Click")
 ENDIF

 IF PEMSTATUS(oObject, "Objects", 5)
 This.BindClick(m.oObject)
 ENDIF
ENDFOR

El método Init incluye el código en el Listado 7, que usa las propiedades para determinar si llamar a los métodos y configurar el enlace para cada uno de los tres eventos (Click, DblClick, MouseDown).

Listado 7. Este código en el método Init de la clase Contenedor de nivel superior vincula los métodos de controles Click, DblClick y MouseDown dentro del contenedor al contenedor, si se establecen los indicadores relevantes.

IF This.lBindDblClick
 This.BindDblClick(This)
ENDIF
IF This.lBindMouseDown
 This.BindMouseDown(This)
ENDIF
IF This.lBindClick
 This.BindClick(This)
ENDIF

Con esta estructura en su lugar, para cualquier contenedor dado, todo lo que tiene que hacer es establecer las propiedades lBindClick, lBindDblClick y lBindMouseDown para determinar qué acciones deben propagarse desde los controles contenidos al contenedor mismo. (Una mejora útil sería crear un único método BindMethod que acepte que el método se vincule como parámetro y reemplace los métodos individuales BindClick, etc.). MouseDown se incluye aquí porque es el evento más fácil para activar operaciones de arrastrar y soltar. Si se implementa la función de arrastrar y soltar, vincular MouseDown permite al usuario hacer clic en cualquier objeto dentro de un contenedor para arrastrar todo el contenedor.

En la aplicación Biblioteca, la pestaña Copiar del panel derecho del formulario Catálogo tiene todos sus controles en un solo contenedor. Cuando se ha seleccionado una copia de un libro, puede arrastrar desde ese contenedor al formulario CheckIn o CheckOut para agregar el libro a la lista de check-in o check-out respectivamente. No quiero que el usuario tenga que preocuparse por dónde se encuentra en la página Copiar, por lo que el contenedor, cntCopyInformation, tiene lBindMouseDown configurado en .T., Por lo que MouseDown en cualquier objeto en el contenedor activa el método MouseDown del contenedor, que contiene el código del Listado 8 para comenzar a arrastrar.

La figura 4 muestra un arrastre en curso.

Listado 8. Este código en el evento MouseDown de cntCopyInformation se activa cuando el usuario hace clic en cualquier lugar del contenedor porque el evento MouseDown de todos los objetos contenidos está vinculado al MouseDown del contenedor.

LPARAMETERS nButton, nShift, nXCoord, nYCoord
IF NOT EMPTY(This.txtBarCode.Value)
 This.OLEDrag(.T.)
ENDIF

Hay código tanto en el contenedor como en los formularios en los que puede soltar para manejar la operación de arrastrar y soltar, pero no es muy relevante para el enlace.

Figura 4. La página de copia del catálogo contiene un contenedor que identifica esta copia del libro especificado. Puede arrastrar desde el contenedor si está sobre el contenedor en sí o sobre cualquiera de sus controles contenidos.

Continua en: "Enlazar eventos para obtener mejores aplicaciones - Parte 2 de 4" ...


Copyright 2017, Tamar E. Granor

No hay comentarios. :

Publicar un comentario

Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.