1. Suponga usted que diseña un típico formulario VCL con un componente dentro de él, y le asigna a este componente un nombre mediante el inspector de objetos (propiedad Name).  Entonces, en tiempo de ejecución, al crearse una instancia de dicho formulario, ésta se generará con el componente tal cual lo dejó establecido en tiempo de diseño.  En Delphi todas las clases de componentes poseen, por herencia de TComponent, un constructor virtual llamado Create, el cual comúnmente aparece redefinido en dichas clases.  La re-generación de un formulario tal como quedó diseñado implica llamadas automáticas a estos constructores, a fin de re-crear cada uno de los componentes que se colocaron sobre el mismo.

    La pregunta es:

    En tiempo de ejecución, ¿cómo podría el constructor Create de un componente, como el que pusimos dentro de nuestro formulario, saber qué nombre le dio usted en tiempo de diseño?  Es decir, si dentro del código del constructor del componente necesitáramos averiguar qué valor se dio a la propiedad Name del objeto que está creándose, ¿cómo podríamos, desde el contexto de ese constructor —cuando las propiedades establecidas con el inspector de objetos aún no han sido asignadas—, obtener tal valor?  Considere en ello que el formulario puede contener una cantidad indefinida de componentes de la misma u otras clases.

    Para resolver la cuestión, es válido derivar clases y redefinir métodos virtuales, lo que sea menos modificar el código fuente de la VCL / RTL o inyectarle código máquina.  La solución no tiene que ser perfecta, pero sí robusta, eficaz y escrita en Delphi por el interesado.

    Si queremos hacerlo más atractivo, ofrezco 20 dólares estadounidenses de recompensa a la persona que lo resuelva de la mejor forma.  Lo sé, la cantidad es una miseria, pero exhorto a todos los amigos de la Comunidad para que ayuden a incrementar esta suma a fin de incentivar a los programadores con gusto por la investigación o apetito de conocimiento sobre nuestra herramienta.  Por otro lado, para no menguar el deseo de los interesados, pido completa honestidad y que nos abstengamos de concursar quienes sabíamos cómo resolver el problema antes de esta convocatoria.

    Las posibles soluciones deben presentarse aquí mismo, ya sea mediante el código fuente o poniendo un enlace que no deje lugar a dudas sobre su autoría.  Los lectores de esta bitácora, a manera de jurado y sin usar anonimato, podrán aprobar o rechazar con sus debidos argumentos las soluciones presentadas.

    Finalmente, decir que no es un reto demasiado difícil, pues el código fuente de ciertas clases nativas señala un camino a seguir.  Considere que el sólo intentar resolverlo puede darle conocimiento que le sea útil en el futuro.  La intención de esta propuesta es fomentar la investigación y desarrollo en valiosísimas áreas de Delphi que desafortunadamente han recibido poca atención, como es la Programación Orientada a Objetos o el entendimiento de los procesos internos de este lenguaje.  Al cabo de un tiempo razonable, si no es presentada una solución válida, publicaré la que tengo yo; pero me aferro a pensar que tal situación no se dará, por lo que Delphi representa en el mundo del desarrollo.

    ¿Acepta usted el reto?

    Al González.
    52

    Ver comentarios


  2. [Array constant versus "on the fly" array]

    Considere dos procedimientos (o métodos) como los siguientes:

    Procedure ProcInts (Const A :Array Of Integer);
    Begin
      // ...
    End;

    Procedure ProcStrs (Const A :Array Of String);
    Begin
      // ...
    End;

    De la forma en que están definidas, ambas rutinas declaran un parámetro de matriz abierta —en inglés, open array parameter—.  Es decir, al llamar a ProcInts y ProcStrs debemos especificar una serie de valores de tipo Integer y String, respectivamente; cualquier serie que tenga la forma de una matriz unidimensional.  Así que podemos usar para ello:

    1. Una variable Array estática.  Matriz de longitud predefinida y fija que, como toda variable, puede cambiar de contenido, pero su cantidad de elementos es siempre la misma:
    A :Array [0..4] Of Integer;

    1. Una variable Array dinámica.  Matriz donde ambos, contenido y longitud, pueden ser modificados durante su uso:
    A :Array Of Integer;

    1. Una constante Array.  Matriz en cuya declaración se especifica tanto la longitud como el contenido invariables:
    A :Array [0..4] Of Integer = (10, -20, 30, -40, 50);

    1. Un matriz construida "al vuelo".  Más formalmente conocida como constructor de matriz abierta (open array constructor), es una manera fácil de especificar una serie de elementos o valores justo en la llamada que se hace a una rutina:
    Rutina ([10, -20, Edad,  List1.Count, Trunc (Fecha)]);

    Para utilizar un constructor de matriz abierta o matriz al vuelo, la rutina a llamar debe tener declarado un parámetro de matriz abierta, y los elementos a pasar pueden ser cualquier secuencia de expresiones separadas por comas y encerradas entre corchetes, siempre que el tipo de dato de dichas expresiones sea compatible con el tipo de dato del parámetro declarado en la rutina.  En esa secuencia de expresiones pueden aparecer constantes, variables, propiedades de objetos, llamadas a funciones, etcétera.

    Lo anterior hace de las matrices construidas al vuelo un mecanismo muy socorrido, pues con ellas nos ahorramos tener que declarar una constante Array, o una variable Array y luego rellenar esa variable con una sentencia de asignación para cada posición.  Es bueno que existan, pero, aquí viene la pregunta: ¿conviene emplearlas aun cuando todas las expresiones del grupo sean valores constantes?  Es decir, si lo que vamos a pasarle a nuestro procedimiento ProcInts o  ProcStrs fuese una serie de valores constantes, ¿sería mejor utilizar un constructor de matriz abierta (al vuelo) o una constante matriz previamente declarada?  Pongamos algo de código para apreciar la diferencia:

    procedure TForm1.Button1Click(Sender: TObject);
    Const
      // Array constants
      AInts :Array [0..3] Of Integer = (1, 2, 3, 4);
      AStrs :Array [0..3] Of String = ('one', 'two', 'three', 'four');
    begin
      // Calls with array constants
      ProcInts (AInts);
      ProcStrs (AStrs);
    end;

    procedure TForm1.Button2Click(Sender: TObject);
    begin
      // Calls with open array constructors ("on the fly" arrays)
      ProcInts ([1, 2, 3, 4]);
      ProcStrs (['one', 'two', 'three', 'four']);
    end;

    Visto el código que tendríamos que escribir en un caso y en el otro, podríamos afirmar que una matriz construida al vuelo es mejor opción que declarar una constante matriz.  Pero antes de dar esto por sentado, debemos hacernos una segunda y muy importante pregunta: ¿cuál es el costo de procesamiento para la CPU?  Uno de los pilares que hacen grande a Delphi es su potente depurador integrado, una maravilla de ingeniería que algunos lenguajes más populares han intentado imitar.  Y una de las muchas utilidades que tiene esta herramienta, es poder observar y dar seguimiento al código máquina del programa ejecutable generado por el compilador.

    Considerando el código anterior, podemos situarnos en la línea de la primera llamada a ProcInts, en el interior del método TForm1.Button1Click, y presionar la tecla F5 para establecer ahí un punto de ruptura.  Entonces, al ejecutar el programa y presionar el botón Button1, el depurador hará que el programa quede detenido justo en esa sentencia de código.  En ese momento tendremos acceso a todas las ventanas del depurador integrado, muchas de las cuales aparecen nombradas en el menú View|Debug Windows.

    Si abrimos la ventana CPU (puede ser con el atajo Ctrl+Alt+C), nos aparecerá el código máquina de nuestro programa en lenguaje ensamblador, de tal manera que podremos enterarnos de cuántas y cuáles instrucciones de CPU generó el compilador para cada sentencia de código fuente:


    El lector notará lo reveladora que está resultando esta inspección.  Cada una de las cuatro sentencias de llamada (dos a ProcInts y dos a ProcStrs) se convierten en un grupo de instrucciones de CPU compuesto por una serie de Movs rematados por una instrucción Call.  Las instrucciones Mov del código máquina es lo que el programa necesita ejecutar para preparar el argumento matriz que ha de enviar al procedimiento (el parámetro formal A), mientras que la instrucción Call es la llamada en sí (el "salto") al procedimiento.

    Al usar una constante matriz, independientemente de su longitud o tipo de dato, el compilador genera sólo dos instrucciones Mov para su preparación previas a la instrucción Call.  Y esto es porque en algún lugar de la memoria esa matriz ya está debidamente estructurada: el compilador se encarga de insertar la serie de valores de la constante en el interior del programa ejecutable, todos juntos y en el mismo orden de su declaración.

    En contraste, cuando usamos una matriz construida al vuelo, esta no existe con anterioridad, por lo cual se agregan más instrucciones de CPU cuando el compilador traduce a código máquina la sentencia de llamada.  Dependiendo del tipo de dato, se genera una o dos instrucciones Mov adicionales por cada elemento de la matriz.  Estas instrucciones son las primeras en ejecutarse y consisten en asignaciones a cada una de las casillas.  Así entonces, cuando usamos una matriz construida al vuelo, la llamada a la rutina tiene un costo de procesamiento mayor, dado que la matriz debe ser creada (rellenada) cada vez que esa llamada ocurre.

    Lo descrito explica porqué las llamadas a ProcInts y ProcStrs hechas desde el método Button2Click se traducen a siete y once instrucciones de ensamblador, respectivamente, a diferencia de las escasas tres instrucciones que se generan para las llamadas a esos mismos procedimientos desde el método Button1Click.  Por otra parte, apenas se observa diferencia en el tamaño del archivo ejecutable cuando se compila usando uno u otro mecanismo.

    Conclusión: Si busca optimizar recursos, no use matrices construidas al vuelo cuando éstas se compongan de puras expresiones constantes.  En ese caso, mejor declare una constante matriz y luego coloque su nombre como parámetro de la rutina a llamar, aunque esto signifique escribir un poco más de código fuente.

    Hasta Delphi XE2 no ocurre nada especial al respecto, pero puede que alguna versión futura del compilador optimice los constructores de matriz abierta que expresan únicamente valores constantes (condición para hacerlos sustituibles por constantes matrices), de tal manera que el código máquina para la sentencia de llamada sea más eficiente.

    Por lo pronto hemos visto que a veces lo barato sale caro.  Consultemos de vez en cuando la ventana CPU para ver lo que realmente estamos creando.

    Al González.
    5

    Ver comentarios


  3. [Using .NET classes from Delphi code]

    Han existido varios intentos por hacer de Delphi una herramienta compatible con .NET.  En sí ya hay varios productos Object Pascal que cubren esa necesidad, pero éstos se enfrentan a algunos obstáculos:

    1. La riqueza y calidad del producto Delphi estándar como herramienta de programación que, aún con los últimos tropiezos, conserva mucho de lo que Borland dejó en herencia (sus exquisitos compilador y depurador sin ir más lejos).
    2. La enorme cantidad de proyectos hechos con Delphi y que sostienen las operaciones cotidianas de muchas empresas y oficinas de gobierno alrededor del mundo; proyectos a los que, de cuando en cuando, las nuevas condiciones del entorno les exige ampliaciones.
    3. La selectividad con que Microsoft decide qué tanto deja subir al barco a sus competidores (esto ya no será por mucho tiempo).
    Bien, para quienes no valga la pena mover todo un proyecto Delphi estándar a la plataforma .NET, pero que deseen emplear alguna de las muchas utilidades del marco .NET (.NET framework), existe una salida muy poco explorada pero sí documentada desde los años en que nació Delphi 7.

    Primero conviene leer de la ayuda de Delphi los temas titulados:

    • Requirements for COM interoperability [Requerimientos para interoperatividad COM]
    • .NET components and type libraries [Componentes .NET y bibliotecas de tipos]
    • Accessing user-defined .NET components [Acceder a componentes .NET definidos por el usuario]
    Éstos se encuentran bajo el apartado "Developing COM-based applications - Creating COM clients" de la propia ayuda electrónica que viene con Delphi (los encontré haciendo uso de la búsqueda avanzada).

    Hace aproximadamente un año, uno de mis clientes —empresa que hace la mayor parte de sus desarrollos en Delphi— me solicitó una solución al problema que tenían para firmar digitalmente cierto documento XML, específicamente el tipo de documento que se utiliza en México para reportar al fisco la cancelación de un grupo de facturas electrónicas.  No encontraban la manera de aplicarle la firma digital a tal documento, lo cual conlleva un proceso de cifrado (no "encriptación") de esos que se usan ahora y que sólo comprenden los del Área 51. ;)

    Investigué sobre varios caminos, pero me resultó muy interesante descubrir que el marco .NET de Microsoft va muy avanzado en todo lo que tiene que ver con criptografía (algún día Delphi vendrá con ese tipo de bibliotecas nativas), luego encontré los artículos de la ayuda de Delphi que mencioné antes, los cuales me hicieron ver que sí pueden utilizarse clases .NET desde código Delphi normal.  Y recordando que existen ediciones gratuitas de Visual Studio, ni tarde ni perezoso descargué este horrible entorno de desarrollo y programé una clase .NET en el desabrido lenguaje C#, exportándola como interfaz COM dentro de un ensamblaje .NET (una DLL compilada con dicho entorno).  Al ser una interfaz COM, no hubo problema para importarla en Delphi 7 y llamarla desde una aplicación de prueba.

    El código lo compartí en Club Delphi en febrero de este año, una vez terminado el breve periodo de gracia que el cliente y yo acordamos (para que los competidores no le perjudicaran su inversión) antes de ofrecer esta solución a la Comunidad en general:


    Espero que pueda ser de utilidad a otros programadores.  Como menciono ahí, aquello funcionó sin mayor problema, y esta reseña pretende servir como punto de partida para quienes pasen por la misma inquietud.

    Consejo: Cuando busquen en Google, usen la opción site:www.clubdelphi.com.  En lo personal me ha servido muchísimo (hasta para encontrar mis propios mensajes).

    Un abrazo entre plataformas.

    Al González.
    6

    Ver comentarios

  4. [Reading from other records without moving from the current one]

    Introducción

    Reformar la manera en que trabajan los conjuntos de datos en Delphi es asignatura pendiente desde hace varios años.  Estos útiles objetos administran en su interior algo llamado cursor, que es una estructura de control para recorrido y procesamiento de registros, y heredan gran parte de su funcionamiento de la clase base abstracta TDataSet.

    Además de sus diversas clases descendientes nativas, algunas de las cuales fueron apareciendo conforme Delphi se expandía (IBX, TClientDataSet, dbExpress, envolturas ADO,..), con el paso de los años llegaron muchas otras por iniciativa de la Comunidad para enriquecer el mosaico: ¿quién no ha descargado un paquete de componentes donde el nombre de alguno de ellos lleve la palabra Query, DataSet o Table?  Sin embargo, la clase base TDataSet sigue casi como hace diez años, haciendo mucho del trabajo sucio de una manera estupenda, pero al mismo tiempo imponiendo limitaciones de diseño.

    Cada vez estoy más convencido de que Delphi vendrá de fábrica, uno de estos años, con un marco de persistencia nativo y bien cimentado en la VCL y sospecho que en la RTL también.  Será algo que involucrará muchos cambios en todo lo que concierne a las clases para acceso a bases de datos, y que contribuirá de manera significativa a la reinserción de esta herramienta de programación en las pequeñas y grandes empresas de desarrollo.  Curiosamente, lo que más hará destacar a Delphi esta vez no serán los recursos para interfaces de usuario (ser de lo mejor en ese terreno ya no es noticia), sino las enfocadas al tratamiento de la información.

    Mientras eso ocurre, hay que encontrar soluciones al alcance de nuestras capacidades.

    Problema

    Suponga usted que se encuentra programando una aplicación de base de datos en Delphi 7, 2010, XE2 o cualquier otro, y ha visto la necesidad de realizar una acción en la capa cliente como respuesta a lo que haya ingresado el usuario en un campo.  Digamos que el usuario del programa introduce, mediante un cuadro de texto, cierto valor para un campo ftInteger llamado Numero, lo cual dispara un evento desde el que desea ejecutar algo en particular.  El control de captura se encuentra asociado a un conjunto de datos que ya tiene varios registros en memoria, registros que o bien acaban de ser capturados por ese mismo usuario o fueron traídos de la base de datos. 

    Usted desea programar una respuesta ante la asignación de valor al campo Numero, y para ello se vale del evento OnDataChange de un componente TDataSource que está enlazado al conjunto de datos y al cuadro de texto (también podría hacerlo con el evento OnChange del respectivo objeto TField).  Pensemos en que tal acción requiere forzosamente conocer el valor que los registros anteriores tienen en ese u otros campos, obviando las razones (comparaciones, cálculos, etcétera).  ¿Cómo obtener en ese momento los valores que hay en otros registros?

    El conjunto de datos se encuentra posicionado en la fila que está siendo capturada, siendo su estado (propiedad State) dsEdit si ese registro ya había sido almacenado antes, o dsInsert si va a ser almacenado por vez primera.  Si dicha fila es la tercera de la lista y en ese momento quiere leer el valor que hay en algún campo de la primera fila, tendría que comenzar por llamar al método First con la inoportuna consecuencia de que éste, como cualquier otro método de navegación (Next, Prior, Last, Locate,...), primero intentará guardar el registro actual que está siendo ingresado o modificado.  Resulta inconveniente porque tal registro quizá no se haya completado todavía o no se encuentre listo para superar las validaciones de guardado.  Y, suponiendo que el registro hubiera sido completado y cumpliera con las validaciones disparadas al ser guardado, ¿qué hay si el usuario decide cancelar su captura?  Evidentemente ya no habría estado dsEdit o dsInsert que cancelar, ya que por movernos de fila el estado del conjunto de datos pasaría a dsBrowse, así que una llamada al método Cancel no tendría ningún efecto.  Entonces la pregunta es: ¿Pueden leerse datos de otros registros sin movernos del actual?

    Solución

    En este punto, el lector experimentado estará pensando en las muy diferentes maneras en que suele esquivarse el problema.  Desde las más fatuas como meter en otras capas de la aplicación todas las reglas y validaciones, hasta las más drásticas como sustituir el uso de controles data-aware por controles simples y armazones de variables, pasando por remedios un poco más sensatos como apoyarse en los cursores de TClientDataSet o en los record sets de ADO.  Sin olvidar las bibliotecas de terceros que permiten un manejo rico de la información (DevExpress, RemObjects y otros), ni los raros y plausibles casos donde el programador inventa su propio data set. 

    Pero existe una alternativa inherente a TDataSet, y por tanto aplicable con cualquier clase de conjunto de datos (exceptuando unidireccionales), que ha sido poco aprovechada como medio para leer datos de cualquier fila sin necesidad de mover el indicador de "registro activo".  Es un mecanismo del que se ha ofrecido escasa documentación pública, no obstante revela sus más íntimos detalles a través del código fuente de la VCL.

    Antes de abordarlo, debemos considerar que leer datos de otros registros sin movernos del actual puede traer beneficios no solo a la hora de estar capturando información.  Un conjunto de datos puede encontrarse en estado dsBrowse y aun así resultar inadecuado movernos al registro que deseamos consultar (usando Locate, RecNo, First,...) para leer los campos necesarios, y luego volver al registro donde estábamos.  Aunque hagamos uso de los acostumbrados métodos DisableControls y EnableControls, algunos controles visuales pueden no quedar exactamente como los dejamos, apreciándose en pantalla extraños efectos que podrían confundir al usuario.  Una rejilla, por ejemplo, volverá a posicionarse en el mismo registro, pero quizá mostrándolo algunas líneas más arriba o más abajo.

    La solución que propongo a continuación no tiene como objeto reemplazar a ninguna otra, es solamente una sencilla alternativa que programé tras entender cómo trabajan TDataSet y sus clases descendientes con lo que se conoce como buffers de registros.  La clave está en una clase llamada TDataLink que antaño Borland implementó en la misma unidad que TDataSet (DB), siendo su función principal coordinar las operaciones que realizan los objetos data-aware con las operaciones hechas en los objetos TDataSource y los conjuntos de datos.  Grosso modo, la interacción de estos elementos es:

    Derivado de TDataSet <–> TDataSource <–> TDataLink o derivado <–> Objeto data-aware (TDBEdit, TDBGrid,...)

    Salvo que, para esta solución, no es necesario el último eslabón de la cadena. De momento no ahondaré en los detalles técnicos de TDataLink y los buffers de registros, ya que por ahora tiene más valor poner el código fuente de la solución junto con unos ejemplos fáciles de poner en práctica (además de que hoy tengo mucho que barrer y trapear).

    Clase TCursorInspect:

    Uses
      DB;

    Type
      TCursorInspect = Class (TDataLink)
        Public
          SavedActiveRecord :Integer;
          Constructor Create (Const ADataSet :TDataSet);
          Destructor Destroy; Override;
      End;

    // Implementación:

      Type
        TDataSetAccess = Class (TDataSet);
    Constructor TCursorInspect.Create (Const ADataSet :TDataSet);
    Var
      DataSet :TDataSetAccess Absolute ADataSet;
      I :Integer;
    Begin
      Inherited Create;

      { Asociamos este objeto con el conjunto de datos dado (creando y
        enlazando el TDataSource intermediario) }

      DataSource := TDataSource.Create (Nil);
      DataSource.DataSet := ADataSet;

      // Indicador para saber si ADataSet está en su última fila "visible"
      I := DataSet.BufferCount - DataSet.ActiveRecord;

      { Nos aseguramos de que el conjunto de datos tenga un buffer de registro
        por cada fila de su cursor interno (el entero devuelto por RecordCount
        tiene que ser verídico), más uno adicional para la fila que se haya
        empezado a capturar (si hay tal inserción) }

      BufferCount := DataSet.RecordCount + Byte (DataSet.State = dsInsert);

      // Guardamos el número de fila donde está el conjunto de datos
      SavedActiveRecord := ActiveRecord;

      { Recuperamos la fila que TDataSet suprime al insertar.  Sólo sucede
        cuando es llamado el método Insert, estando el conjunto de datos
        posicionado en la última fila de su lista de buffers (I = 1). }

      If (I = 1) And (DataSet.GetBookmarkFlag (
      DataSet.Buffers [ActiveRecord]) = bfInserted) Then
      Begin
        DataSet.InternalLast;
        DataSet.CursorPosChanged;

        For I := BufferCount - 1 DownTo ActiveRecord + 1 Do
          DataSet.GetRecord (DataSet.Buffers [I], gmPrior, False);
      End;
    End;

    Destructor TCursorInspect.Destroy;
    Begin
      If Active Then
        // Regresamos el conjunto de datos a la fila donde se encontraba
        ActiveRecord := SavedActiveRecord;

      DataSource.Free;
      Inherited Destroy;
    End;

    Ejemplo 1.- Leer toda la columna de un campo:

    procedure TForm1.Button1Click(Sender: TObject);
    Var
      I :Integer;
    begin
      { NOTA: dt1 es un conjunto de datos abierto de cualquier clase (excepto
        unidireccional) y mm1 es un cuadro de texto TMemo }


      // Con un objeto temporal TCursorInspect asociado a dt1:
      With TCursorInspect.Create (dt1) Do
        Try
          mm1.Clear;

          { Ponemos todos los valores del campo Numero, desde la primera hasta
            la última fila, dentro del control mm1.  Si dt1 tiene el estado
            dsEdit o dsInsert, éste se conserva al hacer el recorrido. }

          For I := 0 To BufferCount - 1 Do
          Begin
            ActiveRecord := I;  // Leeremos de la fila I
            mm1.Lines.Add ('Número: ' + dt1.FieldByName ('Numero').AsString);
          End;
        Finally
          Free;
        End;
    end;

    Ejemplo 2.- Usando el evento OnNewRecord, copiar en el nuevo registro un valor del registro inmediato anterior:

    procedure TDataModule1.dt1NewRecord(DataSet: TDataSet);
    Var
      CodigoPostal :Variant;
    begin
      With TCursorInspect.Create (DataSet) Do
        Try
          If ActiveRecord = 0 Then  // Si esta es la primera fila
            Exit;  // No hacer nada, salimos (con escala en el Finally)

          ActiveRecord := ActiveRecord - 1;  // Leeremos de la fila anterior
          CodigoPostal := DataSet ['CodigoPostal'];
        Finally
          Free;
        End;

      // Establecemos en la nueva fila el mismo código postal de la anterior
      DataSet ['CodigoPostal'] := CodigoPostal;
    end;

    Ejemplo 3.- Considerando que esto último puede ser algo común, ¿qué tal una función que simplifique la tarea?:

    // Función para leer el valor de un campo de la fila inmediata anterior:

    Function PriorRecValue (Const DataSet :TDataSet; Const Field :String)
      :Variant;
    Begin
      With TCursorInspect.Create (DataSet) Do
        Try
          If ActiveRecord > 0 Then
          Begin
            ActiveRecord := ActiveRecord - 1;
            Result := DataSet [Field];
          End
          Else
            Result := Null;
        Finally
          Free;
        End;
    End;

    // Uso:

    procedure TDataModule1.dt1NewRecord(DataSet: TDataSet);
    begin
      DataSet ['CodigoPostal'] := PriorRecValue (DataSet, 'CodigoPostal');
    end;
     
    Me parece responsable añadir que las dos principales desventajas que le veo a este mecanismo son la de no poder usarlo durante el evento TField.OnValidate (por la peculiar manera en que opera dicho evento) y la de que el consumo de memoria y CPU puede llegar a ser relevante cuando el conjunto de datos tiene miles de filas.  Por otro lado, es pertinente aconsejar que se trate a las instancias TCursorInspect como objetos temporales que se crean, se usan y se destruyen sin darle oportunidad a la interfaz de usuario de actualizarse (prescindiendo de llamadas a Application.ProcessMessages durante su existencia).  Para rematar las contras, hay que tener presente que la propiedad RecordCount del conjunto de datos debe estar debidamente actualizada (si usamos IBX será necesario haber alcanzado la última fila antes de usar esta clase).

    Sin embargo tiene sus ventajas también:

    1.    Su uso es cómodo y sencillo, como puede apreciarse en los ejemplos.
    2.    Se evita la creación de cursores adicionales, consultas paralelas u otros objetos que regularmente consumen más recursos.
    3.    Conserva intactos el conjunto de datos (incluyendo su propiedad State) y los demás objetos relacionados a éste, siendo en este sentido mejor que el mecanismo DisableControls...EnableControls.
    4.    Aunque el estado sea dsInsert, las nuevas filas tienen un lugar específico en la lista como cualquier otro registro (puede recorrerse el conjunto de datos incluyendo­ el registro que aún no ha sido guardado).
    5.    Con la propiedad heredada ActiveRecord o el campo SavedActiveRecord, es muy fácil determinar en qué lugar se encuentra la fila activa cuando el estado es dsInsert; en comparación con la propiedad RecNo del conjunto de datos, que suele devolver -1 durante ese estado.
    6.    Para acceder a los datos se utilizan los mismos objetos (conjunto de datos e instancias TField) que en lecturas normales.

    Esta clase, TCursorInspect, está hecha de manera simple y podría ser mejorada en muchos aspectos.  Pero tal como se encuentra cumple con su deber: facilitar la lectura de cualquier fila de un conjunto de datos, sin que el usuario o el propio conjunto adviertan movimiento alguno.

    No sobra mencionar que el código del constructor deja entrever un posible mecanismo más por el cual podríamos leer datos de diferentes filas silenciosamente: el método virtual GetRecord, del que valdría la pena escribir luego.

    Por lo pronto espero que esta solución apoyada en TDataLink resulte provechosa.  Ha sido probada con éxito en TClientDataSet, IBX y ADO.  No debe tener problemas con otros componentes.

    Al González.
    27

    Ver comentarios


  5. Esta noche, curioseando en las estadísticas de Rescatando a Delphi, descubrí que algún internauta de buen corazón entró aquí a través de mi firma en uno de los mensajes de Club Delphi.  Y a pesar de que el tema en cuestión es de hace escasos seis meses, ya había olvidado por completo esa intervención.

    Tras leer el tema nuevamente me pareció que podría valer la pena facilitar a otras personas la explicación que, de forma muy resumida, puse ahí sobre el funcionamiento de un temporizador:


    Además, esto invita a otros desarrolladores a enriquecer la discusión en el foro.  Una respuesta de pocas líneas (como fue la mía) no hace un apartado digno de encuadernarse, pero con la participación atrevida de otros más lo peor que puede pasar es que surjan experiencias inolvidables.

    Normalmente se facilitan hipervínculos de bitácoras Web dentro de los foros, pero ¿cuál es la razón por la cual no es costumbre poner enlaces a temas puntuales de foros dentro de las bitácoras Web?  Razonándolo, creo que mientras haya algo bueno qué compartir, no debe ser de gran consideración dónde se encuentre ni dónde se divulgue.

    O, ¿ustedes qué piensan?

    Al González.
    1

    Ver comentarios

  6. Un truco nada complicado pero en ocasiones útil:

    type
      TForm1 = class(TForm)
      private
        { Private declarations }
        Procedure WMMoving (Var Mensaje :TMessage); Message wm_Moving;
      public
        { Public declarations }
      end;

    var
      Form1: TForm1;

    implementation

    {$R *.dfm}

    Procedure TForm1.WMMoving (Var Mensaje :TMessage);
    Begin
      PRect (Mensaje.LParam)^ := BoundsRect;
      Inherited;
    End;
    17

    Ver comentarios

  7. [Anchoring a unit at the end of Uses]


    Contexto

    Una pregunta cuya respuesta es la razón de este artículo: ¿Importa el orden en el que aparecen los nombres de unidades en las cláusulas Uses de una aplicación?  Un programador Delphi con cierta experiencia sabe que la respuesta es sí, y que existen básicamente dos razones por las cuales ese orden puede tener relevancia: ámbito e inicializaciones / finalizaciones.

    El ámbito se refiere a cómo el compilador de Delphi  (al igual que muchos otros compiladores e intérpretes) localiza el origen de los identificadores que son referidos en el código fuente.  Para ilustrarlo, iniciemos en el IDE un nuevo proyecto Windows estándar.  Con esto será establecido en automático un primer formulario para el nuevo proyecto; guardemos todo (proyecto, formulario y su archivo de código fuente) con los nombres que Delphi propone.  El nuevo formulario aparecerá vacío y su código fuente, es decir, la unidad Unit1.pas, tendrá un aspecto como el siguiente:

    unit Unit1;

    interface

    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs;

    type
      TForm1 = class(TForm)
      private
        { Private declarations }
      public
        { Public declarations }
      end;

    var
      Form1: TForm1;

    implementation

    {$R *.dfm}

    end.

    Este código compilará sin problemas, pues se encuentra sintácticamente bien y todos los identificadores referidos pueden ser encontrados por el compilador (si no se ha hecho ningún cambio inadecuado en las opciones de configuración).  Durante la compilación, el analizador sintáctico de Delphi revisa este código fuente de arriba abajo y cuando encuentra la declaración de variable Form1: TForm1 verifica que ese tipo de dato, "TForm1", haya sido definido anteriormente.  En efecto, ahí podemos ver que la declaración del tipo clase TForm1 hubo de ser encontrada previamente varias líneas más arriba (TForm1 = class...).  Por tanto, la declaración de la variable Form1 es correcta, ya que su tipo de dato, la clase TForm1, fue declarado con anticipación y además en el mismo ámbito: la sección pública Interface de la unidad.

    ¿Pero entonces qué pasó milésimas de segundo antes, cuando el analizador encontró el inicio de dicha clase, es decir, la línea  TForm1 = class(TForm)?  Esta línea indica que se define una clase, de nombre "TForm1" e hija de la clase "TForm", pero ¿acaso TForm fue también definida antes en nuestra Unit1?  No, pero sí lo está dentro de una de las unidades nativas de Delphi señaladas por la cláusula Uses: la unidad Forms.  En la VCL, dentro de la sección pública Interface de la unidad Forms.pas, podemos encontrar su declaración:

      TForm = class(TCustomForm)
      ...

    Dado que Forms aparece nombrada en la cláusula Uses de Unit1, el análisis sintáctico del compilador no tiene problemas para resolver que la clase TForm definida en Forms.pas será el padre de TForm1 —TForm1 = class(TForm)—.  Lo que ocurrió fue que el analizador sintáctico primero revisó si el identificador referido (TForm), había sido declarado o no de manera previa en la misma sección de código de Unit1.  Como la clase TForm no está definida ahí, el analizador comenzó a buscarla en la parte pública de las unidades agrupadas bajo la cláusula Uses, pero comenzando por la última (Dialogs) y luego la penúltima (Forms), dentro de la cual fue felizmente encontrada.  Suponiendo que TForm no hubiera estado definida dentro de la unidad Forms, el analizador habría revisado en la sección pública Interface de cada una de las otras (Controls, Graphics, Classes, Variants, SysUtils, Messages y Windows) hasta aparecer un error Undeclared identifier: 'TForm'.

    He explicado de una manera un tanto burda cómo hace Delphi para localizar un identificador referido, pero es muy probable que dicha búsqueda se lleve a cabo sobre código pre-compilado y no entrando cada vez y físicamente a las unidades nombradas en el Uses.  El hecho es que, por las reglas de ámbito del compilador, un identificador referido es localizado "hacia arriba" y comenzando por el elemento "más cercano", de ahí que la búsqueda en las unidades del Uses empiece por la última de estas.

    Ahora bien, puede ocurrir que entre las unidades que tengamos nombradas en una cláusula Uses, dos de ellas definan en su interior algún elemento público con el mismo nombre (coincidencia de identificadores).  Esto suele ocurrir cuando utilizamos varias bibliotecas de terceros y alguna constante, función, clase, etc. de una de las bibliotecas lleva el mismo identificador que algún elemento público de las otras (aún hay programadores de bibliotecas que no aprecian el debido uso de prefijos).

    También hay casos donde la coincidencia de identificadores es deliberada, como sucede al emplear clases interpuestas, una socorrida técnica que Ian Marteens explica muy bien en uno de sus artículos: http://www.marteens.com/trick46.htm.  Ponga especial atención sobre "LOS LIMITES DE LA TÉCNICA", en el párrafo que dice:

    «[...] tenemos que incluir la nueva unidad en la cláusula uses del formulario, pero teniendo cuidado de ubicarla después de la unidad donde se encuentra la clase original [...]»

    En ambas situaciones de coincidencia de nombres (caso fortuito o intencional), conocer la regla de ámbito del compilador, de buscar "hacia arriba" y empezando por "lo más cercano", nos permite determinar en qué orden deben ser colocadas las unidades del Uses para que el compilador y la aplicación misma trabajen como se quiere.

    Con todo lo anterior queda explicado que, debido a la regla de ámbito mencionada, sí puede ser importante el orden en el que aparecen los nombres de unidades en las cláusulas Uses.  Mientras que el otro motivo que mencioné al principio —inicializaciones / finalizaciones — se refiere a cómo se ejecutan las secciones Initialization y Finalization contenidas en algunas unidades de código.  Pero en ello no habremos de profundizar ahora, sólo diremos que el orden en el cual se ejecutan dichas secciones depende de cómo el compilador va encontrando referencias a esas unidades en las diferentes cláusulas Uses.

    El problema

    Ya vimos que ante ciertas situaciones puede ser relevante el lugar que ocupe el nombre de una unidad dentro de una cláusula Uses.  El problema es que ni el lenguaje Object Pascal ni el IDE de Delphi poseen un mecanismo para indicar de forma explícita tal relevancia, y bajo algunas circunstancias el IDE, en su noble afán de facilitarnos las cosas, agrega por sí solo nombres de unidades al final del Uses.

    Para ver cuándo ocurre eso, volvamos a nuestro proyecto de ejemplo.  Supongamos que el formulario Form1, es decir, la clase TForm1, requiere de un archivo de código llamado Ultima.pas (unidad propia, nativa o de terceros) para las operaciones que se realizan en él, y que por alguna razón como las ya mencionadas es menester que el nombre de esa unidad aparezca siempre en el extremo final de la cláusula Uses.  Pues sencillo: simplemente escribimos ", Ultima" al final de dicha cláusula:


    Pero ¿qué sucedería si luego agregamos al formulario un componente, por ejemplo un TEdit y, acto seguido, guardamos nuevamente el formulario?  Lo que va a pasar es que Delphi añadirá a nuestra clase TForm1 un campo llamado "Edit1" (que representará al componente) indicando que su tipo de dato es TEdit.  Como la clase TEdit pertenece a la unidad nativa StdCtrls, esta debe aparecer en el Uses para que más tarde el compilador localice esa clase y no haya error alguno.  Por tal motivo el IDE nos ahorrará el trabajo de tener que escribir StdCtrls, colocando él mismo ese nombre de unidad justo al final de la cláusula Uses:

    Este mecanismo automático y tan útil de Delphi resulta contraproducente cuando queremos mantener una unidad específica al final de la cláusula Uses, independientemente de qué otras unidades estén o vayan a estar luego en la lista.  El IDE no emite aviso alguno cuando agrega unidades de esta manera.  Podemos no estar viendo el código al momento de guardar el formulario y su respectivo archivo .pas, o encontrarnos en alguna sección de este último muchas líneas abajo, de tal suerte que la cláusula Uses de la sección Interface será modificada por Delphi sin percatarnos de que nuestra unidad Ultima ya no es la última.

    Esto sugiere la necesidad de poder "anclar" el nombre de una unidad al final del Uses.  Y existen otras razones, como que los nombres de estas unidades especiales casi nunca son tan elocuentes (recuerde que "Ultima" es un nombre ficticio), así que, conforme transcurran las semanas y los meses de labor en un proyecto, puede que olvidemos el requerimiento de mantener tales unidades al final de las cláusulas Uses para garantizar el correcto funcionamiento de la aplicación.

    La solución

    Hace unos tres años tuve la necesidad de crear una serie de clases interpuestas, de muy diversa índole, en un proyecto que tenía varias decenas de formularios, y casi todos ellos necesitaban una o varias de esas clases interpuestas. Ubiqué el código de estas clases en un par de unidades y me di a la tarea de colocar el nombre de ese par de unidades al final del Uses de cada formulario.  Pero el camino empezó a hacerse nebuloso cortesía del complaciente IDE, debido al problema descrito en el apartado anterior.  Entonces, meditando sobre alguna posible solución, me surgió una idea: «¿Se podrá engañar al IDE usando la directiva $Include?».

    Desde los tiempos de Turbo Pascal existe un directiva de compilación que permite insertar (virtualmente) el contenido de un archivo de texto en casi cualquier punto del código fuente, de tal manera que el compilador toma ese texto como si fuera parte integral del código.  Pues inmerso en la emoción, descubrí que aquella directiva $I, que hoy puede usarse con un nombre más largo y apropiado, $Include, puede ayudarnos a fijar uno o varios nombres de unidades al final del Uses de la sección Interface, sin que el IDE los mueva de su sitio cuando añada por sí mismo otros nombres de unidades.

    El truco es crear un archivo de texto, cuyo nombre puede ser LastUnits.inc, y escribir en ese archivo el nombre de todas las unidades que deseemos anclar al extremo final de una cláusula Uses (separados por coma si son más de uno).  Siguiendo con el ejemplo de este artículo, el contenido del archivo LastUnits.inc sería simple y sencillamente el nombre de la unidad Ultima:


    Luego hay que colocar la directiva {$Include LastUnits.inc} al final del Uses de la sección Interface:

    Repitiendo la operación de hace un momento, pero con este nuevo esquema, observe lo que pasa en el código fuente cuando agregamos al formulario un componente TEdit y guardamos:

    Sorpresivo, ¿no? :)  A mí me causó esa misma grata impresión cuando lo vi por primera vez.  Si colocamos esta directiva $Include al final del Uses, el IDE la respeta y agrega la unidad StdCtrls delante y no detrás de la directiva.  Por lo tanto nuestra unidad Ultima, cuyo nombre es el contenido de LastUnits.inc, queda anclada al final de la cláusula Uses.  No importa que Delphi añada más nombres de unidades conforme llenemos el formulario de componentes, la directiva conservará su privilegiado último lugar.  Además, como se trata de un elemento muy distintivo, difícilmente olvidaremos con qué propósito la tenemos ahí, y menos con un nombre de archivo sumamente explícito como LastUnits.  Si por nuestra cuenta llegamos a escribir en esa cláusula Uses otros nombres de unidades, lo haremos conscientemente a la izquierda de la directiva, no al final como es costumbre.

    Por último decir que, como requisito de la directiva $Include, el archivo debe localizarse ya sea en el mismo directorio del proyecto, o en las "rutas de búsqueda" (en Delphi 7 se accede a ellas con la opción "Project|Options|Directories/Conditionals|Search path").  Así no habrá problema alguno a la hora de compilar.

    Siéntanse animados a compartir sus dudas, opiniones o sugerencias.

    Al González.
    13

    Ver comentarios

  8. Hace un par de semanas nos aproximamos a cómo implementar ese par de nuevos eventos en una clase derivada de TClientDataSet. Si el lector no tuvo oportunidad de visitar esta bitácora recientemente, quizá por estar al servicio de algún indómito capitalista, le recomiendo leer la parte 1 y la parte 2 de esta emocionante aventura.

    Este es el código que hemos escrito hasta ahora:

    Interface

    Uses
      DB, DBClient;

    Type
      { Tipos de datos procedimentales para los
        eventos AfterFieldChange y BeforeFieldChange }

      TFieldAfterChangeEvent = Procedure (
        Field :TField; PrevValue :Variant) Of Object;

      TFieldBeforeChangeEvent = Procedure (
        Field :TField; NewValue :Variant) Of Object;

      // Clase derivada de TClientDataSet
      TClientDataSetB = Class (TClientDataSet)
        Private
          FAfterFieldChange :TFieldAfterChangeEvent;
          FBeforeFieldChange :TFieldBeforeChangeEvent;
        Protected
          Procedure SetFieldData (Field :TField;
            Buffer :Pointer); Override;
        Published
          // Eventos nuevos
          Property AfterFieldChange
            :TFieldAfterChangeEvent
            Read FAfterFieldChange
            Write FAfterFieldChange;
          Property BeforeFieldChange
            :TFieldBeforeChangeEvent
            Read FBeforeFieldChange
            Write FBeforeFieldChange;
      End;

    Implementation

    Procedure TClientDataSetB.SetFieldData (
      Field :TField; Buffer :Pointer);
    Var
      Value :Variant;
    Begin
      Value := { Valor en Buffer };  // NewValue

      If Value = Field.Value Then  // No cambia
        Inherited SetFieldData (Field, Buffer)
      Else  // Sí cambia
      Begin
        If Assigned (BeforeFieldChange) Then
          BeforeFieldChange (Field, Value);

        Value := Field.Value;  // PrevValue
        Inherited SetFieldData (Field, Buffer);

        If Assigned (AfterFieldChange) Then
          AfterFieldChange (Field, Value);
      End;
    End;


    La más difícil tarea del método SetFieldData redefinido es obtener como Variant el valor apuntado por el parámetro Buffer. Como señalé en la entrada anterior, el formato de lo contenido en ese buffer depende de cada tipo de campo y los conjuntos de datos nativos no cuentan con un método específico para extraer dicho contenido en forma de Variant.

    El parámetro puntero Buffer puede traer un valor de Nil, lo cual significa que el campo será limpiado (puesto en blanco). Cuando no es Nil, apunta a un grupo de bytes cuyo formato binario y tamaño corresponden al tipo de campo en cuestión. Si el campo (parámetro Field) es de clase TIntegerField, Buffer apuntará a un bloque de cuatro bytes, un entero en la típica representación de 32 bits, siendo por tanto Buffer equivalente a un valor de tipo PInteger. Si el campo es de clase TStringField, Buffer apuntará al inicio de una cadena de caracteres de longitud variable, siendo Buffer equivalente a un valor de tipo PChar.

    El problema es que hay al menos una veintena de posibles estructuras apuntadas por Buffer, debido a la gran variedad de tipos de campos que existen en Delphi. Por otra parte, con algunos tipos de campos el formato nativo es menos transparente que el de los ejemplos anteriores. Por ejemplo, para los campos de clase TDateField, el buffer es un entero de 32 bits que indica una cantidad de días transcurridos desde el inicio de la Era Cristiana, incompatible con el estándar número flotante TDateTime (cuya cuenta inicia el 30 de diciembre de 1899). Allende de esto, el formato nativo de los campos puede variar de algunos conjuntos de datos a otros, lo cual dificultaría adaptar los eventos BeforeFieldChange y AfterFieldChange a clases derivadas de diferentes ramas.

    Si bien Delphi no dispone de una función a la que podamos darle como argumento uno de esos buffers nativos y, acaso indicándole el tipo de campo, nos regrese en forma de Variant el valor que contiene, ¿poseen los conjuntos de datos algún mecanismo que realice esa conversión de otra manera? La respuesta es definitiva: Así como al escribir el valor de un campo, éste llega a SetFieldData contenido en un buffer de formato nativo que seguidamente es copiado en el interior del registro, cuando leemos el valor de un campo, el método virtual GetFieldData del conjunto de datos realiza la operación inversa. GetFieldData copia los bytes de la misma sección del registro en un buffer que el objeto campo en cuestión le proporciona; entonces el objeto campo toma el valor de ese buffer según el formato que corresponde a su clase.

    Consideremos la siguiente sentencia con un objeto campo de clase TDateField:

    Fecha := DS.FieldByName ('Fecha').Value;

    En tiempo de ejecución, al evaluarse la expresión que está del lado derecho de la asignación, el objeto TField devuelto por FieldByName llamará al método GetFieldData del conjunto de datos para obtener una copia del grupo de bytes que guardan el valor nativo del campo en el registro actual, y la convertirá a un valor de tipo TDateTime. La referencia a la propiedad Value causará que ese dato sea a su vez convertido en un Variant, siendo éste el resultado de la expresión, ya que la propiedad Value de TField es de tipo Variant y su referencia en el ejemplo es polimórfica (FieldByName regresa un TField, no un TDateField).

    En efecto. Estoy sugiriendo que, para convertir el parámetro Buffer en un valor de tipo Variant, aprovechemos el habitual mecanismo de lectura que poseen los campos. Puede que lo anterior haga perder el sosiego: «¡Espera Al! Recuerda que esa conversión debe ser hecha en el interior del nuevo SetFieldData antes de aplicar la asignación, no después. Si leemos la propiedad Value del campo, ésta nos regresará el valor que tiene actualmente en el registro, no lo que viene dentro del parámetro Buffer. Pero bueno, te conozco y seguramente nos sorprenderás». xD

    Versión maquiavélica: «Seguramente hará una especie de asignación silenciosa en el registro para en seguida leer con la propiedad Value del campo, e inmediatamente restaurará el valor con otra asignación silenciosa, para entonces llamar al evento BeforeFieldChange, y, si no pasa nada, ejecutar la asignación normal. Que poco elegante, ¡demasiadas asignaciones! Borraré a Al de mi lista de contactos».

    Pero antes de que me pongan como no admitido, convendría hacernos una segunda pregunta: ¿puede decírsele a un campo que lea su valor de otro lugar que no sea la memoria del registro actual? La respuesta la tiene el casi místico evento OnValidate de TField. Quienes hayan usado este evento con regularidad, tendrán claro que durante su ejecución puede leerse el nuevo valor del campo como si ya hubiera sido plenamente asignado, pero si ahí mismo es elevada alguna excepción, el campo recupera el valor que tenía previamente. ¿Cómo sucede esto? Es sencillo: sin realizar todavía la asignación, el conjunto de datos le dice al campo que mire temporalmente en otro lugar, siendo ese otro lugar el mismísimo buffer dado a SetFieldData. Si el lector siente curiosidad por conocer los detalles de esa dinámica, puede apoyarse en esta breve explicación.

    Aquí haremos algo similar, agregando a nuestra clase TClientDataSetB tres elementos nuevos:

    • Un método función llamado NativeValue, que servirá para convertir un buffer de formato nativo a Variant.
    • Un campo de tipo PPointer llamado TempFieldData, para guardar de forma temporal una referencia al buffer que deseamos convertir.
    • La redefinición del método virtual GetFieldData para desviar la lectura del campo a TempFieldData, en lugar de leer del registro actual.
    La declaración de los dos primeros estará en la sección protegida de la clase:

        Protected
          TempFieldData :PPointer;
          Function NativeValue (Const Field :TField;
            Const Buffer :Pointer) :Variant;


    Y la declaración del tercero en la sección pública (su visibilidad en la clase padre es pública también):

        Public
          Function GetFieldData (Field :TField;
            Buffer :Pointer) :Boolean; Override;


    La implementación del método NativeValue es realmente sencilla:

    Function TClientDataSetB.NativeValue (
      Const Field :TField; Const Buffer :Pointer)
      :Variant;
    Begin
      TempFieldData := @Buffer;

      Try
        Result := Field.Value;
      Finally
        TempFieldData := Nil;
      End;
    End;


    Ahora, cuando necesitemos extraer el valor contenido en un buffer nativo, bastará que hagamos una llamada al método auxiliar NativeValue, dándole como parámetros el campo en cuestión y dicho buffer. Como puede verse en el código, NativeValue asigna de forma temporal la dirección del puntero Buffer al campo TempFieldData, esto con el fin de extraer su contenido mediante la propiedad Value del objeto campo Field. La clave para que eso sea posible se encuentra en la nueva implementación del método GetFieldData:

      Type
        TFieldAccess = Class (TField);
    Function TClientDataSetB.GetFieldData (
      Field :TField; Buffer :Pointer) :Boolean;
    Begin
      If TempFieldData = Nil Then
        Result := Inherited GetFieldData (Field,
          Buffer)
      Else
      Begin
        Result := TempFieldData^ <> Nil;

        If Result And (Buffer <> Nil) Then
          TFieldAccess (Field).CopyData (
            TempFieldData^, Buffer);
      End;
    End;


    Cuando TempFieldData tenga un valor de Nil, se realizará una lectura del valor almacenado en el registro, de la forma en que dicha lectura está implementada por la clase padre. Pero si TempFieldData es distinto de Nil, significa que apunta a un buffer nativo del cual hay que sacar el valor, y no del registro.

    GetFieldData debe devolver False cuando el campo leído es Null. Esto es importante para el correcto funcionamiento de varios métodos de TField y sus clases derivadas. La sentencia “Result := TempFieldData^ <> Nil” es congruente con ese requisito, considerando que el puntero Buffer proporcionado al método NativeValue podría ser Nil, en cuyo caso, viniendo del método SetFieldData, significa que el campo está por ser limpiado. Regresando False, hará que el campo —la expresión Field.Value dentro de NativeValue— devuelva un valor de Null, puesto que ese es el Variant que un buffer nativo Nil representa.

    En cambio, si el buffer apuntado por TempFieldData es diferente de Nil, GetFieldData devolverá un valor de True (indicación de que sí hay un valor para el campo). En este caso GetFieldData copiará el contenido del buffer dado a NativeValue a su propio parámetro Buffer, el cual es suministrado internamente por el objeto campo en cuestión siempre que se produce una lectura de su valor, en esta ocasión la lectura hecha por NativeValue. Para ello nos valemos de un método virtual llamado CopyData, el cual, dependiendo de la clase del campo, sabe cuántos bytes copiar de un buffer a otro y de qué manera. A fin de lograr que el compilador admita referirnos a ese método protegido de TField, empleamos el clásico truco del molde de acceso (clase auxiliar TFieldAccess). Y la condición “Buffer <> Nil” es por si se llegara a hacer un uso alternativo del campo TempFieldData, junto con métodos que no proporcionen un buffer para la extracción del valor, como es el caso de TField.IsNull.

    Con todo esto, la propiedad Value del objeto campo en cuestión hará que NativeValue devuelva el contenido del buffer que recibe como parámetro, pero en forma de Variant. Siendo su resultado un valor de Null si tal buffer es Nil. Así pues, queda todo listo para utilizar el método NativeValue en el interior de SetFieldData:

    Procedure TClientDataSetB.SetFieldData (
      Field :TField; Buffer :Pointer);
    Var
      Value :Variant;
    Begin
      If State In [dsEdit, dsInsert] Then
      Begin
        // NewValue
        Value := NativeValue (Field, Buffer);
        
        If Value <> Field.Value Then  // Sí cambia
        Begin
          If Assigned (BeforeFieldChange) Then
            BeforeFieldChange (Field, Value);

          Value := Field.Value;  // PrevValue
          Inherited SetFieldData (Field, Buffer);

          If Assigned (AfterFieldChange) Then
            AfterFieldChange (Field, Value);

          Exit;
        End;
      End;

      Inherited SetFieldData (Field, Buffer);
    End;


    Respecto a la aproximación que hicimos anteriormente, he agregado una condición para asegurar que el disparo de los eventos BeforeFieldChange y AfterFieldChange ocurra solamente si el estado del conjunto de datos es dsEdit o dsInsert (durante otros estados suele haber asignaciones especiales). Si el conjunto de datos está en modo de edición (primer If) y la asignación que está por realizarse representa un cambio real de valor (segundo If), se dispararán los eventos al realizar tal operación si éstos tienen manejador asignado. Si se quiere, desde el manejador de eventos asignado a BeforeFieldChange puede ser elevada una excepción y con ello se evitará que el valor del campo sea modificado. En caso de incumplirse alguna de las dos condiciones, el programa saltará a la última sentencia de la rutina, efectuándose una asignación nativa normal, sin el disparo de los nuevos eventos.

    El código es funcional, siéntanse con la libertad de registrar esta nueva clase en el IDE y probarla en alguna aplicación. Asimismo, con el código y la explicación dados, confío en que no les resultará difícil añadir estos dos nuevos eventos en los conjuntos de datos de su preferencia. En mi opinión se trata de una implementación básica que puede ser mejorada en varios aspectos, pero suficiente para disfrutar ya de los beneficios planteados al inicio de este artículo.

    Es importante señalar que los campos BLOB quedan imposibilitados para el uso de estos eventos, debido a que emplean mecanismos especiales de lectura y escritura que no hacen llamadas a los métodos virtuales GetFieldData y SetFieldData.

    Un abrazo antes y otro después.

    Al González.
    5

    Ver comentarios

  9. En el episodio anterior, planteamos la utilidad que tendrían en Delphi un par de eventos relacionados con el cambio genuino de valor en los campos de un conjunto de datos. Un evento que se dispare antes de que ese auténtico cambio ocurra, diciéndonos cuál es el nuevo valor que el campo está por aceptar; y un evento disparado tras ocurrir el cambio, informándonos sobre el valor que fue reemplazado.

    Por ahora Delphi y otros lenguajes orientados a objetos no admiten redefinición de clases. Así que en lugar de implementar estos nuevos eventos de forma centralizada para que toda la familia de los TDataSet cuente con ellos sin escribir código redundante o invasivo, los crearemos como parte de una sola clase derivada de TClientDataSet (la más acreditada de las nativas). Considero que el lector, si lo prefiere, podrá adaptar esta misma implementación a otros descendientes de TDataSet sin grandes dificultades.

    Empecemos pues.

    Algo que me entona de la programación es que se parece a escribir en prosa. Una vez que se tiene la idea básica de lo que se pretende expresar, puede uno depositar esa semilla en el escrito para que entonces el conocimiento solar y la hídrica imaginación la hagan germinar y crecer.

    En la sección Interface de una nueva unidad, coloquemos el siguiente código:

    Uses
      DB, DBClient;

    Type
      { Tipos de datos procedimentales para los
        eventos AfterFieldChange y BeforeFieldChange }

      TFieldAfterChangeEvent = Procedure (
        Field :TField; PrevValue :Variant) Of Object;

      TFieldBeforeChangeEvent = Procedure (
        Field :TField; NewValue :Variant) Of Object;

      // Clase derivada de TClientDataSet
      TClientDataSetB = Class (TClientDataSet)
        Private
          FAfterFieldChange :TFieldAfterChangeEvent;
          FBeforeFieldChange :TFieldBeforeChangeEvent;
        Published
          // Eventos nuevos
          Property AfterFieldChange
            :TFieldAfterChangeEvent
            Read FAfterFieldChange
            Write FAfterFieldChange;
          Property BeforeFieldChange
            :TFieldBeforeChangeEvent
            Read FBeforeFieldChange
            Write FBeforeFieldChange;
      End;


    Estoy utilizando no más de 50 columnas de caracteres debido a la estrechez del diseño de fábrica que tiene esta bitácora.

    Primero aparece la cláusula Uses anunciando a las unidades donde se encuentran las clases referidas TField y TClientDataSet. Luego se declaran dos tipos de datos procedimentales para las propiedades eventos AfterFieldChange y BeforeFieldChange. Y finalmente la nueva clase descendiente, que de manera simplista hemos llamado TClientDataSetB, declarando en ella los dos nuevos eventos y los campos privados en los cuales se almacenarán éstos. Para ambos eventos el parámetro Field señalará el objeto que representa al campo que cambia de valor. El parámetro NewValue del evento BeforeFieldChange (antes de que el campo cambie) indicará el valor que está por sustituir al actual, mientras que el parámetro PrevValue del evento AfterFieldChange (después de que el campo cambió) indicará el valor que fue sustituido.

    Si registramos esa clase como parte de un paquete e instalamos éste en el IDE, tendremos un nuevo tipo de componente con eventos AfterFieldChange y BeforeFieldChange perfectamente visibles y asignables con el inspector de objetos. Pero, claro está, dichos eventos no tendrían funcionamiento alguno todavía, debido a que nuestra nueva clase ya tiene el qué, pero todavía hay que escribir en ella el dónde y el cómo.

    Los eventos actúan como avisos de que algo está pasando, está por pasar o acaba de pasar. En esencia, queremos que cada vez que se asigne valor a un campo en el registro activo de un objeto TClientDataSetB, y dicho valor sea distinto del actual, se ejecute un aviso previo a la operación que nos informe que está por cambiar el valor de ese campo, y también un aviso posterior que nos diga que ese campo ha cambiado. Como es de esperarse, dichos avisos serán llamadas a los manejadores de eventos (event handlers) que estén asignados a las propiedades BeforeFieldChange y AfterFieldChange, respectivamente. Es responsabilidad del creador de nuevas propiedades eventos escribir el código necesario para que éstos sean disparados en el momento apropiado, así que debemos buscar la forma de que eso ocurra en nuestra nueva clase.

    Hablando de conjuntos de datos, un campo cambia de valor en el registro actual solamente por una operación de asignación, la cual puede ser evidente en sentencias como estas:

    DS.FieldByName ('Fecha').Value := Now;
    DS.FieldByName ('Fecha').AsString := ed1.Text;
    DS ['Fecha'] := qrAlumnosFechaIngreso.Value;
    DSFecha.Value := dmServidor.CurrentDate;
    DS.Fields [I].Assign (DSOrigen.Fields [I]);
    DS.FieldByName ('Fecha').Clear;  // Asignamos Null


    O también puede ocurrir mediante captura de datos en un control data-aware (como un cuadro de texto TDBEdit o una rejilla TDBGrid).

    Haciendo pruebas con el depurador y observando el código fuente de la unidad DB, podemos percatarnos de que, sea cual sea la vía de asignación de valor a un campo, siempre se ejecuta el método SetData del objeto TField en cuestión. Incluso el método TField.Clear, que nos permite dejar en blanco a un campo, llama a SetData. TField.SetData, a su vez, siempre llama al método SetFieldData del conjunto de datos al cual pertenece el campo.

    De la unidad DB.pas de Delphi 7 y otras versiones:

    procedure TField.SetData(Buffer: Pointer;
      NativeFormat: Boolean = True);
    begin
      if FDataSet = nil then
        DatabaseErrorFmt(SDataSetMissing,
          [DisplayName]);

      FValueBuffer := Buffer;
      try
        FDataSet.SetFieldData(Self, Buffer,
          NativeFormat);
      finally
        FValueBuffer := nil;
      end;
    end;

    procedure TField.Clear;
    begin
      if FieldKind in [fkData, fkInternalCalc] then
        SetData(nil);
    end;

    procedure TBooleanField.SetAsBoolean(
      Value: Boolean);
    var
      B: WordBool;
    begin
      if Value then Word(B) := 1 else Word(B) := 0;
      SetData(@B);
    end;


    Al final del artículo haré una mención especial sobre la familia de los campos BLOB, ya que éstos son la excepción a esa regla y desde ya descartamos el uso de los nuevos eventos con ese tipo de campos.

    El método TDataSet.SetFieldData viene en dos versiones (es sobrecargado) y ambas son virtuales. Una de ellas tiene tres parámetros y es a la que llama TField.SetData, como bien puede verse en el código fuente que transcribí arriba. La otra tiene solamente dos parámetros y es la única redefinida por TCustomClientDataSet (la clase precursora de TClientDataSet y descendiente inmediata de TDataSet):

    De la unidad DBClient.pas de Delphi 7 y otras versiones:

    TCustomClientDataSet = class(TDataSet)
    ...
    protected
      ...
      procedure SetFieldData(Field: TField;
        Buffer: Pointer); override;
      ...

    TClientDataSet = class(TCustomClientDataSet)
    published
    ...


    En la unidad DB, la versión de tres parámetros de TDataSet.SetFieldData siempre llama a la versión de dos parámetros, es decir, a la que TClientDataSet tiene redefinida. Por lo tanto, siempre que se asigne valor a un campo de un conjunto de datos cliente se ejecutará el método virtual redefinido TCustomClientDataSet.SetFieldData.

    TField.SetData -> TDataSet.SetFieldData(3p) -> TCustomClientDataSet.SetFieldData(2p)

    Observando el código fuente de este último, podemos apreciar que es ahí, precisamente, donde el conjunto de datos otorga el nuevo valor al campo mediante ciertas operaciones de buffers. Y como un método virtual nunca deja de serlo, ¿qué les parecería redefinirlo una vez más en nuestra clase TClientDataSetB para disparar desde su interior a los dos nuevos eventos? ;)

      TClientDataSetB = Class (TClientDataSet)
        Private
          FAfterFieldChange :TFieldAfterChangeEvent;
          FBeforeFieldChange :TFieldBeforeChangeEvent;
        Protected
          Procedure SetFieldData (Field :TField;
            Buffer :Pointer); Override;
        Published
          Property AfterFieldChange
            :TFieldAfterChangeEvent
            Read FAfterFieldChange
            Write FAfterFieldChange;
          Property BeforeFieldChange
            :TFieldBeforeChangeEvent
            Read FBeforeFieldChange
            Write FBeforeFieldChange;
      End;


    Esta redefinición del método SetFieldData tiene toda la pinta de ser el dónde, faltando solamente el cómo, es decir, su implementación. La siguiente es una aproximación:

    Procedure TClientDataSetB.SetFieldData (
      Field :TField; Buffer :Pointer);
    Var
      Value :Variant;
    Begin
      Value := { Valor en Buffer };  // NewValue

      If Value = Field.Value Then  // No cambia
        Inherited SetFieldData (Field, Buffer)
      Else  // Sí cambia
      Begin
        If Assigned (BeforeFieldChange) Then
          BeforeFieldChange (Field, Value);

        Value := Field.Value;  // PrevValue
        Inherited SetFieldData (Field, Buffer);

        If Assigned (AfterFieldChange) Then
          AfterFieldChange (Field, Value);
      End;
    End;


    En ella hay varias cuestiones qué atender, empezando por cómo extraer en forma de Variant el nuevo valor que viene dentro del parámetro Buffer, pues como Variant lo necesitamos para compararlo contra el valor actual del campo, y también para usarlo como parámetro NewValue del evento BeforeFieldChange. Ese parámetro Pointer señala un buffer que contiene el valor a asignar pero en formato nativo, es decir, la representación binaria del valor cuyos bytes terminan siendo copiados en el registro. Proviene tal cual de alguno de los métodos de asignación del campo, o bien de una conversión especial hecha por el método TDataSet.SetFieldData. Para casi todos los tipos de campos su estructura interna es por demás distinta de la estructura interna de un Variant, y depende completamente del tipo de campo en cuestión (TIntegerField, TStringField, TBooleanField,...).

    Aunque el lector no lo crea, Delphi carece de una función o método especializado en convertir estos buffers nativos en valores de tipo Variant. Considerando que existe una amplia variedad de tipos de campos, podríamos entonces decir que estamos ante un buffer incómodo que nos hará escribir un enorme y divertido Case.

    Pues no es así. Porque encontré una forma elegante de convertir ese buffer a Variant...
    6

    Ver comentarios

Cargando