11 de diciembre de 2012

Usando el Control Microsoft Date and Time Picker con valores de fecha y hora

Articulo original: "Using the Microsoft Date and Time Picker Control with Date and Time Values"
http://doughennig.blogspot.com/2008/06/using-microsoft-date-and-time-picker.html
Autor: Doug Hennig
Traducido por: Luis María Guayán

He usado el control ActiveX Microsoft Date and Time Picker (DTPicker). Ayer tropecé con un tema interesante. En primer lugar, algunos antecedentes.

El control tiene cuatro modos diferentes de ingreso de datos, a través de la propiedad Format: fecha corta, fecha larga, hora, y personalizado. Lo bueno de los tres primeros, es que utilizan automáticamente los ajustes de fecha y hora de la Configuración Regional en el Panel de control, por lo que no tiene que preocuparse por cuestiones de localización. La cuarta, se utiliza para los formatos personalizados.

El formato de hora está bien si sólo desea la hora, pero si quiere mostrar tanto la fecha y la hora, tiene que definir la propiedad Format a 3 para personalizarlo, luego, establezca la propiedad CustomFormat al formato deseado. Por ejemplo, "MM/dd/yyy HH:mm:ss" utiliza dos dígitos para la mayoría de los valores y los cuatro dígitos del año (sí, cuatro, aunque la cadena de formato utiliza tres). Sin embargo, aquí está la cuestión: ¿Cómo saber qué formato usar? El usuario puede estar utilizando MM/DD/AAAA, DD/MM/AAAA, o cualquier otra variedad de formatos.

Afortunadamente, VFP tiene varias funciones que retornan los formatos de fechas. SET("DATE") retorna valores como MDY o American para el formato MM/DD/AAAA, DMY o BRITISH para el formato DD/MM/AAAA, y así sucesivamente. SET("MARK") retorna el carácter que separa las partes de la fecha (como por ejemplo "/" o "-").

Tengo una subclase del control DTPicker llamada SFDatePicker (en realidad, es un contenedor que contiene un DTPicker) que ofrece funcionalidades adicionales, incluyendo el soporte a fechas vacías, datos obligatorios, como así también el control de formato. El control de formato se maneja a través de la propiedad personalizada lDateTime; el valor por omisión es .F. que significa que sólo la fecha aparece, mientras que .T. significa que se mostrará la fecha y la hora conjuntamente.

Suelte uno en un formulario, establezca la propiedad cControlSource a el control fuente que desea, configure lDateTime a .T. si desea la fecha y la hora, y listo.

El siguiente código en el método SetCustomFormat de SFDatePicker, que se llama en los métodos Init y Assiggn de lDateTime (en caso de que cambie la propiedad por programación), es necesario establecer CustomFormat:
with This  
  lcFormat = set('DATE')  
  if lcFormat <> 'SHORT' or .lDateTime  
    .oleDTPicker.Object.Format = 3  
    lcMark = set('MARK')  
    do case  
      case inlist(lcFormat, 'AMERICAN', 'MDY', 'USA', 'SHORT')  
        lcCustomFormat = 'MM' + lcMark + 'dd' + lcMark + 'yyy'  
      case inlist(lcFormat, 'BRITISH', 'DMY', 'FRENCH', ; 
        'GERMAN', 'ITALIAN')  
        lcCustomFormat = 'dd' + lcMark + 'MM' + lcMark + 'yyy'  
      case inlist(lcFormat, 'JAPAN', 'YMD', 'TAIWAN', 'ANSI')  
        lcCustomFormat = 'yyy' + lcMark + 'MM' + lcMark + 'dd'  
    endcase  
    if .lDateTime  
      lcCustomFormat = lcCustomFormat + ' HH:mm:ss'  
    endif .lDateTime  
    .oleDTPicker.CustomFormat = lcCustomFormat  
  endif lcFormat <> 'SHORT' ... 
endwith
Parece que este código se ocupa de los diferentes formatos de fecha adecuadamente, incluído el caracter de separación de la fecha, por lo que ¿cuál es el problema? Es muy sutil: Si utiliza SET SYSFORMATS ON, o sea que debe respetar la configuración regional del usuario, SET("DATE") retorna "SHORT". Este código asume que las fechas "cortas" son tratados como MDY.

Ahora el problema es cómo determinar cuál es el formato de fecha del usuario actual.Imaginé que una función de la API de Windows se ocuparía de esto, y encontré algunas posibilidades en MSDN, pero parece que estas funciones no pueden ser llamadas directamente desde VFP, ya que requieren funciones de llamada. Entonces, tomé el planteamiento de la fuerza bruta:
if lcFormat = 'SHORT' 
  ldDate = date(2008, 1, 3) 
  lcDate = dtoc(ldDate) 
  lnPos1 = at('8', lcDate) 
  lnPos2 = at('1', lcDate) 
  lnPos3 = at('3', lcDate) 
  do case 
    case lnPos1 < lnPos2 and lnPos2 < lnPos3 
      lcFormat = 'YMD' 
    case lnPos1 > lnPos2 and lnPos2 > lnPos3 
      lcFormat = 'DMY' 
    case lnPos1 > lnPos3 and lnPos3 > lnPos2 
      lcFormat = 'MDY' 
  endcase 
endif lcFormat = 'SHORT' 
Este código utiliza 01/03/2008 (en formato MDY) como una fecha, lo convierte en una cadena (que respeta la configuración regional de los usuarios), entonces de acuerdo al orden de los digitos para el mes, día y año, establecemos en consecuencia lcFormat.

Feo, sí, pero funciona, así que adelante con ella hasta que una solución más elegante esté disponible.

Actualización: Imaginé que había una forma mejor de hacer esto. Mientras miraba el código de la linda clase ctl32_datepicker de Carlos Alloatti, me encontré que el uso SET('DATE',1). Buscando en el archivo de ayuda de VFP, descubrí que esta era exactamente la función que yo necesito. No puedo creer que no conocia esto (o mas probable) que lo olvide. De esta manera ahrora el código es mas sencillo y limpio:
if lcFormat = 'SHORT' 
  lnDate = set('DATE', 1) 
  do case 
    case lnDate = 0 
      lcFormat = 'MDY' 
    case lnDate = 1 
      lcFormat = 'DMY' 
    otherwise 
      lcFormat = 'YMD' 
  endcase 
endif lcFormat = 'SHORT' 
Si eres curioso sobre cómo las otras características trabajan, aquí están los detalles.

Soporte a ControlSource: Como he mencionado anteriormente, cControlSource es una propiedad personalizada que contiene el nombre del ControlSource si así se desea. También hay una propiedad Value, por lo que el control puede estar ligado otros controles. Refresh actualiza Value desde el ControlSource:
if not empty(This.cControlSource) 
  This.Value = evaluate(This.cControlSource) 
endif not empty(This.cControlSource)
El evento Change del DTPicker, que se dispara cuando el usuario cambia la fecha y/u hora, plantea un evento personalizado DateChanged del contenedores. DateChanged actualiza el ControlSource, lo que podría ser un campo en un cursor u otra cosa, como una propiedad de un objeto:
with This 
  lcAlias = juststem(.cControlSource) 
  lcField = justext(.cControlSource) 
  do case 
      case empty(.cControlSource) 
      case used(lcAlias) 
        replace &lcField with .Value in (lcAlias) 
      otherwise 
        store .Value to (.cControlSource) 
  endcase 
endwith
Value_Access obtiene el valor desde DTPicker, cambiando a un Fecha, si una FechaHora no es necesario:
luValue = This.oleDTPicker.Object.Value 
if not This.lDateTime 
  luValue = ttod(luValue) 
endif not This.lDateTime 
return luValue
Soporte de Fecha en blanco: Si ha trabajado con el control DTPicker, sabe que no le gustan las fechas en blanco. Así, el código en Value_Assign utiliza la fecha y hora actual en ese caso. Además, ya que DTPicker espera que su propiedad Value tome un valor de fecha y hora, tenemos que manejar el paso a fecha:
lparameters tuValue 
local luValue 
with This 
  do case 
    case empty(tuValue) 
      luValue = datetime() 
    case vartype(tuValue) = 'D' 
      luValue = dtot(tuValue) 
    case vartype(tuValue) = 'T' 
      luValue = tuValue 
    otherwise 
      luValue = .NULL. 
  endcase 
  if not isnull(luValue) 
    try 
      .oleDTPicker.Object.Value = luValue 
      if not .CalledFromThisClass() 
        raiseevent(This, 'DateChanged') 
      endif not .CalledFromThisClass() 
    catch 
      endtry 
  endif not isnull(luValue) 
endwith

3 de diciembre de 2012

Programación Orientada a Objetos: Clonación


Para quienes estén acostumbrados a la Programación Orientada a Objetos les será común la expresión de que los objetos se manipulan por referencias a los mismos. La mejor manera de explicar este concepto es con un ejemplo sencillo.

Imaginemos el siguiente código:
obj1 = CREATEOBJECT("Custom")
obj2 = obj1
obj2.Comment = "Un valor cualquiera"
obj3 = obj2
? obj3.Comment
? obj1.Comment
¿Cuántos objetos de tipo (clase) Custom existen en memoria durante este fragmento de código?

¿Respondió 3: obj1, obj2 y obj3? Lo lamento. La respuesta es incorrecta. Existe solo un objeto Custom (el creado con CREATEOBJECT). Obj1, obj2 y obj3 son 3 referencias al mismo objeto Custom.

Por lo tanto, ¿cuál sería la salida por pantalla de este código? Se imprimirían 2 líneas con la cadena de caracteres "Un valor cualquiera". Si no me cree a esta altura, lo invito a que haga la prueba. Esto es así porque, como ya dijimos, el objeto es único. Se modificó el estado del objeto mediante la referencia obj2 (cuando se le asignó un valor a Comment). Luego, con cualquiera de las otras referencias, se verá el mismo valor, ya que apuntan al mismo objeto. El tema de las referencias es similar a utilizar apodos. La persona es única, pero puede responder a su nombre real o a alguno de sus apodos.

Por un lado, el manejo por referencia es útil: mejora el uso de memoria o facilita la modificación de un objeto compartido por varias funcionalidades. En otros casos, es un verdadero dolor de cabeza, porque se necesitaría, tal vez, copiar el objeto o duplicarlo. Algunos casos prácticos:
  • Imagine que cuenta con una clase Cliente y quiere modificar los datos de un cliente particular (objeto). Empieza a cambiar los valores y luego presiona sobre Cancelar. ¿Cómo vuelve atrás los cambios? Sería genial contar con una copia del original sin modificaciones.
  • Suponga ahora que quiere duplicar un objeto de tipo Factura, para no tener que cargar nuevamente todos los datos. ¿Cómo lo hacemos? Recuerde que Factura2 = Factura1 no genera un objeto nuevo y distinto.
Para casos como los anteriores es que se comenzaron a aplicar técnicas de clonación de objetos. ¿En qué se basan estas técnicas? Básicamente, en copiar, propiedad por propiedad, desde un objeto hacia otro. Pero cuidado, alguna de esas propiedades puede ser otro objeto, y volvemos con las referencias. Esto nos lleva a que hay dos tipos de clonaciones: una superficial, donde se genera un nuevo objeto y se le asignan los mismos valores que el objeto origen; y una profunda, donde se genera un nuevo objeto y, por cada propiedad, se analiza si se puede asignar (es un tipo primitivo) o si es necesario clonar también esa propiedad componente (es un tipo objeto dentro del objeto principal o un array).

Es difícil plantear una clonación genérica porque no se puede violar el principio de ocultamiento de la Programación Orientada a Objetos. Es decir, un clonador genérico solamente puede utilizar las propiedades públicas de un objeto. De ser necesario, cada clase, que tiene acceso a sus atributos ocultos, podría implementar un método para clonarse, de la forma:
DEFINE CLASS UnaClase AS CUSTOM
  HIDDEN propiedad1
  HIDDEN propiedad2
  HIDDEN propiedad3

  PROCEDURE Clonar() AS OBJECT
    obj2 = CREATEOBJECT("UnaClase")
    obj2.setPropiedad1(THIS.propiedad1)
    obj2.setPropiedad2(THIS.propiedad2)
    obj2.setPropiedad3(THIS.propiedad3)
    RETURN obj2
  ENDPROC

  * Faltarían los PROCEDURES para setPropiedad1, setPropiedad2 y setPropiedad3
  
ENDDEFINE
Engorroso, ¿no? Imagine un objeto con muchas propiedades ocultas. Sin embargo, a veces, suele ser la única opción.



Clase Clonador

Volviendo al caso de los objetos con propiedades públicas, hace un tiempo desarrollé una clase para que funcione como clonador genérico. Obviamente, funciona solamente para las propiedades públicas de los objetos, pero muchas veces suele ser suficiente con esto. La clase se basa en el uso de macrosustitución y las funciones AMEMBERS, PEMSTATUS y EVALUATE. A continuación se ve un PRG con la clase Clonador más una clase llamada UnaClase, para hacer una prueba.
LOCAL obj1, obj2
obj1 = CREATEOBJECT("UnaClase")
clonador = CREATEOBJECT("Clonador")
obj2 = clonador.clonar(obj1)
obj2.propiedad = .NULL.

? obj1.propiedad
? obj2.propiedad

DEFINE CLASS UnaClase AS CUSTOM
  propiedad = ""
  PROCEDURE INIT()
    THIS.propiedad = CREATEOBJECT("Collection")
    THIS.propiedad.ADD(CREATEOBJECT("ClaseAuxiliar", "Hola"))
    THIS.propiedad.ADD(CREATEOBJECT("ClaseAuxiliar", "Chau"))
  ENDPROC
ENDDEFINE

DEFINE CLASS ClaseAuxiliar AS CUSTOM
  propiedadAux = ""
  PROCEDURE INIT(valor)
    THIS.propiedadAux = valor
  ENDPROC
ENDDEFINE

DEFINE CLASS Clonador AS CUSTOM

  PROCEDURE clonar(OBJREF AS OBJECT) AS OBJECT
    IF VARTYPE(OBJREF) <> "O"
      RETURN OBJREF
    ENDIF

    LOCAL ARRAY laMiembros(1,3)
    LOCAL i AS INTEGER, objRef2 AS OBJECT, lcPropiedad AS STRING, lcClaseBase AS STRING
    objRef2 = CREATEOBJECT(OBJREF.CLASS)

    FOR i = 1 TO AMEMBERS(laMiembros, OBJREF, 1, "G")
      IF ALLTRIM(UPPER(laMiembros[i,2])) == "PROPERTY"
        lcPropiedad = laMiembros[i,1]
        lcClaseBase = "objRef." + lcPropiedad + ".BaseClass"
        DO CASE
          CASE TYPE("objRef." + lcPropiedad) == "O" AND ALLTRIM(UPPER(EVALUATE(lcClaseBase))) == "COLLECTION"
            objRef2.&lcPropiedad = THIS.clonarColeccion(EVALUATE("objRef." + lcPropiedad))
          CASE TYPE("objRef." + lcPropiedad) == "O"
            objRef2.&lcPropiedad = THIS.clonar(EVALUATE("objRef." + lcPropiedad))
          CASE TYPE("objRef." + lcPropiedad, 1) == "A"
            THIS.clonarArray(OBJREF, lcPropiedad, objRef2, lcPropiedad)
          OTHERWISE
            IF NOT PEMSTATUS(objRef2, lcPropiedad, 1)  && No es de solo lectura.
              objRef2.&lcPropiedad = EVALUATE("objRef." + lcPropiedad)
            ENDIF
        ENDCASE
      ENDIF
    ENDFOR
    RETURN objRef2
  ENDPROC

  HIDDEN PROCEDURE clonarArray(objOrigen, cOrigen, objDestino, cDestino)
    LOCAL ARRAY aOrigen(1), aDestino(1)
    ACOPY(objOrigen.&cOrigen, aOrigen)
    LOCAL i AS INTEGER, j AS INTEGER
    IF ALEN(aOrigen, 2) == 0
      DIMENSION objDestino.&cDestino(ALEN(aOrigen, 1))
      FOR i = 1 TO ALEN(aOrigen, 1)
        objDestino.&cDestino[i] = THIS.clonar(aOrigen[i])
      ENDFOR
    ELSE
      DIMENSION objDestino.&cDestino(ALEN(aOrigen, 1), ALEN(aOrigen, 2))

      FOR i = 1 TO ALEN(aOrigen, 1)
        FOR j = 1 TO ALEN(aOrigen, 2)
          objDestino.&cDestino[i, j] = THIS.clonar(aOrigen[i, j])
        ENDFOR
      ENDFOR
    ENDIF
  ENDPROC

  HIDDEN PROCEDURE clonarColeccion(objCol AS COLLECTION) AS COLLECTION
    LOCAL oClon AS COLLECTION
    oClon = CREATEOBJECT("Collection")

    LOCAL obj AS OBJECT
    FOR EACH obj IN objCol
      oClon.ADD(THIS.clonar(obj))
    ENDFOR
    RETURN oClon
  ENDPROC
ENDDEFINE
Note que de esta forma se puede modificar el atributo propiedad de obj2 sin perjudicar al objeto apuntado por la referencia obj1. En este caso, existen dos instancias de la clase UnaClase: la apuntada por obj1, definida explícitamente, y la apuntada por obj2, creada por reflexión de código dentro del método clonar, en la línea objRef2 = CREATEOBJECT(objRef.Class).

Note también que los Arrays y los objetos de tipo Collection merecen un tratamiento especial. El ejemplo está armado a propósito con objetos de tipo Collection.

Espero que les sea útil.

Pablo Lissa