Archivo de la categoría: Programacion

Datasets tipados, amor y odio

Llevo varios meses con un proyecto que empezó siendo algo pequeño (sustituir una aplicación Access de gestión de incidencias por un desarrollo en .NET, por temas de falta de licencias de Access para todos los usuarios) y ha ido creciendo hasta el punto que mataría por tener una semana para documentar con calma todo lo que llevo hecho. Este crecimiento, aunque lo tenía en mente (no tanto, claro) cuando diseñé la aplicación, me hace dudar de ciertas decisiones que tomé en su día, como el uso de datasets tipados.

Supongo que la idea de usar datasets tipados con VB2010 teniendo Entity Framework puede parecer rara o anacrónica, pero llevo usando datasets desde 2005 y tirar de ellos me ahorraba un tiempo de aprendizaje que no tenía. Ahora, con 17 datasets tipados (y creciendo) no sé hasta qué punto fue buena idea.

Entendámonos, me gustan porque el asistente es cómodo y puedo personalizarlos rápida y fácilmente. Se han convertido en poderosas (y pesadas, me temo) entidades que se mueven entre la aplicación tanto con los datos como con las órdenes de qué hacer con ellos.

El problema viene cuando tengo que reconfigurar alguno, porque se ha añadido algún campo a la base de datos o, como está pasando estos días, ha habido que hacer cirugía mayor y donde antes tiraban de una tabla ahora lo hacen de una vista. Esa forma de machacarse la configuración de los campos existentes, esos problemas con el AllowDBNull y con los NullValue…

A veces odio los datasets tipados.

El problema de la barra de progreso

El año pasado, intentando desembarazarme de una tarea tediosa que dejaba frito mi equipo, usé mi primera función recursiva con utilidad práctica. Como la tarea consumía su tiempo, tiré de un BackGroundWorker. Componente sencillo de usar y con ejemplos bastante claros en la ayuda de Visual Studio. No problem: tenía un valor de entrada pedido por el usuario, se realizaban n operaciones de forma recursiva, se obtenía un valor de salida.

Como el proceso era algo largo (mínimo unos diez segundos, pero podía llegar superar el minuto en la versión 2, que admitía de entrada una serie de valores), se me ocurrió usar una barra de progreso para mantener informado al usuario.

Entonces empezaron los problemas.

Todos los ejemplos que he visto que usan el evento ProgressChanged del BackGroundWorker para alimentar una barra de progreso presentan una serie de operaciones en secuencia con llamadas a ProgressChanged entre ellas, es decir: hago paso 1; progreso cambiado; hago paso 2; progreso cambiado… Sin embargo, mi problema era otro: tengo una instancia de mi clase de trabajo, cojo los valores del usuario (a través de DoWorkEventArgs.Argument), se los paso, y le digo “ea, majo. Tú puedes” llamando al método MiClase.HacerCurro. Ahí dentro se harán las n operaciones (recursivas), número desconocido hasta que se hagan.

Vale, pensé. Puedo provocar un evento OperaciónCompletada cada vez que complete una operación. Pero la instancia de MiClase está declarada dentro del DoWork del BackGroundWorker. No tengo acceso desde fuera. ¿Cómo lo hago?

¿Y si meto alguien que escuche en el DoWork? De esta forma tendría mi “escuchador” de eventos en el mismo hilo. Es decir, una clase, digamos Escuchante, a la que le paso mi instancia de MiClase y la del BackGroundWorker. Escuchante gestiona el evento OperacionCompletada provocando a su vez el ProgressChanged, que actualiza la barra de progreso.

No sé si será una solución muy limpia, pero funcionó.

En resumen, algo así:

Public Class Escuchante

Private WithEvents _MiClase as MiClase

Private _MiBGW as BackGroundWorker

Public Sub New(mc as MiClase, bw as BackGroundWorker)

_MiClase=mc

_MiBGW=bw

End Sub

Private Sub OpCompletada(sender as Object, e as OperacionCompletadaEventArgs) _

        Handles _MiClase.OperacionCompletada

_MiBGW.ReportProgress=e.NumOp

End Sub

End Class

Quiero mi Date Null, gracias

Quiero mi bo cadillo decía ese gran filósofo moderno que es Homer Simpson. Algo parecido he estado yo pidiendo estos días. El problema se me ha presentado en una aplicación con acceso a datos, con un montaje convencional: dataset tipado + bindingsource + controles en el formulario. Uno de los campos era una fecha y podía tener valores nulos. Hasta ahí bien. El problema es que en ediciones podía recuperar el valor nulo. En esos casos, fallaba la validación de datos y no se guardaba nada, aunque tampoco conseguía saber por qué: tuve que echar mano del evento BindingComplete del BindingSource para enterarme que se producía un error en la conversión de tipos. El tipo Date no acepta nulos.

Tras una larga búsqueda en internet, casi todo en inglés, averigüé dos cosas: que era un problema común y muy molesto y que no tenía solución fácil. También hallé unas pistas que me indicaron el camino. Un trabajo no muy limpio, pero que funciona, dividido en dos partes: el enlace al control y la datatable correspondiente.

En enlace, usando el diseñador (DataBindings, Avanzado) para asignar un formato de fecha corta, queda así:

Sigue leyendo

El valor no puede ser nulo. Nombre del parámetro: objectType

El error en cuestión me salía en el diseñador de Visual Basic Express 2010 de forma insistente. Una molestia a la hora de trabajar con el diseñador, más que nada, porque luego la aplicación funcionaba sin problemas. Ayer me dediqué a buscarle solución, tirando de Google.

El problema, apuntaban en varios sitios, se daba en formularios o controles de usuario heredados y estaba causado al gestionar eventos de objetos de alguna de las clases padre con la cláusula Handles en la clase hija. Tras investigar con calma mi usercontrol, encontré que el causante del desaguisado era, efectivamente, una cláusula handles que apuntaba a un evento de un BindingSource del abuelo del usercontrol. Quité el Handles y lo sustituí por un AddHandler en el constructor y todo funciona con suavidad.

Tocamiento de narices de un TextBox

Ando estos días programando una serie de plantillas y controles personalizados. He estado preparando un TextBox personalizado con posibilidad de validación y filtrado de datos de serie o mediante validadores/filtradores a medida que implementen cierta interfaz. Hasta ahí sin problema. Como el tipo de TextBox en cuestión va a ir montado, como regla general, sobre un formulario o un control de usuario que incorporan un sistema de gestión del estado (VerDatos, Modificar, Nuevo, etc.) se me ha ocurrido que el TextBox ajuste su estado ReadOnly automáticamente. Vale, sin problema. Ha funcionado a la primera. Salvo por un pequeño detalle visual.

Al indicar ReadOnly=True el color de fondo del TextBox cambia.

Pensé en controlar el evento ReadOnlyChanged, pero no conozco el color de fondo anterior al cambio y, francamente, saberlo complicaría más las cosas (reescribir la propiedad BackColor y el método OnReadOnlyChanged). Reescribir el método OnPaint, que de todas formas ni se me pasaba por la cabeza, no serviría porque, leído aquí, el cambio de color provocado por el cambio de ReadOnly se hace “por su cuenta” (Actually, .Net TextBox is a simple wrapper of Win32 Edit control, and it passes the painting issue to the Win32 Edit control). En el mensaje en cuestión se propone sobreescribir la propiedad ReadOnly del TextBox, algo que parece sencillo, simple y razonablemente elegante.

Salvo por otro pequeño problema: ReadOnly es una palabra reservada. Todo intento de crear una propiedad que se llame ReadOnly es frustrada por el propio IDE. ¿Cómo se hace para llamar a una propiedad como una palabra reservada?

Este pequeño problema me ha llevado bastante rato solucionarlo, así que lo comparto. Es una tontería y tiene su lógica, pero me ha hecho perder un tiempo precioso.

Usar corchetes:

Public Overloads Property [ReadOnly] as Boolean

En fin, sigamos…

Pegar datos de una hoja de Excel a un DataGridView

Un intento de simplificar el uso de una aplicación por lo demás bastante simple (de cara al usuario) me ha llevado a jugar con el portapapeles para permitir que el usuario copie una serie de valores de una hoja de cálculo (Excel, para más señas) y las pegue en un DataGridView, algo que me ha costado un buen rato, no porque sea complicado, sino porque no he encontrado información al respecto por Internet (seguramente por la torpeza “buscativa” de uno).

El problema, como digo, no es complicado. Por una parte, acceder al portapapeles es muy sencillo, mediante My.Computer.Clipboard y hay varios métodos para extraer su contenido según lo que contenga. En este caso (la copia de texto) usaremos el método GetText, que nos devuelve un String. El dónde gestionar esto ya queda a gusto del consumidor pero, para mí, lo más fácil es añadir un menú contextual con el habitual atajo de teclado (Ctrl + V).

El siguiente paso es tratar ese texto. Cualquier colección de celdas de Excel es copiada al portapapeles como un conjunto de valores separados por tabulación. El otro día mostraba el uso de la clase TextFieldParser para procesar ficheros de texto que nos viene como anillo al dedo. Bueno, casi, porque al TextFieldParser no le podemos pasar un String, está pensado para procesar ficheros. Si vemos sus constructores, a uno se le pasa la ruta de un fichero, a otro un Stream, precisamente para tratar con un fichero, y a la tercera sobrecarga, un TextReader.

¿Qué es un TextReader (era la primera vez que lo veía)? Según la biblioteca de MSDN, TextReader es la clase abstracta de StreamReader (éste si lo conocía) y StringReader. Éste último tiene en su nombre la palabra mágica, String, así que merece la pena echar un ojo. Me da igual los tutoriales que nos puedan aparecer ni los mil usos que tenga StringReader. Para el caso que nos ocupa, todo se reduce a crear un StringReader a partir del texto contenido en el portapapeles:

Dim miTexto As StringReader

miTexto = New StringReader(My.Computer.Clipboard.GetText)

Y pasarle ese StringReader a un TextFieldParser, con lo que estamos casi, casi, en el mismo caso del ejemplo de TextFieldParser. Ya podemos trabajar de forma simple con el texto del portapapeles. Sólo hay que añadir los controles de errores necesarios y decidir qué vamos a hacer con cada fila válida devuelta por el TextFieldParser. Siguiendo la lógica del título de esta entrada, pasárselas al DataGridView, pero tengo que confesar que no lo hice así. La razón es muy simple: me interesaba más pasárselo directamente al origen de datos del DataGridView.

Para mi caso concreto (Origen de datos + BindingSource + DataGridView), antes de iniciar el proceso de lectura de TextFieldParser, llamé al CancelEdit del BindingSource para evitar la posibilidad de que se generase una fila en blanco o no válida (si se da el caso, se saltaría la validación de datos) y le quité también la referencia al DataSource:

Me.bsArticulos.CancelEdit()

Me.bsArticulos.DataSource = “”

Después de añadir los valores del portapapeles al origen de datos, se vuelve a enlazar el BindingSource con éste y listo. Bueno, vale, no he pegado realmente en el DataGridView. Espero que nadie se sienta estafado.

Nos vemos en el Forlon.

Leer ficheros csv y de campos de ancho fijo en Visual Basic

En mi vuelta al servicio activo he tenido que procesar dos ficheros en texto plano: uno con campos separados por un carácter y otro con campos de ancho fijo. En su día ya lo hice (concretamente para un csv, valores separados por comas), pero no me acordaba de cómo lo hice (fue al poco de empezar con VB2005) y no tenía a mano ni el código que hice ni el libro que usé de referencia, así que busqué en Internet.

Y fíjese usted por donde, encontré una forma más simple de hacer las cosas que no conocía: usando la clase TextFieldParser.

En la forma más fácil, le pasamos en el constructor la ruta del archivo a tratar. Le indicamos luego qué es, si de ancho fijo (FixedWith) o separado por carácter (Delimited) con la propiedad TextFieldType. Si es el segundo caso, con el método SetDelimiters indicamos el delimitador (por ejemplo, MiTfp.SetDelimiters(vbTab) para indicar campos separados por una tabulación).

En el primero, indicaríamos el ancho de los campos mediante una lista de enteros que le pasamos al método SetFieldWiths: MiTfp.SetFieldWiths(2, 3, 12, 85, 5).

Para leer cada línea tenemos el método ReadFields, que nos devuelve un array de cadenas con cada campo de la línea del fichero en la posición correspondiente del array. Se tarda más en explicar que en hacer.

Hay un pequeño problema adicional: la codificación de caracteres. Por defecto, se usa UTF8. Ahora mismo no recuerdo si Vista y 7 usan UTF8 por defecto, pero XP y versiones anteriores de Windows, no, y podemos encontrarnos con problemas con algunos caracteres, como ñ, tildes, etc. En ese caso, seguramente estemos usando Windows occidental (Windows 1252) o, más raramente, ISO-8859-1. Podemos indicarle esto al TextFieldParser en el constructor, pasándole el código de la página de códigos a emplear:

Dim MiTfp as New TextFieldParser(MiRuta, System.Text.Encoding.GetEncoding(1252))

Lo más largo, como siempre, es el control de errores, o el propio tratamiento de los valores que hayamos leído, pero el hecho de leer en sí más simple no puede ser.

DataRepeater y el evento Enter de sus controles

A la hora de evitar que el usuario manazas edite el valor de algún control, una opción habitual es controlar el evento Enter de ese control o de su contenedor (un GroupBox, un Panel…), mandando el foco a otro control mediante el método OtroControl.Select(). Esto funciona para buena parte de los controles disponibles en Windows Forms, pero no para todos (dtpicker, optionbutton… hay varios que pasarán de nosotros).

Si nosotros queremos usar este sistema con controles que estén dentro de un DataRepeater… En fin, probadlo si queréis, pero ya os digo que la aplicación se quedará muerta si mandáis el foco a un control que esté fuera del DataRepeater. ¿Entonces?

Pues no salgamos. Pasemos el foco al ítem del DataRepeater al que pertenezca el control y listo:

MiDataRepeater.CurrentItem.Select()

El control DataRepeater es más versátil que el habitual DataGridView, pero es caprichoso como él solo.

Evitar error en consultas de agregado de LINQ

Nombre pomposo de la entrada pero que contesta a un problema que se me ha presentado esta mañana (volver a trabajar tiene estas cosas): al calcular el valor máximo de un campo en un conjunto de datos con una consulta LINQ sobre un dataset (por ejemplo, el importe más alto de un determinado cliente) se producía una excepción si no se devuelve ningún valor (en el ejemplo, si ese cliente no tiene pedidos). Una búsqueda por Internet me ha apuntado como solución usar extensiones de métodos, algo que no domino y se me antojaba excesivamente complicado para la tarea.

Tras pensarlo un poco, he probado a convertir el valor del campo pasado a la instrucción MAX a un tipo Nullable, que acepta valores nulos. En mi caso, como era valores enteros, esa parte de la consulta LINQ quedaría tal que así:

Into Max(Ctype(Fila.Campo, Integer?))

La consulta ya no da error, y sólo nos queda controlar si hay valor devuelto (propiedad HasValue), convertirlo al tipo que queramos (Integer, en el ejemplo) y seguir adelante. Más sencillo se me hace que otras ideas que he visto por ahí, y más rápido.

Como siempre, si a alguien le resulta de utilidad, pues mejor.

Jugando con colecciones Dictionary

Esta entrada podría valer como la tercera de la serie “Mantener actualizados unos ComboBox”, pero va en modo corto, que es nochebuena y me dan ganas de hacer algo más acorde con la fecha (ver La tumba de las luciérnagas, por ejemplo). Hagamos un breve resumen: tenemos un formulario maestro-detalle de series y las releases o distintas versiones de esas series. Como el número de releases por serie es pequeño (0 a 3 ó 4) y el número de campos de la tabla releases es amplio, en lugar de usar una rejilla o algún control parecido, decidí emplear un TabControl que mostrara tantas pestañas como releases y, en cada una, un UserControl con todos los datos. Es decir, esto:

Sigue leyendo