Archivo de la categoría: Visual Basic 2010

Alucinado estoy

Me encuentro con que hay un error en el encabezado de ciertos mensajes de correo enviados automáticamente por el gestor de incidencias (SOS). Debía poner “SOSNext: aviso de cierre de la acción de mejora nº tal”, pero llega cortado en la “ó” de acción, que es sustituida por un carácter raro. Lo curioso es que el resto de correos con la palabra “acción” en el título llegan sin problemas.

Lo más curioso es que la contrapartida al cierre llega sin problemas: “SOSNext: aviso de reapertura de la acción de mejora nº tal”. Curioso, ¿por qué?, diréis. Bueno, pues por el código que genera el título del mensaje es similar en ambos casos:

Titulo = “SOSNext: Aviso de cierre de la “ & Singular & ” nº “ & Id

Y

Titulo = “SOSNext: Aviso de reapertura de la “ & Singular & ” nº “ & Id

¿Cuál es la diferencia?

La longitud de la cadena. No sé el porqué, pero el problema está en los seis caracteres que ocupa “cierre”. Cambiando la longitud de la palabra o, como he hecho, añadiendo un espacio más antes de “cierre”, problema solucionado.

Aunque la cara de pasmo tardará más en quitárseme.

Si no es por las buenas…

El otro día, intentando filtrar un DataView, me encontré con un problema pintoresco: quería filtrar los usuarios con determinados permisos en la aplicación, para lo cual necesitaba hacer una comparación bit a bit. Hacer esa comparación en SQL no supone problema (Permisos & PermisoPedido = PermisoPedido). En VB, tampoco (UsuarioActivo.Permisos AND PermisoPedido = PermisoPedido), pero la propiedad RowFilter del DataView es mucho más restrictiva. Usa sintaxis SQL, pero con muchas limitaciones.

En la Geekpedia encontré una solución al problema: pasar de las operaciones bit a bit y resolver el problema con operaciones decimales normales y corrientes:

(Convert((Permisos – Permisos % PermisoPedido) / PermisoPedido,’System.Int32′) % PermisoPedido = 1)

Donde PermisoPedido es el valor numérico buscado de nuestra enumeración (1, 2, 4, 8, 16…). El operador % es, en Visual Basic, el módulo (resto de la división entera).

Aghhhh, quiero morir

En algún momento, pareció buena idea. Desdoblar incidencias para cubrir también peticiones de servicio. Se trataba sólo de añadir un campo a tres tablas y luego gestionarlo en tres formularios.

Ya, claro.

Pero había cuatro vistas de esas tablas. Y luego procedimientos almacenados sobre esas vistas y esas tablas. Que eran llamados por los tableadapters de cinco datasets tipados, que los usaban para trabajar con esas vistas y esas tablas. Una docena de tableadapters, más o menos. Muchos métodos llamados (por fortuna) desde unos pocos sitios bien controlados.

Un horror de día.

Las datarow pasan a modificadas al navegar entre registros

He tenido durante meses un problema pintoresco: en una aplicación de Windows Forms con enlace de datos (datasets tipados, BindingSource y demás) se me cambiaba el estado de las datarow a modificadas sólo con navegar entre los registros. Dado cómo se actualizaban los datos a la base de datos, resultó ser un problema menor, pero molesto. El problema parecía estar en el enlace de datos (binding) de usercontrols. En su día no encontré nada en MSDN y lo dejé para otra ocasión. Ayer, buscando otra cosa, encontré la solución al problema.

Resumiendo: si buscamos ayuda sobre cómo hacer un enlace de datos simple en nuestro control personalizado, nos dicen que usemos <BindableAttribute(True)>:

<BindableAttribute(True)>
Public Property IdAccion As Integer

Pero resulta que, con sólo eso, el BindingSource supone que siempre cambian los valores enlazados y actualiza el origen de datos. Resulta que además debemos implementar la interface INotifyPropertyChanged y notificar cuándo cambia el valor, tal que de esta forma:

<BindableAttribute(True)>
Public Property IdAccion As Integer
    Get
        Return Me._IdAccion
    End Get
    Set(value As Integer)
        If _IdAccion <> value Then
            Me._IdAccion = value
            RaiseEvent PropertyChanged(Me, _
                    New PropertyChangedEventArgs("IdAccion"))
            Me._IdAccion = value
        End If
    End Set
End Property

Sigo, eso sí, sin haberlo visto tan bien explicado en la documentación. Menos mal que existen los foros y los blogs.

Pero, ajá, no es la panacea. Ahora me encuentro con que, a veces, el valor no cambia aunque debería. Por ejemplo, al añadir un nuevo registro que tiene (o tendrá) el mismo valor de la propiedad que el anterior. Hay que revisar estos casos con cuidado para, o bien asegurarnos de que cambia el valor de la propiedad o bien provocar a mano el evento cuando lo creamos necesario. En fin, problemas del enlace a datos.

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.