6 de enero de 2020

La programación orientada a objetos (OOP) directa: enlace de eventos y acoplamiento flexible

La programación orientada a objetos (OOP) directa: enlace de eventos y acoplamiento flexible


Artículo original: The Straight OOP: Event binding and loose coupling
Autor: Nancy Folsom
Traduccion: Luis María Guayán


Este mes eche un vistazo a una nueva característica en VFP 8.0 y la relaciono con el concepto de programación orientada a objetos de acoplamiento flexible.

Lo siento, pero me temo que esto tiene clasificación para Publico General, por lo que este artículo no será así. En este contexto, el acoplamiento se refiere al grado de dependencia entre objetos. Si hay poca dependencia, entonces un sistema está débilmente acoplado. Si los objetos se refieren directamente a otros objetos, entonces están estrechamente acoplados. Siempre ha habido al menos cierta dependencia entre los contenedores y sus objetos contenidos, aunque ha sido posible eliminar la mayoría, si no toda, la dependencia entre los objetos dentro de un contenedor, ya sea una clase de control, contenedor, formulario, etc.

¿Por qué? Una de las primeras preguntas con las que la mayoría de la gente se encuentra cuando comienza a usar Visual FoxPro es el problema de lo que sucede cuando se cambia el nombre, o incluso se elimina, un objeto en un contenedor al que se refieren otros objetos. Cuando los objetos están estrechamente acoplados, es difícil cambiar su comportamiento, cambiarlos por otros objetos y rastrear errores, ya que puede ser difícil encontrar en qué parte de la jerarquía de objetos se esconde el problema.

¿Dónde entra el acoplamiento?


La mayoría de nosotros creamos formularios detallados de entrada de datos que realizan una serie de tareas comunes. Los usuarios pueden ingresar datos, cambiar datos, guardar o deshacer ediciones, y podrían bloquear un registro contra la edición. La Figura 1 muestra un ejemplo simple de un formulario de entrada de datos.


Figura 1 Estado de la interfaz de usuario cuando se abre un formulario por primera vez en modo de edición

Para ayudar a los usuarios a tomar decisiones sensatas, es útil deshabilitar los botones que no tienen sentido en un contexto dado. En el ejemplo, una vez que se modifican los datos, se habilitan los botones Guardar y Deshacer, lo que indica al usuario que hay cambios pendientes. La figura 2 muestra este estado. Si hago clic en el botón Editar para bloquear los datos de la edición accidental, los campos de datos se deshabilitan, como muestra la Figura 3.


Figura 2 Estado de los elementos de la interfaz de usuario cuando los datos han cambiado


Figura 3 Vista de la interfaz de usuario después de desactivar el modo de edición


Miembros coordinadores


La coordinación de varios elementos en el formulario que tienen que interactuar y, sin embargo, son componentes independientes puede ser difícil de implementar para que las dependencias sean mínimas. En el ejemplo, el contenedor que tiene los datos es una unidad lógica, y el contenedor de botones es otra. Los botones deben ser reutilizables en cualquier formulario de entrada de datos, y los datos existirán independientemente de las acciones que puedan estar disponibles. Hay una tercera unidad formada por la relación entre el contenedor del botón y el contenedor de datos.

La relación entre los datos y las funciones disponibles (Guardar, Deshacer, Editar) está representada por el formulario, en este ejemplo. El formulario coordinará los dos contenedores. Cuanto menos se conozcan los dos contenedores, mejor será la reutilización. Es posible que desee guardar (o deshacer) los cambios no solo en datos relacionados con la persona, sino también en modelos de automóviles, artículos de inventario, etc. El contenedor de datos relacionados con la persona puede reutilizarse en una página en un marco de página, mostrarse como de solo lectura para informes, etc. Cada clase tiene algunas responsabilidades fundamentales. El contenedor de botones debe ser capaz de habilitar y deshabilitar botones, y debe tener algún método que sea paralelo a la funcionalidad representada por los botones. En otras palabras, el contenedor de botones tendrá un método para cada una de las funciones de los botones que los botones pueden llamar cuando se hace clic en ellos.

¿Por qué no poner la funcionalidad en el botón? Podría decirse que el primer paso para lograr el estado iluminado de acoplamiento débil es eliminar las referencias que los objetos hacen a los objetos contenidos dentro de otro contenedor. Entonces, por ejemplo, elimine referencias como las siguientes:

*!* SomeForm.SomePageFrame.SomePage.SomeButton.Click()
THIS.PARENT.Page2.Text1.VALUE = "I've changed!"

En cambio, deje que los contenedores simplemente notifiquen al mediador (el formulario) que algo ha ocurrido, pero luego deje que el formulario haga algo con la información. El Listado 1 muestra una forma simplista de desacoplar el botón y los contenedores de datos en un formulario de entrada de datos. En este escenario, un formulario tiene dos contenedores: uno muestra datos de un registro y el otro contenedor tiene 3 botones para Guardar, Deshacer y Editar. Cuando se modifican los datos, se habilitan los botones Guardar y Deshacer. Cuando el modo de Editar es falso, los objetos de datos están deshabilitados. Cuando se guardan o invierten los cambios, los botones Guardar y Deshacer deben deshabilitarse y los datos deben guardarse o revertirse. Cuando el modo Editar está desactivado, se guardan los cambios pendientes. Entonces, en resumen, los botones y los objetos de datos tienen efectos más allá de sus responsabilidades inmediatas. Para lograr esto, los botones pasan sus mensajes a su contenedor propietario, que pasa los mensajes al formulario, que luego pasa los mensajes al contenedor de datos, que, finalmente, hace algo (o no) con el objetos de datos.

Irónicamente, para desacoplar la lógica en un cuadro de texto de Apellido de la lógica en un botón Guardar, deben involucrarse muchos objetos. Sin embargo, es solo una aparente ironía. Cualquier acoplamiento apretado ocurre entre un objeto y su padre, lo cual es aceptable siempre que el contenedor sea el único responsable de comunicarse con el mundo. Sin embargo, hay muchos acoplamientos ajustados, sin embargo.

La lista de códigos muestra que los contenedores tienen métodos que son paralelos a los eventos que mediarán. Cuando se hace clic en el botón Guardar, llama al método Guardar del contenedor de botones, que llama al Guardar del mediador. El mediador (el formulario) llama a Save() del contenedor de datos. Esto es lo que quiero decir cuando digo un acoplamiento perfecto.

Aunque el contenedor de botones y el contenedor de datos en este ejemplo están desacoplados, todavía hay más dependencia entre el formulario y los contenedores de lo que es cómodo. Primero, es difícil obtener la sincronización correcta (es mejor implementar un bit de funcionalidad a la vez y probarlo), segundo, es difícil recordar en qué lugar de la jerarquía se coloca el código crítico y, tercero, si desea soltar el contenedor del botón en un formulario diferente, por ejemplo, debe asegurarse de que los métodos de mediación (como Guardar) estén presentes.


Enlace de eventos (Event binding)


Afortunadamente, Visual FoxPro 8.0 nos permite desacoplar la lógica de mensajería compleja como en mi ejemplo anterior, al permitirnos generar, vincular y encadenar eventos juntos. Incluso podemos tratar los eventos como objetos. Primero, deja yo retrocedo un momento rápido. Los eventos son diferentes de los métodos de esta manera: los eventos ocurren automáticamente, bajo ciertas circunstancias. Los métodos solo se ejecutan cuando los invocamos mediante programación, por ejemplo. Todavía se pueden llamar eventos como se invocarían métodos, pero es mejor no hacerlo.

Los eventos solo deberían ejecutarse cuando VFP crea que deberían ejecutarse. Entonces, en lugar de llamar al clic de un botón, por ejemplo, es mejor tener un método de nivel de formulario llamado, por ejemplo, OnClick() que pueden llamar tanto su código en otro lugar como el evento Click() del botón. En el ejemplo anterior, los botones Guardar y Deshacer llaman al código del método en el contenedor que guarda o revierte los cambios, respectivamente.

El enlace de eventos significa que podemos asociar los eventos que ocurren en un objeto con eventos en otro objeto. No es necesario que los eventos tengan el mismo nombre en ambos objetos. Hay algunas complejidades en la sintaxis y el uso, que no son el tema de este artículo en particular.

Lo que es importante tener en cuenta con respecto al acoplamiento es que mi mediador ahora puede reemplazar todos sus métodos personalizados para Guardar, Deshacer, etc., y los contenedores pueden ignorar la notificación a un padre sobre eventos. Depende del mediador configurar el enlace del evento entre los objetos. Veamos un ejemplo. El listado 2 reescribe el primer ejemplo para hacer uso de la función BindEvent(), nueva en VFP 8.0. Las diferencias críticas entre las dos metodologías es que los contenedores no tienen métodos paralelos que contengan objetos que llaman cuando su estado cambia, y hay menos necesidad de métodos de acceso y asignación. Estos son algunos de los fragmentos de código relevantes de la lista.

LOCAL loForm AS FORM
loForm = NEWOBJECT("BindEvents")
loForm.SHOW(1)
DEFINE CLASS BindEvents AS FORM
  ...

  PROCEDURE INIT
    *!* Set up event bindings.
    *!* When the edit button is clicked, trigger the data container's enable method.
    BINDEVENT(THIS.objDataCmd,"SetEdit",THIS.objPerson,"EnableControls",2)
    *!* If we're switching out of edit mode, save any changes.
    BINDEVENT(THIS.objDataCmd,"SetEdit",THIS.objDataCmd,"Save",2)
    *!* When data is being changed, trigger the button container refresh.
    BINDEVENT(THIS.objPerson,"OnChange",THIS.objDataCmd,"OnChange",2)
    *!* When Save is clicked, trigger the data container to save.
    BINDEVENT(THIS.objDataCmd,"Save",THIS.objPerson,"Save",2)
    *!* When Undo is clicked, trigger the data
    *!* container to revert changes.
    BINDEVENT(THIS.objDataCmd,"Undo",;
      THIS.objPerson,"Undo",2)
  ENDPROC
ENDDEFINE

Si bien el formulario todavía tiene dos contenedores: uno con botones y otro con objetos de datos, el formulario ya no necesita tener métodos paralelos coincidentes (como Guardar). En cambio, el formulario establece la relación entre los datos y los botones al relacionar los dos métodos relevantes en los contenedores. Observe también que los eventos pueden estar vinculados a más de un evento. En el ejemplo anterior, SetEdit guardará y habilitará controles. Esto está al más alto nivel. Incluso dentro de los contenedores, el enlace de eventos simplifica la tarea de los objetos comunicándose con sus padres.

Agregué un método personalizado al contenedor de botones para Guardar, Deshacer y Editar llamado BindEvents(), al que llamo desde Init(). Este método permite que el contenedor se enganche en los eventos interesantes de los botones. En este caso, se notifica al contenedor cuando cambia el modo Editar y cuando se hace clic en Guardar o Deshacer. Los eventos de clic Guardar y Deshacer ni siquiera tienen ningún código en ellos.

DEFINE CLASS DataCommands AS CONTAINER
  ...

  PROCEDURE BindEvents
    *!* Instead of button's click() calling a
    *!* method in the parent container. Simply
    *!* use the edit button's own event to trigger
    *!* the parent to some action.
    BINDEVENT(THIS.btnEdit,"InteractiveChange",;
      THIS,"SetEdit",2)
    BINDEVENT(THIS.btnEdit,"ProgrammaticChange",;
      THIS,"SetEdit",2)
    BINDEVENT(THIS.btnUndo,"Click",THIS,"Undo",2)
    BINDEVENT(THIS.btnSave,"Click",THIS,"Save",2)
  ENDPROC

Hago algo similar con el contenedor de datos. Ato un método contenedor de cliente (OnChange) a los eventos InteractiveChange() de cada TextBox. Entonces, en lugar de llamar al método personalizado EntityContainer.OnChange desde TextBox InteractiveChange(), EntityContainer define, una vez, que los eventos InteractiveChange() ejecutarán no solo cualquier código que pueda estar en ellos, sino también el método OnChange(). En este caso, el método OnChange() no hace nada, sin embargo, el formulario puede vincular este método al método del contenedor de botones, llamado OnChange(), casualmente, que habilita los botones Guardar y Deshacer cuando hay cambios pendientes.


¿Por qué es esto algo bueno?


BindEvent() nos lleva un paso más cerca de implementaciones poco acopladas. Las clases se pueden escribir para administrar su propia unidad de trabajo, sin tener que estar diseñadas para pasar acciones y mensajes a un padre, que luego pasa la acción o el mensaje a otros objetos. La misma funcionalidad se logra simplemente uniendo los eventos en el contexto en el que cooperan, como en un formulario. Además, esto significa que incluso nuestro código de tiempo de ejecución puede, sobre la marcha, crear enlaces de eventos para un sistema dinámico. Y los controladores de eventos se pueden objetivar y, por lo tanto, adjuntar a los objetos tal como nos estamos acostumbrando a hacer con las reglas comerciales. Visual FoxPro 7.0 y ahora 8.0 ofrecen cada vez más formas de implementar la orientación a objetos en nuestras aplicaciones.


Listado de programas


Listado 1

LOCAL loForm AS FORM
loForm = NEWOBJECT("NoBindEvents")
loForm.SHOW(1)
DEFINE CLASS NoBindEvents AS FORM
  HEIGHT = 170
  WIDTH = 334
  CAPTION = "Acoplamiento suelto sin BindEvents"
  EditMode = .T.
  DirtyBuffer = .F.
  NAME = "NoBindEvents"
  *!* Container responsible for displaying data
  ADD OBJECT ObjPerson AS EntityContainer WITH ;
    TOP = 20, ;
    LEFT = 38, ;
    NAME = "objPerson", ;
    Label1.NAME = "Label1", ;
    Label2.NAME = "Label2", ;
    txtFirst.NAME = "txtFirst", ;
    txtLast.NAME = "txtLast"
  *!* Container responsible for managing buttons that can
  *!* trigger actions
  ADD OBJECT ObjDataCmd AS DataCommands WITH ;
    TOP = 100, ;
    LEFT = 38, ;
    NAME = "objDataCmd", ;
    btnEdit.NAME = "btnEdit", ;
    btnUndo.NAME = "btnUndo", ;
    btnSave.NAME = "btnSave"
  *!* When the edit mode changes, alert the data container
  PROCEDURE EditMode_Assign
    LPARAMETERS vNewVal
    THIS.EditMode = m.vNewVal
    THIS.ObjPerson.EnableControls(m.vNewVal)
  ENDPROC
  *!* Method parallels container actions. Used for mediation.
  PROCEDURE SAVE
    THIS.ObjPerson.SAVE()
  ENDPROC
  *!* Method parallels container actions. Used for mediation.
  PROCEDURE UNDO
    THIS.ObjPerson.UNDO()
  ENDPROC
  PROCEDURE DirtyBuffer_Assign
    LPARAMETERS vNewVal
    THIS.ObjDataCmd.OnChange()
  ENDPROC
  *!* One of the significant (public) events is when data changes.
  PROCEDURE ObjPerson.DirtyBuffer_Assign
    LPARAMETERS vNewVal
    STORE m.vNewVal TO ;
      THIS.DirtyBuffer, ;
      THIS.PARENT.DirtyBuffer
  ENDPROC
  *!* One of the significant (public) events is when the edit mode changes.
  PROCEDURE ObjDataCmd.EditMode_Assign
    LPARAMETERS vNewVal
    STORE vNewVal TO THIS.EditMode, THIS.PARENT.EditMode
  ENDPROC
  *!* When Undo is selected, the button container first tells the
  *!* mediator (form), so it can do whatever it needs to, and then
  *!* the container takes care of its internal business. In this case,
  *!* the container resets the buttons' enabled property.
  PROCEDURE ObjDataCmd.UNDO
    THIS.PARENT.UNDO()
    DODEFAULT()
  ENDPROC
  *!* When Save is selected, the button container first tells the
  *!* mediator (form), so it can do whatever it needs to, and then
  *!* the container takes care of its internal business. In this case,
  *!* the container resets the buttons' enabled property.
  PROCEDURE ObjDataCmd.SAVE
    THIS.PARENT.SAVE()
    DODEFAULT()
  ENDPROC
ENDDEFINE
*!* Container of Save, Undo, and Edit buttons...like a CommandGroup
DEFINE CLASS DataCommands AS CONTAINER
  WIDTH = 271
  HEIGHT = 49
  EditMode = .T.
  NAME = "DataCommands"
  DirtyBuffer = .F.
  *!* Uncheck Edit to lock the data against edits
  ADD OBJECT btnEdit AS CHECKBOX WITH ;
    TOP = 10, ;
    LEFT = 180, ;
    HEIGHT = 27, ;
    WIDTH = 79, ;
    CAPTION = "\<Editar", ;
    VALUE = .T., ;
    CONTROLSOURCE = "THIS.PARENT.EditMode", ;
    STYLE = 1, ;
    NAME = "btnEdit"
  *!* Button will undo changes since the last save
  ADD OBJECT btnUndo AS COMMANDBUTTON WITH ;
    TOP = 10, ;
    LEFT = 12, ;
    HEIGHT = 27, ;
    WIDTH = 84, ;
    CAPTION = "\<Deshacer", ;
    ENABLED = .F., ;
    NAME = "btnUndo"
  *!* Save pending changes
  ADD OBJECT btnSave AS COMMANDBUTTON WITH ;
    TOP = 10, ;
    LEFT = 96, ;
    HEIGHT = 27, ;
    WIDTH = 84, ;
    CAPTION = "\<Guardar", ;
    ENABLED = .F., ;
    NAME = "btnSave"
  *!* Provide Assign methods to properties that
  *!* represent significant (i.e. public) events. This is
  *!* helpful for leaving a hook for a container to tell a
  *!* mediator that something has happened.
  PROCEDURE EditMode_Assign
    LPARAMETERS vNewVal
    THIS.EditMode = m.vNewVal
  ENDPROC
  *!* Dirty buffer is a logical property reflecting whether
  *!* there are any changes to the data.
  PROCEDURE DirtyBuffer_Assign
    LPARAMETERS vNewVal
    THIS.DirtyBuffer = m.vNewVal
  ENDPROC
  *!* Only enable save and undo buttons if there are changes
  *!* to save or undo.
  PROCEDURE OnChange
    STORE .T. TO ;
      THIS.btnUndo.ENABLED, ;
      THIS.btnSave.ENABLED
  ENDPROC
  *!* Set the edit mode of the data (lock data against edits)
  PROCEDURE setedit
    LPARAMETERS tlEditMode
    THIS.EditMode = tlEditMode
  ENDPROC
  *!* In real life, the container might notify the
  *!* business object to save.
  PROCEDURE SAVE
    STORE .F. TO THIS.DirtyBuffer
    THIS.OnSave()
  ENDPROC
  *!* In real life, the container might notify the
  *!* business object to undo changes.
  PROCEDURE UNDO
    STORE .F. TO THIS.DirtyBuffer
    THIS.OnSave()
  ENDPROC
  *!* Once changes are saved or reversed, there aren't any more
  *!* pending changes, so disable these buttons.
  PROCEDURE OnSave
    STORE .F. TO THIS.btnUndo.ENABLED, THIS.btnSave.ENABLED
  ENDPROC
  *!* Container buttons simply call the container's parallel method
  PROCEDURE btnUndo.CLICK
    THIS.PARENT.UNDO()
  ENDPROC
  PROCEDURE btnSave.CLICK
    THIS.PARENT.SAVE()
  ENDPROC
ENDDEFINE
*!* Container of data objects, for editing, viewing, and so on.
DEFINE CLASS EntityContainer AS CONTAINER
  WIDTH = 227
  HEIGHT = 64
  NAME = "EntityContainer"
  DirtyBuffer = .F.
  *!* The usual textboxes and labels for displaying or editing
  *!* a first name and a last name.
  ADD OBJECT Label1 AS LABEL WITH ;
    BACKSTYLE = 0, ;
    CAPTION = "Nombre", ;
    HEIGHT = 17, ;
    LEFT = 19, ;
    TOP = 14, ;
    WIDTH = 60, ;
    NAME = "Label1"
  ADD OBJECT Label2 AS LABEL WITH ;
    BACKSTYLE = 0, ;
    CAPTION = "Apellido", ;
    HEIGHT = 17, ;
    LEFT = 117, ;
    TOP = 14, ;
    WIDTH = 60, ;
    NAME = "Label2"
  ADD OBJECT txtFirst AS TEXTBOX WITH ;
    HEIGHT = 23, ;
    LEFT = 14, ;
    TOP = 30, ;
    WIDTH = 100, ;
    VALUE = "Grace", ;
    NAME = "txtFirst"
  ADD OBJECT txtLast AS TEXTBOX WITH ;
    HEIGHT = 23, ;
    LEFT = 115, ;
    TOP = 30, ;
    WIDTH = 100, ;
    VALUE = "Hopper", ;
    NAME = "txtLast"
  PROCEDURE EnableControls
    LPARAMETERS tlEnable
    STORE tlEnable TO ;
      THIS.txtFirst.ENABLED, ;
      THIS.txtLast.ENABLED
  ENDPROC
  *!* Normally a save would result in data changing.
  PROCEDURE SAVE
    LOCAL loi AS OBJECT
    FOR EACH loi IN THIS.CONTROLS
      IF PEMSTATUS(loi,'OldVal',5)
        loi.OLDVAL = loi.VALUE
      ENDIF
    NEXT loi
  ENDPROC
  PROCEDURE UNDO
    LOCAL loi AS OBJECT
    FOR EACH loi IN THIS.CONTROLS
      IF PEMSTATUS(loi,'OldVal',5)
        loi.VALUE = loi.OLDVAL
      ENDIF
    NEXT loi
  ENDPROC
  PROCEDURE DirtyBuffer_Assign
    LPARAMETERS vNewVal
    THIS.DirtyBuffer = m.vNewVal
  ENDPROC
  PROCEDURE OnChange
  ENDPROC
  PROCEDURE txtFirst.INIT
    THIS.ADDPROPERTY('OldVal',THIS.VALUE)
  ENDPROC
  PROCEDURE txtFirst.GOTFOCUS
    THIS.OLDVAL = THIS.VALUE
  ENDPROC
  *!* Interactive change is important trigger for starting the process
  *!* of alerting all who might care that data has changed.
  PROCEDURE txtFirst.INTERACTIVECHANGE
    THIS.PARENT.DirtyBuffer = THIS.OLDVAL <> THIS.VALUE
  ENDPROC
  PROCEDURE txtLast.GOTFOCUS
    THIS.OLDVAL = THIS.VALUE
  ENDPROC
  PROCEDURE txtLast.INIT
    THIS.ADDPROPERTY('OldVal',THIS.VALUE)
  ENDPROC
  PROCEDURE txtLast.INTERACTIVECHANGE
    THIS.PARENT.DirtyBuffer = THIS.OLDVAL <> THIS.VALUE
  ENDPROC
ENDDEFINE

Listado 2

LOCAL loForm AS FORM
loForm = NEWOBJECT("BindEvents")
loForm.SHOW(1)
DEFINE CLASS BindEvents AS FORM
  HEIGHT = 170
  WIDTH = 334
  CAPTION = "Acoplamiento suelto con BindEvents"
  NAME = "BindEvents"
  *!* Container responsible for displaying data
  ADD OBJECT objPerson AS EntityContainer WITH ;
    TOP = 20, ;
    LEFT = 38, ;
    NAME = "objPerson", ;
    Label1.NAME = "Label1", ;
    Label2.NAME = "Label2", ;
    txtFirst.NAME = "txtFirst", ;
    txtLast.NAME = "txtLast"
  *!* Container responsible for managing buttons that can
  *!* trigger actions
  ADD OBJECT objDataCmd AS DataCommands WITH ;
    TOP = 100, ;
    LEFT = 38, ;
    NAME = "ObjDataCmd", ;
    btnEdit.NAME = "btnEdit", ;
    btnUndo.NAME = "btnUndo", ;
    btnSave.NAME = "btnSave"
  PROCEDURE INIT
    *!* Set up event bindings.
    *!* When the edit button is clicked, trigger the data container's enable method.
    BINDEVENT(THIS.objDataCmd,"SetEdit",THIS.objPerson,"EnableControls",2)
    *!* When data is being changed, trigger the button container refresh.
    BINDEVENT(THIS.objPerson,"OnChange",THIS.objDataCmd,"OnChange",2)
    *!* When Save is clicked, trigger the data container to save.
    BINDEVENT(THIS.objDataCmd,"Save",THIS.objPerson,"Save",2)
    *!* When Undo is clicked, trigger the data container to revert changes.
    BINDEVENT(THIS.objDataCmd,"Undo",THIS.objPerson,"Undo",2)
  ENDPROC
ENDDEFINE
DEFINE CLASS DataCommands AS CONTAINER
  WIDTH = 271
  HEIGHT = 49
  NAME = "DataCommands"
  DirtyBuffer = .F.
  ADD OBJECT btnEdit AS CHECKBOX WITH ;
    TOP = 10, ;
    LEFT = 180, ;
    HEIGHT = 27, ;
    WIDTH = 79, ;
    CAPTION = "\<Editar", ;
    VALUE = .T., ;
    STYLE = 1, ;
    NAME = "btnEdit"
  ADD OBJECT btnUndo AS COMMANDBUTTON WITH ;
    TOP = 10, ;
    LEFT = 12, ;
    HEIGHT = 27, ;
    WIDTH = 84, ;
    CAPTION = "\<Deshacer", ;
    ENABLED = .F., ;
    NAME = "btnUndo"
  ADD OBJECT btnSave AS COMMANDBUTTON WITH ;
    TOP = 10, ;
    LEFT = 96, ;
    HEIGHT = 27, ;
    WIDTH = 84, ;
    CAPTION = "\<Guardar", ;
    ENABLED = .F., ;
    NAME = "btnSave"
  PROCEDURE BindEvents
    *!* Instead of button's click() calling a method in the parent container,
    *!* Simply use the edit button's own event to trigger the parent to some action.
    BINDEVENT(THIS.btnEdit,"InteractiveChange",THIS,"SetEdit",2)
    BINDEVENT(THIS.btnEdit,"ProgrammaticChange",THIS,"SetEdit",2)
    BINDEVENT(THIS.btnUndo,"Click",THIS,"Undo",2)
    BINDEVENT(THIS.btnSave,"Click",THIS,"Save",2)
  ENDPROC
  *!* Method that can be called when data is changed.
  PROCEDURE OnChange
    IF .NOT. THIS.DirtyBuffer
      STORE .T. TO ;
        THIS.DirtyBuffer, ;
        THIS.btnUndo.ENABLED, ;
        THIS.btnSave.ENABLED
    ENDIF
  ENDPROC
  *!* Method that can be called when data is saved.
  PROCEDURE SAVE
    STORE .F. TO THIS.btnSave.ENABLED, ;
      THIS.btnUndo.ENABLED, ;
      THIS.DirtyBuffer
  ENDPROC
  *!* Method that can be called when data is reverted.
  PROCEDURE UNDO
    STORE .F. TO THIS.btnSave.ENABLED, ;
      THIS.btnUndo.ENABLED, ;
      THIS.DirtyBuffer
  ENDPROC
  PROCEDURE INIT
    THIS.BindEvents()
  ENDPROC
  PROCEDURE SetEdit
  ENDPROC
ENDDEFINE
DEFINE CLASS EntityContainer AS CONTAINER
  WIDTH = 227
  HEIGHT = 64
  NAME = "EntityContainer "
  ADD OBJECT Label1 AS LABEL WITH ;
    BACKSTYLE = 0, ;
    CAPTION = "Nombre", ;
    HEIGHT = 17, ;
    LEFT = 19, ;
    TOP = 14, ;
    WIDTH = 60, ;
    NAME = "Label1"
  ADD OBJECT Label2 AS LABEL WITH ;
    BACKSTYLE = 0, ;
    CAPTION = "Apellido", ;
    HEIGHT = 17, ;
    LEFT = 117, ;
    TOP = 14, ;
    WIDTH = 60, ;
    NAME = "Label2"
  ADD OBJECT txtFirst AS TEXTBOX WITH ;
    HEIGHT = 23, ;
    LEFT = 14, ;
    TOP = 30, ;
    WIDTH = 100, ;
    VALUE = "Grace", ;
    NAME = "txtFirst"
  ADD OBJECT txtLast AS TEXTBOX WITH ;
    HEIGHT = 23, ;
    LEFT = 115, ;
    TOP = 30, ;
    WIDTH = 100, ;
    VALUE = "Hopper", ;
    NAME = "txtLast"
  PROCEDURE EnableControls
    THIS.txtFirst.ENABLED = !THIS.txtFirst.ENABLED
    THIS.txtLast.ENABLED = !THIS.txtLast.ENABLED
  ENDPROC
  PROCEDURE BindEvents
    *!* When data is changed, alert the parent container.
    BINDEVENT(THIS.txtFirst,"InteractiveChange",THIS,"OnChange",3)
    BINDEVENT(THIS.txtLast, "InteractiveChange",THIS,"OnChange",3)
  ENDPROC
  PROCEDURE SAVE
    LOCAL loi AS OBJECT
    FOR EACH loi IN THIS.CONTROLS
      IF PEMSTATUS(loi,'OldVal',5)
        loi.OLDVAL = loi.VALUE
      ENDIF
    NEXT loi
  ENDPROC
  PROCEDURE UNDO
    LOCAL loi AS OBJECT
    FOR EACH loi IN THIS.CONTROLS
      IF PEMSTATUS(loi,'OldVal',5)
        loi.VALUE = loi.OLDVAL
      ENDIF
    NEXT loi
  ENDPROC
  PROCEDURE INIT
    THIS.BindEvents()
  ENDPROC
  PROCEDURE OnChange
  ENDPROC
  PROCEDURE txtFirst.INTERACTIVECHANGE
  ENDPROC
  PROCEDURE txtFirst.GOTFOCUS
    THIS.OLDVAL = THIS.VALUE
  ENDPROC
  PROCEDURE txtFirst.INIT
    THIS.ADDPROPERTY('OldVal',THIS.VALUE)
  ENDPROC
  PROCEDURE txtLast.INIT
    THIS.ADDPROPERTY('OldVal',THIS.VALUE)
  ENDPROC
  PROCEDURE txtLast.GOTFOCUS
    THIS.OLDVAL = THIS.VALUE
  ENDPROC
ENDDEFINE