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

Usando un TableAdapterManager

Una de las entradas de este blog que más visitas registra hace referencia a un error en el código generado de los TableAdapterManager, un componente de acceso a datos para usarlo junto con datasets tipados y sus respectivos tableadapters. Dicho error fue solucionado en el SP1 de Visual Studio y de las versiones Express, por lo que ya creía el tema olvidado. Sin embargo, parece que su uso despierta bastantes dudas, así que me vais a permitir que retome el tema con un pequeño ejemplo.

Un TableAdapterManager es un componente que se genera al crear un dataset tipado. Por lo tanto, si no usamos un dataset tipado ni necesitamos saber que tal cosa existe. Un TableAdapterManager automatiza el proceso de actualizar varias tablas relacionadas, manteniendo (o intentándolo) la integridad de los datos al hacer los update, insert y deletes en el orden apropiado. Hemos dicho que el TableAdapterManager se crea con un dataset tipado, por lo que sólo tendrá utilidad si este dataset tipado tiene varias tablas relacionadas.

Veamos el siguiente ejemplo, un dataset tipado, SeriesDataSet, con tres tablas: Series, Género y GeneroSerie. GeneroSerie es la tabla que permite la relación n:m entre géneros y series (una serie puede tener varios géneros, puede ser un drama romántico de ciencia ficción, por ejemplo).

Sigue leyendo

Mantener actualizados unos ComboBox (II)

No tenía intención de volver sobre este tema pero, la verdad, tampoco tengo nada más que escribir. Y, al final, fue un problema curioso cuya resolución no me quedó tan elegante como a mí me hubiera gustado. El primer problema que me dio la solución que comentaba fue cuando intenté ordenar el contenido de la BindingList. Mi gozo en un pozo: la BindingList no ordena. Me resulta chocante que la BindingList, que se supone va mejor para enlace a datos, no ordene ni busque, y una List sí. De hecho, por internet e incluso en la misma documentación de MSDN encontramos clases derivadas de BindingList que ordenan y/o buscan. De hecho, encontré una para un caso general que me ha gustado mucho y me la he guardado, pero para esta aplicación he preferido usar una clase preparada para la ocasión.

Partíamos, recuerdo, de una clase muy sencilla, IdMasDescripcion, con dos propiedades, Id de tipo Integer que se corresponde a la clave primaria, y Descripcion de tipo String. La clase, además, implementa la interfaz IComparable(Of T), y el método CompareTo lo que hace es comparar las descripciones. Lo que voy a hacer, entonces, es una BindingList(Of IdMasDescripcion) personalizada que sea capaz de ordenarse. Pero no siempre va a estar ordenada (depende de qué tabla muestre) y la ordenación puede ser alfabética o por resoluciones (de momento; en un futuro puede que me haga falta un tercer tipo). Voy a necesitar un método para ordenar (Sort), y dos campos que me indiquen si la colección está ordenada (IsSorted) y si hay que ordenarla (ToSort):

Sigue leyendo

Ordenando por resoluciones

Esto es una tontería del tipo tonterida, pero me ha llevado un ratito escribir el código, así que lo anoto aquí para cuando me vuelva a pasar, que entonces, seguramente, ya se me habrá olvidado y me tocará echar otro ratito y no me apetece. El problema es sencillo: hay que ordenar una lista de elementos de forma no trivial. En este caso concreto, hay que ordenar por una propiedad de tipo String, cadena de caracteres, que va a contener resoluciones de imagen, esto es, dos valores numéricos separados por una «x»: 640×480, 1024×768 y así. También serviría para cualquier lista de pares de valores y se podría extender a situaciones más complejas, como coordenadas espaciales y cosas así.

Lo primero ha sido elegir el algoritmo de ordenación. Como van a ser pocos elementos, me he tirado a los sencillitos y, de estos, al de inserción, que es el único que soy capaz de desarrollar sin mirar la chuleta (el de la burbuja lo tengo atravesado). El proceso es muy simple: vamos ordenando por el primer valor. Si este es igual, pasamos a ordenar por el segundo valor. En este caso concreto, ordenamos por largo (primer valor) y si son iguales, por ancho. Para ello, lo primero que he hecho ha sido crearme dos funciones que me devuelvan el largo o el ancho a partir de la cadena:

Sigue leyendo