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.
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.
Lo que sufrí hace hace años para hacer algo parecido.
ResponderSuprimirOjalá hubiese tenido este código en aquel entonces.
Un gran trabajo, Al, explicado muy claramente y con mucho detalle.
Muchas gracias por compartirlo.
Saludos.
Muy bien, Al, muy bien. Aunque leí esta última entrega de la "trilogía" hace dos días, he esperado a tomarme un ratito para leer detenidamente la explicación y verificar, "con los fuentes VCL en mano" :-) que todo cuadraba.
ResponderSuprimirNo he derivado todavía ningún Dataset "ex professo" para ponerlo en práctica, realmente uno se plantea más funcionalidades antes de derivar una nueva clase, pero como parte de la suite MagiaData, esta funcionalidad es un valor añadido importante más a dichos componentes.
La solución que das es perfecta, como es habitual en tí, llegando al fondo de la cuestión y resolviéndola de forma elegante y sin chapucillas que hubieran hecho peligrar tu buena reputación :-). Vista la solución final al problema más espinoso de obtener como variant el contenido del Buffer, también se agradece que los creadores de Delphi dejaran abierta esa posibilidad que has sabido explotar, aunque no tienen excusas por no haber implementado los muy socorridos eventos BeforeChange y AfterChange.
Eres un maestro, también en las explicaciones.
Saludos,
Andrés
CASIMIRO dijo:
ResponderSuprimirLo que sufrí hace años para hacer algo parecido. Ojalá hubiese tenido este código en aquel entonces.
Hola Antonio. Infiero que al menos lo intentaste, y eso es mucho más de lo que hace la mayoría. No termino de sorprenderme del gran potencial que tiene la VCL y de lo poco que ese potencial es aprovechado en términos de POO por el grueso de los programadores Delphi.
CASIMIRO dijo:
Muchas gracias por compartirlo
Lo mismo te digo, lo mismo te digo. ;)
ANDRÉS dijo:
he esperado a tomarme un ratito para leer detenidamente la explicación y verificar, "con los fuentes VCL en mano"
Gracias por esa dedicación a leer el tema, Andrés. Y que bueno que eres de los valientes que, teniendo la carpeta VCL repleta de archivos .pas nativos, no le temen a abrir de vez en cuando alguno de ellos para echarles una mirada a conciencia. :)
ANDRÉS dijo:
No he derivado todavía ningún Dataset "ex professo" para ponerlo en práctica, realmente uno se plantea más funcionalidades antes de derivar una nueva clase, pero como parte de la suite Magia Data, esta funcionalidad es un valor añadido importante más a dichos componentes.
Sabes Andrés, hace cuatro años empecé la clase TMagiaClientDataSet con una sola propiedad en mente: "Provider". Como alternativa a ProviderName, para poder enlazar el componente en tiempo de diseño con un TDataSetProvider que no estuviera en el mismo módulo de datos, pero sí en la misma aplicación.
A la fecha la unidad de ese componente tiene 1691 líneas. Es una pena no haber contado con el suficiente apoyo cuando mi empresa estaba viva.
ANDRÉS dijo:
se agradece que los creadores de Delphi dejaran abierta esa posibilidad que has sabido explotar, aunque no tienen excusas por no haber implementado los muy socorridos eventos
Esperemos que no se les olviden cuando venga la gran reforma a los conjuntos de datos y se incluyan las clases row / record que mencioné en la primera parte. :)
Saludos cordiales.
Al González.
Qeu tal Al, el dominio sistemasgh.com esta en línea?
ResponderSuprimirSaludos
ANÓNIMO dijo:
ResponderSuprimirel dominio sistemasgh.com esta en línea?
¡Hola!
No, acabo de revisarlo y sigue suspendido, tal como se lo pedí al proveedor de alojamiento Web. Cabe mencionar que Sistemas GH detuvo sus operaciones hace ya varios meses. Por cierto, si a alguien le interesa revivir a una pequeña empresa mexicana que apostaba por Delphi (aclarando que tiene menos activos que pasivos), mi correo está en el panel derecho. También estoy a la escucha de propuestas de trabajo en el extranjero.
Respecto a mi artículo, ¿algún comentario, duda o sugerencia qué desees compartir?
Saludos cordiales. :)