24 de febrero de 2021

Los métodos Acces y Assign en Visual FoxPro

Artículo original: Access and Assign Methods in VFP (AccessAssign.pdf)
Autor: Doug Hennig
Traducido por: Luis María Guayán


Descarga de lo ejemplos: AccessAssignSamples.zip


Introducción

En mi opinión, una de los agregados más útiles y poderosos en Visual FoxPro son los métodos métodos Access y Assign. Puede parecer extraño que algo tan simple como esto pueda considerarse poderoso, pero en este documento veremos algunas razones muy convincentes para usar estos métodos, incluida la capacidad propiedades fuertemente tipeadas, validar automáticamente los valores de las propiedades, crear propiedades de solo lectura y soporte a clases Collection.

Métodos Access y Assign

¿Qué son los métodos Access y Assign? Si está familiarizado con Visual Basic, los métodos Access y Assign son como los métodos Get y Let. El método Access de una propiedad se activa automáticamente cada vez que algo intenta acceder al valor de la propiedad de alguna manera, como mostrar el valor o usarlo en un cálculo. Quien llama ve el valor de retorno del método como el valor de la propiedad, por lo que normalmente el método Access devolverá el valor de la propiedad. Sin embargo, probablemente pueda imaginar casos en los que haga algo primero, como realizar un cálculo, antes de devolver el valor. De manera similar, el método Assign se activa cada vez que algo intenta cambiar el valor de la propiedad. Al método Assign se le pasa el valor que quien llama quiere asignar a la propiedad, por lo que normalmente almacenará ese valor en la propiedad. Sin embargo, puede hacer muchas cosas con el valor especificado primero, como asegurarse de que sea el tipo de datos adecuado.

Los métodos Access y Assign tienen el mismo nombre que la propiedad con "_access" o "_assign" como sufijo; por ejemplo, para la propiedad Item, estos métodos se denominan Item_Access y Item_Assign. Los métodos Access y Assign son independientes; puede estar uno sin el otro.

Hay dos formas de crear los métodos Access y Assign: automática y manualmente. Para crearlos automáticamente, marque las casillas de verificación "Método Access" o "Método Assign" (o ambas) en el cuadro de diálogo "Nueva propiedad" cuando cree una nueva propiedad o en el cuadro de diálogo "Editar propiedad / método" para una propiedad existente. Para crearlos manualmente, cree métodos con el nombre apropiado. Independientemente de cómo los cree, Visual FoxPro automáticamente coloca código en estos métodos para aceptar los parámetros necesarios y obtener o establecer el valor de la propiedad (a menos que, por supuesto, cree el método en una definición DEFINE CLASS no visual).

Este es un método Access típico:

return This.Property

Este es un método Assign típico:

lparameters vNewVal
This.Property = vNewVal

Puede anular el comportamiento de estos métodos para hacer lo que quiera. Como ejemplo tonto, cree una instancia de la clase Fun en TEST.VCX y muestre el valor de la propiedad Count varias veces (SILLY.PRG lo hace por usted); mire el código en el método Count_Access para ver de dónde provienen los valores.

This_Access

Además los métodos Access y Assign de la propiedad, Visual FoxPro 6 admite un nuevo método de clase global llamado This_Access. Este método, que debe crear manualmente con el cuadro de diálogo "Nuevo método", se ejecuta cada vez que accede a un miembro (propiedad, método u objeto contenido) del objeto. Por ejemplo, cambiar la propiedad Height de un formulario hace que se active el método This_Access del formulario (si hay uno) antes de que se cambie el valor de la propiedad Height.

A This_Access se le pasa el nombre del miembro y debe devolver una referencia de objeto, normalmente This (podría devolver una referencia a un objeto diferente, pero solo lo haría en circunstancias muy específicas, ya que esencialmente está redirigiendo el acceso a un objeto diferente). Tenga en cuenta que, a diferencia de los métodos Access y Assign a propiedades específicas, This_Access no tiene control sobre el valor del miembro al que se accede, ya que al método no se le pasa el nuevo valor para asignar y no devuelve el valor del miembro.

El formulario de muestra THISACCESS.SCX proporciona un ejemplo simple que simplemente demuestra que This_Access se activa al mostrar el nombre del miembro al que se accede, utilizando el siguiente código:

lparameters cMember
? cMember
return This

El método Init de este formulario tiene el siguiente código:

This.Caption = 'This_Access Test'
&& displays "caption"
This.SFTextBox1.Value = 'Test'
&& displays "sftextbox1"

Cuando ejecute este formulario, no solo verá "caption" y "sftextbox1"; también verá los nombres de ciertos métodos y propiedades del formulario a medida que se accede a ellos en la clase en la que se basa éste formulario (SFForm en SFCTRLS.VCX).

Debido a que This_Access se dispara mucho, definitivamente querrá minimizar su uso y la cantidad de trabajo que tiene que hacer.

Notas sobre los métodos de Access y Assign: Si bien los métodos Access y Assign pueden ser muy poderosos, hay algunos problemas que debe tener en cuenta cuando considere su uso.

  • Se pueden crear métodos Access y Assign para propiedades nativas de Visual FoxPro. Por ejemplo, digamos que desea guardar y restaurar la posición de un formulario. Puede crear métodos Assign para las propiedades Top y Left que establezcan una propiedad lógica personalizada llamada lMoved a .T. En el método Destroy del formulario, puede verificar la propiedad lMoved y, si es .T., Guardar las propiedades Top y Left en algún lugar (como el Registro de Windows) y restaurarlas en el método Init del formulario. Sin embargo, actualmente los métodos Assign para determinadas propiedades no se activan cuando el valor se cambia de determinadas formas. Por ejemplo, es posible que sepa que la actualización de un Pageframe solo actualiza la página activa; tienes que actualizar manualmente otras páginas cuando se seleccionan, generalmente colocando código en el método Activate de cada página. Un gran uso de un método Assign sería actualizar la página seleccionada en un método ActivePage_Assign del Pageframe; esto eliminaría la necesidad de insertar manualmente el mismo código en cada página.
    lparameters tnNewPage
    This.ActivePage = tnNewPage
    This.Pages[tnNewPage].Refresh()
    Desafortunadamente, aunque este código se activa si cambia el valor de ActivePage mediante programación o usando el teclado (tabulando a una nueva página), no se activa cuando se cambia el valor al hacer Clic en una página con el ratón. De manera similar, el método Partition_Assign de un Grid no se activa cuando arrastra la barra divisora. Con suerte, Microsoft solucionará estas deficiencias.
  • Los métodos Access y Assign de una propiedad tipo Array se pasan al subíndice para el elemento del Array al que se accede. En el caso de asignar, los subíndices siguen al parámetro del nuevo valor.
  • Los métodos Access y Assign no se ejecutan en el momento del diseño; por ejemplo, cambiar el valor de una propiedad en la ficha de propiedades o en un constructor no activa el código en el método Assign de la propiedad. Esto tiene sentido si lo piensa: el Init y otros métodos del objeto tampoco se activan porque el objeto no se está ejecutando realmente. Estos métodos tampoco se activan cuando las propiedades se muestran o se modifican en las ventanas Watch o Locals.
  • En mi sistema de prueba, acceder a una propiedad sin un método Access tomó 0.000118 segundos y 0.000429 con uno. Asignar un valor de propiedad sin un método de Asign tomó 0,000127 segundos y 0,000540 con uno. En ambos casos, esto es aproximadamente 4 veces más lento en una comparación relativa, pero sigue siendo absolutamente rápido. Por lo tanto, si bien querrá usar los métodos Access y Assign cuando sean útiles, querrá minimizar los Access y Assign en un bucle. En su lugar, almacene el valor de la propiedad (que activa el método de acceso) en una variable y use esa variable para accesos repetitivos cuando sepa que el valor no cambiará. Lo mismo ocurre con Assign: use una variable cuando, por ejemplo, esté acumulando valores en un bucle, y luego almacene el valor de la variable en la propiedad (que activa el método Assign) cuando finalice el bucle.
  • Una forma de mejorar el rendimiento de un método Access o Assign es verificar el valor actual de la propiedad y evitar ejecutar código si no es necesario cambiar nada. Por ejemplo, en un método Assign, puede comparar el nuevo valor con el actual (después de usar VARTYPE() para asegurarse de que el nuevo valor es el tipo de datos correcto, por supuesto) y no hacer nada más si son iguales.
  • En algunas condiciones, es posible que desee establecer el valor de una propiedad en sí mismo en el método Init de la clase para asegurarse de que el método Assign se active y realice cualquier acción requerida.
  • No puede crear métodos Access y Assign para las propiedades nativas de un control ActiveX (bueno, puede crearlos, pero nunca se activan). Sin embargo, puede crear y usar métodos Access y Assign para el Visual FoxPro OLEContainer en el que está alojado el control ActiveX.
  • Actualmente hay un error con This_Access: no se dispara si se accede a un miembro en un bloque WITH. He aquí un ejemplo; obtendrá un error en la asignación de Property3 y Property4.
     oObject = createobject('Test')
     oObject.Property1 = 'test'
     oObject.Property2 = 'test'
     with oObject
      .Property3 = 'test'
      .Property4 = 'test'
     endwith
     define class Test as Custom
      function This_Access
       lparameters tcMember
       if not pemstatus(This, tcMember, 5)
        This.AddProperty(tcMember)
       endif not pemstatus(This, tcMember, 5)
       return This
      endfunc
     enddefine

Usos de los métodos Access y Assign

Los usos que he descrito a continuación no son exhaustivos. Este es un catálogo de usos que se me ocurrieron o vi que otros usan. En otras palabras, es un conjunto de patrones de diseño para acceder y asignar métodos.

Creación de propiedades de "acción"

Algunas propiedades nativas de Visual FoxPro disparan una acción cuando se cambian sus valores. Por ejemplo, cambiar las propiedades Height o Width de un formulario hace que se cambie el tamaño del formulario. Un método Assign le permite hacer lo mismo con una propiedad personalizada o nativa que no tiene este comportamiento.

Este es un uso muy potente de los métodos Assign: las vueltas que solía tener que hacer para garantizar que algo suceda cuando las condiciones cambian se eliminan porque el código adecuado se ejecuta automáticamente. En otras palabras, todavía tiene que escribir el código para realizar la acción, pero puede eliminar el código que administra cuando ocurre la acción, que puede ser complejo, torpe y casi siempre más propenso a errores que el código de acción real.

Por ejemplo, en una clase que he creado, quería saber si se cambió el tamaño del objeto o si se movió, ya sea visual o programáticamente. Antes de Visual FoxPro 6, tenía que almacenar los valores originales en propiedades personalizadas (probablemente en el Init de la clase) y luego, cuando necesito saber si el objeto fue redimensionado o movido, comparar los valores actuales con los guardados. Ahora, simplemente he creado métodos Assign para las propiedades Height, Width, Top, y Left, y estos métodos establecieron las propiedades personalizadas lSized y lMoved en .T. Se requiere menos código y menos propiedades personalizadas.

Aquí hay otro ejemplo: suponga que desea que todos los objetos de un formulario realicen alguna acción, como cambiar su tamaño cuando se cambia el tamaño del formulario. Para pasar un mensaje a cada objeto, indicándole que realice la acción, debe desplazarse por la colección Controls del formulario. Sin embargo, eso no es suficiente para acceder a todos los controles; también tiene que profundizar en los contenedores (y los contenedores que contienen) para llegar a cada objeto. En cambio, si cada objeto tiene una propiedad (como lResize) que tiene un método Assign para realizar la acción deseada, simplemente tendrá que usar un código como el siguiente para desencadenar esta acción para todos los objetos:

This.SetAll('lResize', .T.)

Here are some examples from the FoxPro Foundation Classes (FFC):

  • _Output (in _REPORTS.VCX): the assign methods of several properties (such as cAlias and cDestination) call the SetDestinations() method to ensure that changes to these properties are applied to dependent properties.
  • _HyperlinkCommandButton (_HYPERLINK.VCX): lVisited_Assign sets the color of the button to the “visited” color, while nVisitedForeColor_Assign sets the color of the button to the new color if lVisited is .T. cTarget_Assign sets the ToolTipText for the button to the specified URL.
  • _FindDialog (_TABLE.VCX): lAdvanced_Assign makes those controls used in “advanced” mode visible or not.

Validación de propiedad

Visual FoxPro tiene una verificación de validez incorporada para las propiedades de los objetos nativos, tanto para el tipo de datos (por ejemplo, no puede establecer la propiedad Height con una cadena) como para el rango (el BorderStyle de un formulario puede ser de 0 a 3). Si bien puede probar la validez de las propiedades personalizadas en los métodos que las usan (a menudo se hace con declaraciones ASSERT, ya que los valores de propiedad inválidos suelen ser un error del programador), ¿no sería bueno aplicar esto inmediatamente cuando se almacena un valor inválido como lo hace Visual FoxPro? Con los métodos Assign, puede hacerlo. Aquí tiene un ejemplo:

lparameters tuNewValue
assert vartype(tuNewValue) = 'N' ;
  message 'Invalid data type'
assert between(tuNewValue, 1, 5) ;
  message 'Invalid value'
This.Property = tuNewValue

Este código garantiza que la propiedad se establezca en un valor numérico entre 1 y 5. En este caso, se usó ASSERT para la prueba, pero también podría usar declaraciones IF o CASE y desencadenar un error usando el comando ERROR si la prueba falla.

Aquí hay algunos ejemplos del FFC (FoxPro Foundation Classes):

  • _FileVersion (_UTILITY.VCX): cFileName tiene un método Access (la validación de la propiedad generalmente, pero no siempre, se realiza en el método Assign) que usa GETFILE() para obtener un nombre de archivo si la propiedad está vacía, sin caracteres o si el archivo no existe.
  • _Output (_REPORT.VCX): cReport_Assign asegura que el archivo especificado existe, y cDisplayFontName_Assign asegura que la fuente especificada existe en AFONT().
  • _FilterExpr (_TABLE.VCX): El método Assign de cFilter elimina el retorno de carro, el salto de línea y los caracteres de tabulación del valor especificado (estos pueden haber entrado en la cadena cuando el usuario escribió en un cuadro de edición, por ejemplo).

Crear propiedades de solo lectura

Muchos objetos de Visual FoxPro tienen propiedades de solo lectura. Por ejemplo, la propiedad ControlCount de las clases de contenedor (como los formularios) contiene el número de controles en el contenedor. No puede cambiar el valor de ControlCount directamente; intentar hacerlo provoca un error de "La propiedad es de solo lectura". A veces, sería bueno que las propiedades personalizadas fueran de solo lectura. Por ejemplo, como veremos más adelante, una clase de colección necesita una propiedad Count, y no querrá que nada fuera de la clase pueda cambiar su valor directamente.

Antes de Visual FoxPro 6, no había forma de hacer esto directamente con propiedades personalizadas; había que proteger la propiedad y crear un método público para devolver su valor. A partir de Visual FoxPro 6, esto se hace fácilmente: simplemente cree un método Assign para la propiedad que no almacena el valor en la propiedad (y posiblemente también desencadena el error númeron 1743 usando el comando ERROR). La clase ROTest1 en TEST.VCX muestra esto; para verlo en acción, haga lo siguiente o ejecute ROTEST1.PRG:

x = newobject('rotest1', 'test')
x.Count = 10

Sin embargo, existe una complicación: esta propiedad ahora es completamente de solo lectura; ni siquiera los métodos de la clase pueden cambiar el valor de la propiedad. Para ver esto, intente llamar al método Test() de la clase ROTest1, que simplemente intenta establecer el valor de Count en 5. Entonces, el problema es: ¿cómo hacer que sea de solo lectura para todo lo que está fuera de la clase, excepto lectura-escritura dentro de la clase?

Hay (al menos) un par de formas de manejar esto. Una es usar una propiedad lógica protegida que, cuando el valor sea .T., Indica que un método interno está tratando de cambiar el valor, por lo que el cambio está permitido. Dado que la propiedad (llamada, por ejemplo, lInternal) está protegida, solo se puede establecer en .T. por métodos internos. El método Count_Assign de la clase ROTest2 (en TEST.VCX) permite cambiar el valor de la propiedad cuando lInternal es .T .:

lparameters tuNewValue
if This.lInternal
  This.Count = tuNewValue
else
  error 1743
endif This.lInternal
Here’s the Test method of this class:
This.lInternal = .T.
This.Count = 5
This.lInternal = .F.

Para probar esto, cree una instancia de ROTest2, intente establecer Count en 5, luego llame al método Test() (ROTEST2.PRG hace esto). A diferencia de la clase ROTest1, el método Test() funciona, mostrando que la propiedad es de lectura y escritura en los métodos internos.

Un inconveniente de esta propuesta es tener que configurar lInternal en .T. y regresarlo a .F. en cada método que necesite cambiar el valor de una propiedad de solo lectura. Otro esquema, desarrollado por Toni Feltman, no requiere nada especial en ningún método que cambie el valor; en su lugar, el método de asignación llama a un método CalledByThisClass() personalizado para determinar si el código que intentó cambiar el valor proviene de un método de la clase. Aquí está el código para Count_Assign en la clase ROTest3 en TEST.VCX:

lparameters tuNewValue
if This.CalledFromThisClass()
  This.Count = tuNewValue
else
 error 1743
endif This.CalledFromThisClass()
Here’s the code for CalledFromThisClass:
local lnLevel, ;
 lcProgram, ;
 lcObject, ;
 lcThisName, ;
 loParent, ;
 llReturn
lnLevel = program(-1)
lcProgram = iif(lnLevel > 2, ;
 upper(program(lnLevel - 2)), '')
lcObject = left(lcProgram, rat('.', lcProgram) - 1)
lcThisName = This.Name
loParent = iif(type('This.Parent') = 'O', ;
 This.Parent, .NULL.)
do while vartype(loParent) = 'O'
 lcThisName = loParent.Name + '.' + lcThisName
 loParent = iif(type('loParent.Parent') = 'O', ;
 loParent.Parent, .NULL.)
enddo while vartype(loParent) = 'O'
llReturn = upper(lcObject) == upper(lcThisName)
return llReturn

ROTest3 funciona igual que ROTest2, pero su método de prueba es igual que en ROTest1 (es decir, simplemente asigna un valor a Count), que falló.

Cálculo de valores de propiedad cuando sea necesario

Algunas propiedades contienen valores calculados. Un ejemplo simple es una propiedad Área, que es el producto de las propiedades Height y Width. Decidir cuándo calcular el valor puede resultar complicado, especialmente si el cálculo requiere mucho tiempo. Puede cambiar el valor calculado siempre que cambie alguna de las cosas de las que depende el cálculo, pero esto puede no ser eficaz si los valores dependientes cambian con frecuencia y el valor calculado no se necesita de inmediato. Un ejemplo es un formulario de ingreso de datos en el que el usuario ingresa varios valores que están involucrados en el cálculo; es posible que no desee ralentizar la entrada de datos recalculando el valor después de cada entrada. Sin embargo, no forzar un nuevo cálculo en cada cambio de valor dependiente significa llamar específicamente a un método para realizar el cálculo una vez que sea necesario.

Afortunadamente, existe una solución simple para esto a partir de Visual FoxPro 6: realizar el cálculo en el método Access de la propiedad. De esa manera, no tiene la sobrecarga del cálculo hasta que realmente lo necesita, pero el mecanismo para calcularlo es transparente (es decir, no tiene que llamar a un método para forzar el cálculo antes de obtener el valor de la propiedad). Un ejemplo de esto es la propiedad Count de la clase de colección que veremos más adelante. Esta propiedad realmente solo contiene la cantidad de filas no vacías en una propiedad tipo Array, pero en lugar de tener que actualizarla cada vez que se agrega o elimina una fila del Array, simplemente la calculamos cuando sea necesario utilizando el siguiente código para su acceso método:

local lnCount
with This
  lnCount = alen(.Item)
  lnCount = iif(lnCount = 1 and ;
  isnull(.Item[1]), 0, lnCount)
endwith
return lnCount

Observe algo interesante aquí: el valor de Count, de hecho, nunca cambia; el método Access simplemente devuelve el valor adecuado. Realmente no es necesario tener un valor en Count, ya que cada vez que algo quiere su valor, calculamos y devolvemos el valor. Como resultado, esta propiedad también tiene un método Assign que hace que la propiedad sea de solo lectura simplemente desencadenando el error número 1743 y sin preocuparse por quién intentó cambiar el valor.

Creación de instancias retardada

Relacionado con el uso anterior es retardar la creación de instancias de un objeto miembro hasta que sea realmente necesario. Por ejemplo, una clase de combinación de Word puede tener una propiedad oWord que hace referencia a la aplicación de Word. Dado que crear instancias de Word puede llevar algo de tiempo y memoria, ¿por qué hacerlo hasta que sea absolutamente necesario? Si el usuario hace Click en el botón Cancelar en un cuadro de diálogo con esta clase, Word no se utilizará realmente, por lo que no tiene sentido crear una instancia en el Init de la clase. En cambio, el método Access de oWord puede comprobar si la propiedad contiene .NULL. y de ser así, crear una instancia de Word la primera vez que sea necesario.

Otro ejemplo de este patrón es la clase FFC _HyperlinkCommandButton (_HYPERLINK.VCX). Su propiedad oHyperlink contiene una referencia a un objeto de hipervínculo que hace el trabajo real de vincular el botón a algo, pero si el usuario nunca hace Click en el botón, este objeto no será necesario. Por lo tanto, no se crea una instancia de este objeto hasta que se accede a la propiedad oHyperlink por primera vez.

Conversión de datos

Algunas propiedades contienen valores que podrían expresarse de manera diferente en una escala diferente. Por ejemplo, la propiedad BackColor de un objeto espera un valor numérico (aunque el valor que se muestra en la Hoja de propiedades suele ser los parámetros que se pasarían a la función RGB(), que devuelve un resultado numérico). Con un método Assign, podría permitirle aceptar valores de cadena que representen el color deseado. Aquí tienes un ejemplo:

lparameters tuColor
local lnColor, lcColor
do case
 case vartype(tuColor) $ 'NFIBY' and tuColor >= 0
  This.BackColor = int(tuColor)
 case vartype(tuColor) = 'C'
  lcColor = upper(tuColor)
 do case
  case lcColor == 'RED'
   lnColor = rgb(255, 0, 0)
  case lcColor == 'GREEN'
   lnColor = rgb(0, 255, 0)
  case lcColor == 'BLUE'
   lnColor = rgb(0, 0, 255)
  otherwise
   lnColor = -1
   error 1560
 endcase
 if lnColor >= 0
  This.BackColor = lnColor
 endif lnColor >= 0
 otherwise
 error 1732
endcase

Este código aceptará un valor numérico, 'RED', 'BLUE' y 'GREEN' como valores válidos. COLORS.SCX proporciona un ejemplo de esto. Otro ejemplo es la conversión entre unidades de medida, como entre los sistemas métrico y "medieval". El método de asignación de un valor de temperatura, por ejemplo, podría verificar la configuración de una propiedad de las unidades (ya sea "C" para Celsius o "F" para Fahrenheit) y calcular automáticamente la temperatura para la otra escala. TEMPERATURE.SCX proporciona un ejemplo sencillo de esto. Otros ejemplos son conversiones entre longitudes (como metros, pies y yardas), áreas (acres y hectáreas), volúmenes (galones y litros) y monedas (dólares estadounidenses y canadienses, que requerirán un factor de conversión variable).

Mantenimiento de una Fachada

Las clases bien diseñadas separan la interfaz visual de la implementación; esto le permite variar uno, sin afectar al otro y proporcionar las funciones de implementación en entornos no visuales. Un ejemplo es un objeto "buscar" que localiza registros según los criterios de búsqueda especificados. Esta clase se puede usar en muchos lugares además de un formulario, por lo que poner el código en el método Click de un botón de comando no es la mejor manera. En su lugar, cree un componente de búsqueda no visual con propiedades para los criterios de búsqueda y utilice este componente siempre que sea necesario realizar una búsqueda. A continuación, puede convertir este objeto en otro componente reutilizable.

Sin embargo, surge una complicación: ¿cómo aborda las propiedades de los criterios y el método de búsqueda del objeto de búsqueda? No querrá usar una referencia como Container.oFind.cSearchString y Container.oFind.Search() porque eso significa que debe saber que oFind es el nombre del objeto de búsqueda dentro del contenedor, y los nombres de codificación rígida son un cosa mala. En su lugar, utilice un patrón de diseño de Fachada: cree propiedades de criterios y un método de búsqueda del contenedor y utilícelos en su lugar; por ejemplo, Container.cSearchString y Container.Search(). Este es una propuesta mucho mejor porque oculta el objeto de búsqueda dentro del contenedor del mundo exterior (nada que use el contenedor debería saber cómo el contenedor implementa la búsqueda). Sin embargo, esto requiere que antes de que Container.Search() llame a oFind.Search(), cada una de las propiedades de los criterios se escriban en la propiedad oFind apropiada, y cuando se realiza la búsqueda, las propiedades de oFind (como una marca de éxito) son escrito en las propiedades apropiadas del contenedor.

Podemos simplificar esto mucho usando métodos Access y Assign. Cada propiedad de criterios de contenedor tiene un método Assign que escribe el valor especificado en la propiedad oFind apropiada y un método Access que devuelve el valor de la propiedad oFind. Ahora, Container.Search() no tiene más que una llamada a oFind.Search().

El objeto de búsqueda FFC (_TableFind en _TABLE.VCX) es un ejemplo de este patrón. La clase _FindButton (también en _TABLE.VCX) es un componente visual que usa _TableFind para hacer su trabajo, y sus diversas propiedades (como cAlias, cFindString y lFindAgain) tienen métodos Access y Assign como se describió anteriormente.

Otro ejemplo de soporte de una Fachada es la clase FFC _GraphByRecord (_UTILITY.VCX). Esta clase es un contenedor para un objeto de gráfico y tiene varias propiedades que afectan la forma en que aparece el gráfico, como lAddLegend, lSeriesByRow y nChartType. Cada uno de estos tiene un método Assign que establece la propiedad de gráfico adecuada y actualiza el gráfico. De esta manera, puede tener fácilmente un control visual, como una casilla de verificación, que tiene la propiedad apropiada del objeto _GraphByRecord como su ControlSource y no se requerirá codificación para que funcione.

Soportando un Decorador

El patrón de diseño Decorador proporciona una forma de potenciar el comportamiento de un objeto sin subclasificar el objeto. Los Decoradores son útiles cuando, por ejemplo, no tiene el código fuente del objeto que desea potenciar, por lo que no puede subclasificarlo. La forma en que trabaja un Decorador es que se sienta entre el objeto que se está decorando y cualquier cosa que necesite los servicios de ese objeto, y se presenta como si fuera el objeto. Cualquier mensaje que se transmita al Decorador se transmite directamente al objeto, se envuelve en otro comportamiento (comportamiento previo y/o posterior) y se pasa al objeto, o el Decorador lo maneja completamente.

A continuación, se muestra un ejemplo en el que podría utilizarse un Decorador. Su cliente tiene una clase de manejo de datos con un método de consulta que crea un cursor a partir de algunos datos. Le piden que ponga los datos a disposición de VB y Excel. Se da cuenta de que esto se puede hacer fácilmente subclasificando la clase, convirtiéndola en una clase OLEPublic (por lo que es un objeto COM que se puede llamar desde VB y Excel), y anulando el método Query para convertir el cursor resultante en un conjunto de registros ADO que retorna.

Sin embargo, el cliente le informa que no tiene el código fuente de la clase (tal vez sea parte de una biblioteca que compró). Entonces, en lugar de una subclase, crea una clase Decoradora. Una de las complejidades de crear un Decorador es exponer la misma interfaz que el objeto decorado. Después de todo, si el Decorador se presenta a sí mismo como si fuera el objeto decorado, debe tener las mismas propiedades y métodos públicos que el objeto decorado. Esto puede ser una tarea que requiere mucho tiempo y, además, puede resultar difícil exponer las propiedades del objeto decorado si el objeto mismo puede cambiar esas propiedades.

This_Access al rescate! Este método, que se llama cuando se accede a cualquier miembro del objeto, puede devolver una referencia de objeto al objeto decorado en lugar de This para redirigir el acceso al objeto decorado. Sin embargo, el Decorador puede procesar ciertas propiedades y métodos retornando This. Así es como podría verse este código:

lparameters tcMember
if tcMember = 'query' or ;
  vartype(This.oDecorated) <> 'O' or ;
  not pemstatus(This.oDecorated, tcMember, 5)
  return This
else
  return This.oDecorated
endif tcMember = 'query' ...

Este código ejecutará el método de consulta del Decorador, pero redirigirá todos los demás mensajes al objeto decorado.

DECORATOR.PRG, amablemente proporcionado por Steven Black, demuestra cómo un objeto Decorador puede exponer las propiedades y métodos de un objeto decorado automáticamente.

Colecciones

Una colección es un grupo de cosas relacionadas. En Visual FoxPro, las colecciones están en todas partes: _SCREEN tiene una colección de formularios, un formulario tiene una colección de controles, un PageFrame tiene una colección de páginas, etc. Aunque se implementan de manera diferente en diferentes lugares, las colecciones generalmente tienen algunas cosas en común:

  • Tienen una propiedad que devuelve el número de elementos de la colección. En el caso de la colección Forms, esta propiedad se llama FormCount; para los controles, se llama ControlCount; para Pages, se llama PageCount. En el caso de las colecciones de ActiveX, con frecuencia se denomina Count, lo que facilita su manejo en código genérico. A veces, esta propiedad es de lectura y escritura (como PageCount), pero normalmente son de solo lectura.
  • Tienen una forma de agregar y eliminar elementos de la colección. Con las colecciones nativas de Visual FoxPro, la forma varía; La instanciación de un nuevo formulario lo agrega automáticamente a la colección de formularios, mientras que las nuevas páginas se crean aumentando la propiedad PageCount. Con las colecciones de ActiveX, generalmente hay métodos Add o AddItem, Remove o RemoveItem y Clear.
  • Tienen una forma de acceder a los elementos de la colección, generalmente mediante un número de índice. Para la colección de formularios, es Forms [<indez>]. Para Pages, es Pages [<index>]. Las colecciones de ActiveX suelen tener un método de elemento que puede aceptar un número de índice o el nombre del elemento deseado. Hacer referencia a un elemento de una colección por nombre es más fácil (y a menudo requiere menos código) que por número de índice. Algunas colecciones de Visual FoxPro (como la colección Forms de _SCREEN) no permiten esto, pero algunas (como las colecciones Forms y Projects de _VFP) sí lo permiten.

Últimamente me he encontrado creando y usando muchas colecciones. A menudo, estas colecciones solo constan de una clase que contiene un Array de referencias a objetos. A veces, los objetos de una colección son muy simples: no tienen ningún método, solo son poseedores de propiedades. Otras veces, son objetos más complejos, como formas. Estas son algunas de las razones que se me ocurren para usar colecciones:

  • Con frecuencia, las colecciones son reemplazados por Arrays; Originalmente creé mi clase de colección para reemplazar una propiedad tipo Array con muchas columnas. Descubrí que siempre me olvidaba en qué columna debía entrar cierta información. Es mucho más fácil comprender y mantener el código que simplemente establecer u obtener propiedades de los objetos de una colección que trabaja con filas y columnas.
  • Dado que una colección es un objeto, es más fácil pasar a un método que a un Array. Por ejemplo, con un Array, debe usar @ para asegurarse de que se pase por referencia, pero no puede hacer eso con una propiedad tipo Array, por lo que termina copiando la propiedad Array a un Array local, pasando el Array local al método, luego (si el método cambió el Array) copiando el Array de nuevo a la propiedad Array. Oh, pero no olvide volver a dimensionar la propiedad Array primero, o podría obtener un error. Con una colección, puede omitir varias líneas de código feo. Pasar Arrays es aún más complejo cuando el objeto al que se lo está pasando es un objeto COM que no es de VFP, que puede no tratar los Arrays de la misma manera que lo hace Visual FoxPro.
  • Puede ser más fácil buscar algo en una colección que en un Array. ASCAN busca en todas las columnas, no solo en la que le interesa, por lo que puede encontrar coincidencias falsas. Además, si el Array contiene referencias a objetos, no podrá utilizar ASCAN en absoluto. Con una colección, puede codificar el comportamiento de búsqueda exacto que desea en su clase de colección, luego simplemente llame al método apropiado.
  • Puede proteger más fácilmente el contenido de una colección que una propiedad tipo Array expuesta.

Estos son algunos de los lugares donde se pueden usar las colecciones:

  • Una clase contenedora de menú: un menú es una colección de pads, cada uno de los cuales es una colección de barras (en realidad, los pads hacen referencia a ventanas emergentes, pero la clase contenedora podría ocultar esta complejidad). Sería mucho más fácil trabajar con un menú orientado a objetos que con el sistema de menú actual de VFP, que ha cambiado poco desde FoxPro 2.0.
  • Una clase de entorno de datos personalizada: el entorno de datos de VFP consta de dos colecciones: cursores y relaciones. Un sustituto del entorno de datos nativo podría proporcionar más funcionalidad, como métodos para guardar y revertir tablas en lugar de poner este código en un formulario.
  • La colección de formularios de un objeto de aplicación: a diferencia de la colección de formularios de _SCREEN, que solo tiene una referencia de objeto a cada formulario abierto, la colección de formularios de un objeto de aplicación podría contener información más útil, como a qué barras de herramientas hacen referencia, si admiten múltiples instancias o no, ya sea que agreguen sus títulos al menú Ventana, etc.
  • Clases de servidor de Automatización de VFP: una de las cosas que facilita el trabajo con servidores de Automatización como Word y Excel son sus modelos de objetos enriquecidos, que generalmente consisten en muchas colecciones de objetos similares. Muchos desarrolladores de Visual FoxPro están desarrollando modelos de objetos similares para sus herramientas o aplicaciones, lo que les permite ser utilizados de muchas más formas que una simple interfaz de usuario de Visual FoxPro.

Bien, entonces, ¿qué tienen que ver las colecciones con los métodos Access y Assign? Como veremos en unos momentos, las colecciones se pueden implementar mucho mejor desde Visual FoxPro 6 debido a los métodos Access y Assign.

Para facilitar el trabajo con colecciones de objetos, creé un par de clases abstractas destinadas a ser subclasificadas en clases de colección específicas: SFCollection y SFCollectionOneClass. SFCollection (que se encuentra en SFCOLLECTION.VCX) es una subclase de SFCustom, mi clase base personalizada que se encuentra en SFCTRLS.VCX. La versión Visual FoxPro 5 de SFCollection tiene un array aItems protegida que contiene referencias a los elementos de la colección y un método Item que devuelve una referencia de objeto al elemento especificado. ¿Por qué utilizar un método para acceder a los elementos de la colección en lugar de simplemente abordar el array aItems directamente? Porque quería poder hacer referencia a elementos de la colección de la misma manera que lo permiten los controles ActiveX y las colecciones de VFP más nuevas, ya sea por número de índice o por nombre. Aquí tienes un ejemplo:

oCollection.Item(5)
oCollection.Item('Customer')

Hasta Visual FoxPro 5 no se permitia que se acceda a un elemento en un array por nada que no sea un subíndice numérico, por lo que no podemos acceder a elementos como este.

A partir de la versión Visual FoxPro 6 de SFCollection también se basa en SFCustom, pero hace las cosas de manera un poco diferente. Debido a que ahora tenemos métodos Access y Assign, podemos usar un array llamado Item que contiene los elementos de la colección y acceder a este array directamente.

El método Access de Item acepta un parámetro numérico o de caracteres. Si el parámetro es numérico, asumimos que es el número de elemento del array. Si es caracter, buscamos un artículo con ese nombre.

La ventaja de utilizar esta propuesta es que en la versión de Visual FoxPro 6, Item es una propiedad, no un método. Esto significa que puede usarlo para hablar directamente con un elemento de la colección. Por ejemplo, puede utilizar:

oCollection.Item[<index>].Property

Hasta la versión de Visual FoxPro 5, se debía utilizar:

loObject = oCollection.Item(<index>)
loObject.Property

porque Item es un método, no una propiedad, en esta versión. A partir de Visual FoxPro 6 actúa más como colecciones nativas en controles VFP y ActiveX. Otra ventaja de los métodos Access y Assign: Count es una propiedad de solo lectura en la versión de Visual FoxPro 6 gracias a un método Assign. En la versión de Visual FoxPro 5, se pueden almacenar valores incorrectos.

Aquí está el código para el método AddItem:

lparameters tcClass, ;
 tcLibrary, ;
 tcName
local lnCount, ;
 loItem
with This
 .lInternal = .T.
 lnCount = .Count + 1
 dimension .Item[lnCount], .aNames[lnCount]
 loItem = newobject(tcClass, tcLibrary)
 .Item[lnCount] = loItem
 if vartype(tcName) = 'C' and not empty(tcName)
 .aNames[lnCount] = tcName
 if not ' ' $ tcName and isalpha(tcName)
 loItem.Name = tcName
 endif not ' ' $ tcName ...
 endif vartype(tcName) = 'C' ...
 .lInternal = .F.
endwith
return loItem

Este método acepta tres parámetros: el nombre de la clase para crear el nuevo elemento, la biblioteca en la que está definida la clase y el nombre para asignar al elemento. AddItem crea un objeto de la clase especificada, almacena el objeto en una nueva fila en el array Item y almacena el nombre especificado para que pueda usarse más tarde para encontrar el elemento (en el array protegida aNames). Si el nombre es un nombre válido de Visual FoxPro, el nombre también se almacena en la propiedad Name del nuevo objeto. Luego se devuelve una referencia de objeto al nuevo elemento para que el código que llama a este método pueda establecer propiedades o llamar a métodos del nuevo elemento. Tenga en cuenta que la propiedad Count no se incrementa; se calcula cuando se requiere en su método Access.

El método RemoveItem acepta el índice o el nombre del elemento para eliminar como parámetro. Si se puede encontrar el elemento especificado (utilizando el método GetIndex para encontrar el número de elemento del elemento), el elemento se elimina de los arrays de elementos y nombres. Al igual que con AddItem, el valor de la propiedad Count no cambia. Aquí está el código para este método:

lparameters tuIndex
local llReturn, ;
 lnIndex, ;
 lnCount
with This
 .lInternal = .T.
 lnIndex = .GetIndex(tuIndex)
 if lnIndex > 0
 adel(.Item, lnIndex)
 adel(.aNames, lnIndex)
 lnCount = .Count
 if lnCount = 0
 .InitArray()
 else
 dimension .Item[lnCount], .aNames[lnCount]
 endif lnCount = 0
 llReturn = .T.
 endif lnIndex > 0
 .lInternal = .F.
endwith
return llReturn
Here’s Item_Access, the access method for the Item array property:
lparameters tuIndex
local lnIndex, ;
 loReturn
with This
 lnIndex = iif(.lInternal, tuIndex, ;
 .GetIndex(tuIndex))
 loReturn = iif(lnIndex = 0, .NULL., .Item[lnIndex])
endwith
return loReturn

Este método acepta el índice o el nombre del elemento deseado como parámetro y, si ese elemento existe, devuelve una referencia de objeto.

El método GetIndex se llama desde varios otros métodos para convertir un índice o nombre de elemento especificado en el número de elemento apropiado en el array de elementos (o 0 si no se encontró el elemento).

lparameters tuIndex
local lnCount, ;
 lnIndex, ;
 lcName, ;
 lnI
with This
 lnCount = .Count
 lnIndex = 0
 do case
* If the index was specified as a string, try to find
* a member object with that name.
 case vartype(tuIndex) = 'C'
 lcName = upper(alltrim(tuIndex))
 for lnI = 1 to lnCount
 if upper(.aNames[lnI]) == lcName
 lnIndex = lnI
exit
 endif upper(.aNames[lnI]) == lcName
 next lnI
* If the index wasn't numeric, give an error.
 case not vartype(tuIndex) $ 'NIFBY'
 error cnERR_DATA_TYPE_MISMATCH
* If the index was numeric, give an error if it's
* out of range.
 otherwise
 lnIndex = int(tuIndex)
 if not between(lnIndex, 1, lnCount)
 error cnERR_INVALID_SUBSCRIPT
lnIndex = 0
 endif not between(lnIndex, 1, lnCount)
 endcase
endwith
return lnIndex

Count_Access, el método Access para la propiedad Count, calcula automáticamente el valor cada vez que se accede a Count (de hecho, verá en este código que el valor de Count nunca se cambia; el método Access simplemente devuelve el valor correcto). Aquí está el código:

local lnCount
with This
 lnCount = alen(.Item)
 lnCount = iif(lnCount = 1 and isnull(.Item[1]), ;
 0, lnCount)
endwith
return lnCount

Hay algunos otros métodos en esta clase: Count_Assign asegura que esta propiedad sea de solo lectura, Init llama al método protegido InitArray para inicializar el array de elementos y Destroy destruye todas las referencias de objeto.

Creé una muestra simple para mostrar cómo funcionan las colecciones. FIELDS.PRG abre la base de datos VFP TESTDATA y crea una colección de campos en la tabla CUSTOMER. Omite el campo de clave principal (CUST_ID), ya que probablemente no queremos que el usuario lo vea. También utiliza un lindo título para cada campo en lugar del nombre del campo. Estos subtítulos se muestran en un cuadro de lista utilizando FIELDS.SCX. Al seleccionar un título en el cuadro de lista, el nombre del campo apropiado aparece en el cuadro de texto del formulario.

Usar This_Access para crear propiedades dinámicamente

Además de los métodos Access y Assign, Visual FoxPro 6 agregó un método AddProperty a las clases. AddProperty agrega una propiedad a un objeto en tiempo de ejecución en lugar de en tiempo de diseño. Por qué querrías hacer esto? Necesita esta capacidad cuando no puede predecir la interfaz de un objeto en el momento del diseño. Un objeto de parámetro es un ejemplo.

Un objeto de parámetro es simplemente un objeto que se pasa a un método como si fuera un paquete de parámetros.

Este mecanismo le permite cambiar una larga lista de parámetros ("¿era el título el octavo parámetro o el noveno?") En uno solo: un objeto con los "parámetros" almacenados en las propiedades del objeto. También permite múltiples valores de retorno de una llamada de método, ya que el método puede cambiar fácilmente los valores de muchas propiedades del objeto de parámetro y el llamador puede verificar los valores de esas propiedades.

Sin embargo, hay una complicación con el uso de objetos de parámetro: o necesita una clase diferente para el objeto de parámetro para cada interfaz (una que tenga las propiedades que esperan tanto el método de llamada como el llamado) o debe usar una propiedad de matriz del objeto de parámetro (que simplemente transfiere la complejidad de una larga lista de parámetros a múltiples columnas en un array). Sin embargo, ahora que tenemos AddProperty, podemos crear fácilmente un objeto de cualquier clase (incluso una clase base de Visual FoxPro), agregarle las propiedades que necesitamos, asignarle valores a esas propiedades y estará listo para usar.

This_Access puede mejorar aún más este mecanismo. ¿Por qué molestarse en crear propiedades manualmente? ¿Por qué no dejar que la clase los cree la primera vez que los necesite? Creé una clase llamada SFParameter (en SFCTRLS.VCX) que tiene el siguiente código en su método This_Access:

lparameters tcMember
if not pemstatus(This, tcMember, 5)
 This.AddProperty(tcMember)
endif not pemstatus(This, tcMember, 5)
return This

Este código agrega automáticamente una propiedad al objeto si no existe la primera vez que se accede a él. Por ejemplo, aunque esta clase no tiene una propiedad cCaption, el siguiente código funciona bien:

oParameter = newobject('SFParameter', 'SFCtrls.vcx')
oParameter.cCaption = 'My Caption'
? oParameter.cCaption && displays "My Caption"

Esto se debe a que la declaración que asigna un valor a la propiedad cCaption primero activa This_Access, que descubre que la propiedad no existe y la crea, después de lo cual tiene lugar la asignación. Todo esto en aparentemente (desde el punto de vista del usuario de la clase) una línea de código.

Solo hay una complicación: si la propiedad que se va a crear debe ser un array, This_Access no puede crearla porque no recibe ninguna información de que la propiedad sea un array (el único parámetro es el nombre de la propiedad). Entonces, SFParameter también tiene un método CreateArray para crear específicamente una propiedad de array. Aquí está el código:

lparameters tcArray, ;
 tnRows, ;
 tnColumns
local lnRows
lnRows = iif(vartype(tnRows) = 'N', tnRows, 1)
if vartype(tnColumns) = 'N'
 This.AddProperty(tcArray + '[lnRows, tnColumns]')
else
 This.AddProperty(tcArray + '[lnRows]')
endif vartype(tnColumns) = 'N'

Resumen

En limpio, la adición de métodos Accesss y Assign a Visual FoxPro 6 puede no parecer tan importante. Sin embargo, en este documento le mostré cómo estos métodos pueden proporcionar soluciones a muchos tipos diferentes de problemas. Como cualquier otra cosa, deben usarse correctamente, pero pueden ayudar a sus esfuerzos de desarrollo de muchas maneras.

Agradecimientos

Me gustaría agradecer a las siguientes personas que ayudaron directa o indirectamente con la información de este documento: Steven Black, Toni Feltman, Dan Freeman, Christof Lange, Ken Levy, Lisa Slater Nicholls y Jim Slater.

Copyright © Doug Hennig. Todos los derechos reservados


No hay comentarios. :

Publicar un comentario

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