Esta vez, he seleccionado uno de los artículos del blog de Cary Jensen que me ha parecido especialmente interesante para compartirlo con vosotros. En el artículo, Cary Jensen comenta con sus lectores, un posible bug ocasionado por el comportamiento de los campos persistentes del DataSet (luego se verá en el articulo, comentado y reflexionado por el, que no es tanto un error de código). De cualquier forma, sí me ha parecido interesante pues es algo que debería tenerse en cuenta.
Os explico: Accidentalmente, se da cuenta que al ser movidas las columnas de una rejilla de datos, una facultad que tiene per se de forma automática al ser creadas, el orden en los campos del dataset es alterado. ¿En que casos? Bueno, esto solo sucede cuando no existen los campos persistentes en el componente TDBGrid y puede generar errores si hemos referenciado llamadas a los campos persistentes del dataset mediante el array TField usando el indice de acceso. Cary, a tenor de lo visto reflexiona en la entrada sobre este punto y las posibles soluciones.
Yo tampoco creo que sea un bug. Aunque particularmente a mi sí me hubiera parecido acertado bloquear la funcionalidad de mover las columnas y solo permitirla en el caso de que hubieran sido creadas, de forma que no hubiera podido existir ese potencial error. Pero bueno… es mi opinión.
Ahh. Como siempre comento, perdonad los posibles errores en la traducción que he intentado que fuera lo menos literal posible.
Vamos a ello:
Shifting TFields in TDataSets Bound to TDBGrids: A Potential Source of Bugs in Your Code
Autor: Cary Jensen – Enero 26, 2010
http://caryjensen.blogspot.com/2010/01/shifting-tfields-in-tdatasets-bound-to.html
He estado trabajando con Delphi desde el principio, con particular énfasis en el desarrollo de bases de datos. Como resultado de ello, no es demasiado frecuente que yo encontrase un comportamiento de los componentes relacionados con los datos que me sorprendiera. Bien. Ha sucedido así en el último mes. Y lo que yo observé puede ser el origen de errores potencialmente desastrosos, aunque infrecuentes, en un gran numero de aplicaciones de bases de datos con Delphi.
Esto es lo que yo pude observar: Los TFields en un TDataSet abierto, cambiaron de orden en tiempo de ejecución. Especificamente, un TField que estaba originalmente en la posicion cero (DataSet.Fields[0]), en el momento en el que se habia creado el TDataSet, estaba en una posición diferente en el array de campos un pequeño tiempo después. Yo descubrí este comportamiento cuando una excepción fue lanzada como resultado de mi intento, mediante código, de leer el valor entero del campo de tipo TIntegerField que yo habia creado en la primera posición del array de campos de mi TDataSet. Desde el momento en el que yo había creado el TDataSet y la ejecución de mi código, el campo integer habia sido movido de posición.
Lo que habia sucedio no era mágico. Los campos no pudieron cambiar la posición por ellos mismos, ni lo hicieron en algo basado en mi código. Lo que causó que los TFields cambiaran físicamente su posición en el TDataSet fue que el usuario había cambiado el orden de las columnas en el TDBGrid, el cual estaba vinculados al TClientDataSet (a través del componente TDataSource, por supuesto). La habilidad del usuario para cambiar de posición las columnas en un TDBGrid, por cierto, es el comportamiento por defecto en el componente.
Además de ser interesante (He de suponer que una vez que se abrió conjunto de datos, la posición de la “TFields” en la matriz de Campos ya estaba fijada), este comportamiento es la fuente potencial de excepciones intermitentes, el tipo de las que son particularmente difíciles de localizar. Resulta que este comportamiento, que nunca he visto descrito antes, ha existido desde Delphi 1. (En realidad, he observado este efecto en Delphi 7, Delphi 2007 y Delphi 2010. Sin embargo, entiendo que la fuente subyacente de este comportamiento ha existido desde Delphi 1, aunque no lo he confirmado expresamente.)
He creado una aplicación muy sencilla para demostrar este efecto. Consiste en un único formulario en el que existe un TDBGrid, un TClientDataSet y un componente TButton. El ClientDataSet esta enlazado al TDBGrid a traves del TDataSource. En el evento OnCreate del formulario aparece algo como lo siguiente:
procedure TForm1.FormCreate(Sender: TObject); begin with ClientDataSet1.FieldDefs do begin Clear; Add('StartOfWeek', ftDate); Add('Label', ftString, 30); Add('Count', ftInteger); Add('Active', ftBoolean); end; ClientDataSet1.CreateDataSet; end;
Button1, está marcado con la etiqueta «Show ClientDataSetStructure» y contiene el siguiente código en el evento OnClick.
procedure TForm1.Button1Click(Sender: TObject);
var sl: TStringList; i: Integer; begin sl := TStringList.Create; try sl.Add('The Structure of ' + ClientDataSet1.Name); sl.Add('- - - - - - - - - - - - - - - - - '); for i := 0 to ClientDataSet1.FieldCount - 1 do sl.Add(ClientDataSet1.Fields[i].FieldName); ShowMessage(sl.Text); finally sl.Free; end; end;
Para demostrar el movimiento de los campos, ejecuta la aplicación, haciendo click sobre el botón marcado con la etiqueta Show ClienteDataSet Structure. Tu debería ver lago semejante a lo que muestra la figura 1.
Figure 1
Luego, arrasta algunas columnas del DBGrid y cambia el orden de los campos. Vuelve a hacer click en el botón «Show ClientDataSet Structure». En ese momento veras algo similar a lo mostrado en la Figura 2.
Figure 2
Lo remarcable de este ejemplo es que la posicion de los TFields en el propiedad Fields del TClientDataSet cambió, de forma que el campo que estaba en la posición ClientDataSet.Field[0] en un momento determinado no necesariamente está en un momento posterior. Y desafortunadamente, esto no es responsabilidad del TClientDataSet. He realizado la misma prueba con TTables basadas en el bde y TAdoTables basadas en Ado y se obtuvo el mismo efecto.
Contribuyen al resultado de este comportamiento tres factores. Esto son:
- Un TDBGrid conectado a un DataSet a través de un DataSource
- El TDBGrid permite al usuario mover las columnas en tiempo de ejcución
- Las columnas del TDBGrid son dinámicas; significa esto que son creadas por el TDBGrid en ejecución.
Si tú mediante código haces referencia a los campos del DataSet conectados al TDBGrid, y existen las tres condiciones precedentes usando un indice, tu aplicación puede lanzar una excepción, o producir resultados incorrectos, si el usuario mueve una o mas columnas en ese TDBGrid. En la siguiente sección, considerare algunas soluciones para resolver este problema, asi como compartir con Ustedes la razón de ello.
Existen varias soluciones
Existen varias tácticas que puedes usar para eliminar este potencial bug de vuestras aplicaciones. La primera es definir la TColumns de tu TDBGrid usando campos persistentes.
Crear columnas persistentes, puede ser hecho tanto en tiempo de diseño como de ejecución. Para hacerlo en tiempo de diseño, basta añadir las TColumns usando el editor de Columnas. Éste, se muestra haciendo click con el boton derecho del ratón sobre el TDBGrid y seleccionando el Editor de Columnas o bien haciendo click en la ellipsis de la propiedad Columns del TDBGrid en el Inspector de Objetos. Si tu DataSet está Activo, tú puedes hacer click en el botón «Add All Fields» en la toolbar del Editor de columnas. O bien, añadir uno o mas TColumns y fijar la propiedad FieldName en el Editot de propiedades.
Para crear columnas en tiempo de ejecución, puedes usar los métodos Add o Create de la propiedad Columns del TDBGrid. Puedes fijar los valores de propiedades especificas de las columnas añadidas o creadas.
La segunda solucion, aunque tiene algunas consecuencias negativas, previene que el usuario mueva las TColumns del TDBGrid. Esto puede ser hecho eliminando el flag dgResizeColumn de la popriedad Options del TDBGrid. Mientras este enfoque es efectivo, elimina opciones del interfaz potencialmente valiosas. Además, eliminando el flag no solo se restringe la opción de reordenar las columnas sino que impide redimensionar el ancho de las columnas. (Para aprender como limitar reordenar las columnas sin eliminar la opción de cambiar el tamaño de la columna ver el articulo de Zarko Gajic How to allow column resize by disable movement (in TDBGrid).
Una tercera solución es evitar referenciar a los TFields del TDataSet basandonos en un índice literal en la propiedad de tipo array Fields( ya que esta es la esencia del problema). En otras palabras, si tu necesitas acceder al campo Count, definido en el codigo de ejemplo que precede, no uses ClientDataSet1.Fields[2]. En la medida que tu conozcas el nombre del campo, puedes usar algo como ClientDataSet1.FieldByName(‘Count’).
Sin embargo existe una gran desventaja en el uso de FieldByName. En concreto, este metodo identifica el campo iterando a traves de la propiedad Fields del TDataSet, buscando una concidencia basad en el nombre del campo. Desde el momento en que hace esto cada vez que se invoca FieldByName, deberias evitarlo en situaciones donde el campo necesita ser referenciado muchas veces, como sucede en un bucle que recorre un TDataSet muy extenso, con muchos campos.
Si tu necesitas apuntar al campo repetidamente (y en un gran numero de veces) considera el uso de algo como el siguiente fragmento de código:
var CountField: TIntegerField; Sum: Integer; begin Sum := 0; CountField := TIntegerField(ClientDataSet1.FieldByName('Count')); ClientDataSet1.DisableControls; //assuming we're attached to a DBGrid try ClientDataSet1.First; while not ClientDataSet1.EOF do begin Sum := Sum + CountField.AsInteger; ClientDataSet1.Next; end; finally ClientDataSet1.EnableControls; end; end;
La cuarta solucion es el uso del método FieldByNumber de la propiedad Fields del TDataSet. Si ya tienes código escrito que usa un indice para el array Fields, y trabaja correctamente, siempre y cuando el usuario no mueva las columnas del TDBGrid, esta es otra solucion. Cambia tu codigo para usar FieldByNumber.
Hay dos aspectos interesantes para el uso de FieldByNumber. Primero tu debes cualificar su referencia con la propiedad Fields de tu DataSet. Segundo, al contrario del array Fields, que es basado en indice cero, FieldByNumber se inicia en 1 para indicar la posición del campo que tu quieres referenciar.
La siguiente es una versión actualizada del manejador del evento OnClick de Button1, mostrado anteriormente, que usa el método FieldByNumber.
procedure TForm1.Button1Click(Sender: TObject);
var sl: TStringList; i: Integer; begin sl := TStringList.Create; try sl.Add('The Structure of ' + ClientDataSet1.Name + ' using FieldByNumber'); sl.Add('- - - - - - - - - - - - - - - - - '); for i := 0 to ClientDataSet1.FieldCount - 1 do sl.Add(ClientDataSet1.Fields.FieldByNumber(i + 1).FieldName); ShowMessage(sl.Text); finally sl.Free; end; end;
Para el ejemplo propuesto, el código produce el siguiente resultado, sin considerar la posición de las columnas en la rejilla asociada. Esto se puede ver en la Figura 3
Figura 3
Hay una quinta solución, pero solo está disponible cuando tu TDataSet es de la clase TClientDataSet, como el que existe en mi ejemplo. En esas situaciones, tu puedes crear un clon del TClientDataSet original, y mantener la estructura original. En consecuencia, cualquiera que sea el campo que originalmente aparezca en la posicion zero, seguira estando en la misma posición, con independencia de que haya podido hacer el usuario al TDBGrid que muestra los datos del TClientDataSet.
Notar que no estoy sugiriendo que debas referenciar los campos TFields del TDataSet usando literales enteros. Personalmente, el uso de un variable de tipo TField, que se inicializa a través de una llamada a FieldByName es más legible e inmune a los cambios en el orden físico de la estructura de la tabla(¡aunque no sea inmune a los cambios en los nombres de tus campos!) .
Para terminar
Hay un par de puntos finales que quisiera hacer. Primero, la actual estructura subyacente no es afectada. Especificamente, si despues de cambiar el orden de las TColumns en un TDBGrid, llamas la método SaveToFile de la clase TClientDataSet enlazado a ese TDBGrid, la estructura guardada es la original (la verdadera estructura interna). De forma similar, si tu asignas la propiedad Data de un TClientDataSet a otro, el TClientDataSet destino tambien muestra la estructura verdadera (lo cual es similar al efecto observado cuando un origen de datos TClientDataSet es clonado).
De igual forma, cambios en el orden de las columnas de TDBGrids enlazados a otros probados TDataSets, incluyndo TTable y ADOTable, no afectna a la estructura interna de las tablas. Por ejemplo, un TTable que muestra datos desde la tabla de ejemplo de Paradox customer.db que viene con Delphi no cambia esta estructura de la tabla en disco (ni lo esperarias).
El segundo punto es que esto no es un bug en cualquiera de la clases TDataSet o TDBGrid (o TColumn o TField). Es asi como esas clases han sido diseñadas para trabajar. Y aunque este comportamiento introduce errores en tus aplicaciones, esto es porque nosotros no hemos cuidado ésto hasta el momento. Y, tu ahora conoces como se comportan tan bien como para prevenir que causen excepciones en tus aplicaciones con Delphi.
El punto final viene a nosotros por el usuario Sertac Akyuz desde StackOverflow, quien respondió a la pregunta acerca de este comportamiento que yo publiqué en ese sitio Web. Yo había revisado las fuentes tanto para la clase TDataSet como para la clase TDBGrid y no pude localizar el origen del mismo. Sertac escribió que este comportamiento es escontrado actualmente en las clases TColumns y TFields. Especificamente, cuando cambia la posicion de la columna de una TColumn dinámica (no persistente), fruto de una llamada al metodo que fija la propiedad Index de TField, lo cual afecta a la posición de ese campo en la propiedad Fields del TDataSet.
Ahora tu sabes que este potencial problema existe, bajo que condiciones puede emerger, también como sus efectos, deberias ahora echar un vistazo a tus aplicaciones para ver si tienes TDBGris con TColumns no persistentes que el usuario pueda mover en tiempo de ejecución. Si además, referencias las campos TFields con esas TColumns usando indices literales a la propiedad Fields del TDataSet, puedes eliminar errores potenciales resultado de apuntar a un campo erroneo en tiempo de ejecución usando una de las soluciones que yo he apuntando anteriormente en este artículo.
Copyright (C) 2010 Cary Jensen. All Rights Reserved
Deja una respuesta