28 diciembre 2006

Controlando errores en DataGridView y cancelando líneas nuevas

Hoy vamos a por varios problemillas relacionados con el DataGridView enlazado a una colección de objetos de negocio. Los resumo muy brevemente:
  1. Ya corregí la eliminación de líneas. Originalmente sólo hacía un remove, por lo que la entidad seguía existiendo en la base de datos. Ahora hay una nueva propiedad en la colección que elimina las entidades al quitarlas de la colección en memoria, con lo que ya funciona la eliminación de líneas desde el DataGridView (dgv) sin necesidad de código específico.
  2. Cuando se mete uno en la línea nueva, nada más hacer clic, el dgv ya la trata como una línea existente y crea una nueva debajo. Sucede incluso antes de que el usuario intente escribir algo. Para empezar, este comportamiento no me gusta. Preferiría que se añadiera cuando los datos sean válidos. Pero claro, se necesita una entidad por debajo, y esa entidad debe estar en la colección... así que es posible que no se pueda cambiar el comportamiento. Aún así tenemos 2 alternativas: a) si intenta abandonarse la línea sin estar correcta, ofrecer eliminarla o quedarse en ella para corregirla; b) si se pulsa ESC, debe eliminarse la línea (esto según leo funciona en los dgv asociados a un DataSet automáticamente. ¿Por qué a nosotros no?)
  3. La gestión de errores en un dgv es diferente: las filas (o las celdas) tienen un ErrorText, y parece que si se rellena, aparece un icono de error en el selector de la fila. Vamos a ver cómo puedo facilitar su uso desde nuestras colecciones de negocio.
Tras varias horas (unas 6) he encontrado a qué se debe el problema de las líneas nuevas, y he consiguido que al pinchar en ella o moverme a ella, no se cree inmediatamente otra debajo. Era culpa del evento PropertyChanged disparado por la colección origen de datos del grid (en el caso de mis pruebas, culpa del evento disparado por Provincia de que había cambiado su colección de Poblaciones). El evento provocaba un refresco del enlace (del BindingSource), que se producía durante el AddNew, confirmándolo y fastidiándolo. Para evitarlo y solucionar de un plumazo todos los problemas del punto 2, creamos un método EBase.OnPropertyCollectionChanged que no dispara el PropertyChanged, sólo pone el objeto a sucio. En caso de que tengamos que propagar la suciedad más allá (por ejemplo, Población no sólo a Provincia, sino de ahí a País) tenemos que crear un método específico (ver NotificarCambioEnPoblaciones) para evitar disparar los eventos que refrescarían los controles dgv enlazados a colecciones de negocio.
Antes que esto había conseguido mostrar los errores, provisionalmente lo hago a nivel de línea y con código específico para cada dgv, aunque podría generalizarse. Sólo hay que rellenar la propiedad ErrorText de la Row, o bien de la Cell si queremos mostrar en cada celda su círculo rojo de error. También queda pendiente controlar esto conjuntamente con PieErrores, de forma que éste se ponga en rojo cuando haya fallos en algún dgv.

27 diciembre 2006

Databinding no actualiza valores corregidos

Hemos implementado una funcionalidad en la propiedad Nif de EPersona (en Nibi.Agenda) que comprueba si el Nif es correcto, corrigiéndolo si es posible (añadiendo ceros a la izquierda, añadiendo la letra o cambiándola si fuera errónea). Pero esto da un problema al usar Databinding: tenemos la propiedad enlazada con un control de texto, que propaga los cambios del control al negocio; pero cuando en negocio recibo el Nif, y posiblemente lo corrijo, y disparo el PropertyChanged de esa propiedad, en el control no se actualiza el Nif corregido, quedando el valor escrito por el usuario. Más curioso aún, dicho valor sí es actualizado cuando modificamos otra propiedad.
Todo esto es comentado en detalle en el artículo de Rocky Lockford. El motivo es que el Databinding en .net 2.0 usando INotifyPropertyChanged refresca todos los controles salvo el control actual. Más curioso. Porque en .net 1.x no era así. Mucho más curioso. La causa puede ser (ojo, entramos en zona de elucubraciones) que si el PropertyChanged del negocio vuelve a cambiar el control, este vuelve a disparar un evento de cambio que vuelve a actualizar el negocio, y así indefinidamente en un bucle infinito.
Ahora vamos con las soluciones. Rocky se columpia un taco, usando un Timer para disparar el PropertyChanged después de haber salido del set, cosa que le funciona pero que me da suzto sólo de pensarlo. Hay una solución mucho más sencilla aunque tiene un gran inconveniente: que es responsabilidad del interfaz, no del negocio, por lo que debe conocer cuando una propiedad cambia los valores recibidos (por ahora voy a hacerlo con el Nif). Basta con capturar el evento BindingComplete en la dirección de actualización del origen de datos (Negocio) y forzar un ReadValue del Binding en cuestión (el del Nif). Habrá que trabajar así mientras no den una solución desde Microsoft.

Etiquetas:

19 diciembre 2006

Transas con TransactionScope

Sobre las transas, hay un poco de miedo ante la utilización del nuevo TransactionScope de .net 2.0. Se basan en System.Transaction, un nuevo espacio de nombres que abstrae y centraliza el manejo de transacciones, alejándose de la concreción de las DbTransaction. Esto, que ya suena a pérdida de rendimiento, se ve agravado por el uso de MS DTC (coordinador de transacciones distribuidas), un servicio de Windows para la realización de transacciones que afectan a diferentes orígenes transaccionales (por ejemplo, transas que involucran distintas bases de datos). Muchos advierten de que el uso de MS DTC es una grave pérdida de rendimiento en la mayoría de los casos, ya que su potencia no suele ser necesaria.
El artículo con que decoro esta entrada, junto con este otro que se basa en el anterior, me aclaró mucho las cosas. Deja claro que sobre SQL Server 2000 es cierta esta afirmación, todo uso de TransactionScope delega en una transacción distribuida de DTC. Pero en SQL Server 2005 no es así, ya que incluye las nuevas transacciones promocionables, que pueden comenzar siendo transas locales para luego promocionar cuando sea necesario a transas distribuidas. Por cierto, una cosa que no me queda clara es qué entendemos por una transa local, si incluye el acceso a una única base de datos de la red o sólo en el propio equipo. Cuando lo averigue comentaré esta entrada.
Por supuesto, lo que se comenta para SQL Server 2000 supongo que será de aplicación al resto de motores de bases de datos, que no aprovecharán el Gestor de transacciones ligeras (Lightweight Transaction Manager) para el trabajo con transas promocionables. Ya he podido comprobar que con el motor Jet de Access, no sólo es así, sino que se prohíbe la utilización de transas de contexto (TransactionScope), si es detectada una se aborta el comando.

Así que vuelvo a plantearme la razón de utilizar TransactionScope: en primer lugar me parece una estructura de código más sencilla y de un alto nivel, al definir un ámbito de código donde toda operación es incluida en la transacción. Por otro lado me acerca a la interoperabilidad entre distintos motores, pero por mis pruebas actuales TransactionScope sólo me ha funcionado con SQL Server (y eficientemente con 2005), por lo que esta razón comienza a descolgarse. Y hay una nueva tercera razón: la realización de transacciones donde están involucradas distintas bases de datos, caso no frecuente pero sí posible (de hecho tengo algunos procesos críticos en que hay involucradas distintas bases de datos).

Por ello, opto por una solución combinada: creo un objeto propio AmbitoTransaccional, que en función del motor de base de datos utiliza transacciones de nivel de base de datos, o bien una instancia de TransactionScope. Así puedo seguir ofreciendo una estructura de código elegante y moderna, a la vez que funciona para cualquier motor.

Por cierto, para poder utilizar TransactionScope, es necesario activar MSDTC para permitir conexiones de cliente entrantes y salientes, tanto en el cliente como en el servidor, como se describe en los libros en pantalla de SQL Server. El procedimiento viene bien explicado en una respuesta que copio (tengo el enlace pero han quitado la página):
(1) Arranca: Herramientas administrativas - Servicios de componentes.
(2) Expande: Raíz de la consola - Equipos - Mi PC.
(3) Pulsa con el botón derecho del ratón en "Mi PC" y elige "Propiedades".
(4) En el cuadro "Propiedades de Mi PC" ficha "MSDTC" pulsa el botón "Configuración de seguridad".
(5) En el cuadro "Configuración de seguridad" activa lo siguiente:
a) Acceso a DTC desde la red
1) Permitir clientes remotos.
b) Comunicación del administrador de transacciones:
1) Permitir entrantes
2) Permitir salientes
3) No se requiere autenticación
4) Habilitar transacciones con el protocolo TIP
c) Habilitar transacciones XA

Lo que necesito es documentación acerca de como hacer esto automáticamente, quizá desde un programa de instalación.

Por cierto, y para terminar, dejo un artículo donde se habla bien de TransactionScope (por fin, me ha costado encontrarlo y aún más leerlo, aún no lo he hecho).

Etiquetas:

18 diciembre 2006

Enlace a datos con un DateTimePicker

Tengo unas cuantas cuestiones curiosas en torno al control DateTimePicker, que me van a hacer cambiar algunas cosas.
En primer lugar, estoy enlazando un Nullable a su propiedad Value. Va bien, pero lógicamente no considera el valor null, por lo que siempre muestra una fecha. Veo que el control ofrece una propiedad Checked que se muestra estableciendo su propiedad ShowCheckBox. Para utilizar esta propiedad en el enlace a datos, hay que escribir un código-pegamento (clue-code, como dicen los guiris) donde gestionar los eventos Format y Parse. Está bien explicado en la página de windowsforms.net. Por cierto, mi primera idea de enlazar tanto el Value como el Checked por separado no es la mejor, es preferible enlazar sólo el Value y gestionar bien sus eventos.
A pesar de todo, no me funciona bien el código indicado, el primer enlace con un null me falla y me deja la casilla activada, luego ya al cambiarlo en la ventana cargada va bien, pero la primera vez, aunque le pongo Checked = false, al terminar el evento Format me pone Checked a true, y no es por asignarle una fecha en Value ya que eso se hace también con la ventana cargada y funciona. Hago una trampa que no me gusta, pero que no parece peligrosa: forzar una primera vez manual, llamando al ReadValue() del Binding justo tras controlar sus eventos. De esa forma, la primera vez sigue fallando, pero cuando llega a ResetCurrentItem(), es la segunda vez y lo hace bien. Así que me queda:
- En el Load del formulario, antes de nada:
Binding binding = fechaNacimientoDateTimePicker.DataBindings["Value"];
binding.Format += new ConvertEventHandler(fechaNacimiento_Format);
binding.Parse += new ConvertEventHandler(fechaNacimiento_Parse);
binding.ReadValue();

- Y el código de esos manejadores:
private void fechaNacimiento_Parse(object sender, ConvertEventArgs e) {
DateTimePicker dtp = ((Binding)sender).Control as DateTimePicker;
if(dtp != null && !dtp.Checked)
e.Value = null;
}

private void fechaNacimiento_Format(object sender, ConvertEventArgs e) {
Nullable valorNullable = e.Value as Nullable;
if(valorNullable == null !valorNullable.HasValue) {
DateTimePicker dtp = ((Binding)sender).Control as DateTimePicker;
dtp.Checked = false;
e.Value = dtp.Value;
}
}

Tengo la idea de convertir esto en un FormatProvider para simplificar el código (e incluso poder hacerlo en el Designer, aunque no en el diseñador). Pero IFormatProvider sirve para formatos de cadena simples, no de controles, así que me creo una clase Formateador... que enlaza los eventos Format y Parse para poder reutilizarlos.

Hay un segundo problema con la fechaNacimiento, y es que intenta asignar 01/01/0001 como fecha y provoca un error. Es curioso, en mi proyecto de prueba EjemploNibiNegocio no sucede, pero en el proyecto grande Nibi.Agenda hay un comportamiento extraño:
1. Entra por BindingComplete dando ese error de que la fecha está fuera de rango, lo hace además varias veces, pero en ningún momento hace un get de la propiedad.
2. Después de establecido ese error varias veces, entonces hace los get correspondientes y es cuando asigna los valores a los controles. ¿Por qué entonces dispara los BindingComplete antes, y de dónde saca el valor 01/01/0001 para generar el error?
Pues aún no lo sé, pero lo he solucionado disparando un bind.ResetCurrentItem() en el Load del UserControl.

Etiquetas:

13 diciembre 2006

Acabando con las configuraciones (Settings)

Además de lo que comenté el otro día, he encontrado muchos más problemas con los Settings de .net, y mucho más graves que los comentados.
El primero: las clases de configuración se persisten usando un SettingsProvider, y si no se define ninguno se aplica LocalFileSettingsProvider, que almacena los parámetros de aplicación en un archivo .config y los de usuario en otro .config distinto. Hasta ahí medio bien (después veremos), pero hay un gran problema: esto sólo se cumple para las aplicaciones (proyectos exe), no para las librerías. En este caso, no se da persistencia a los parámetros (así, como suena). Y .net no ofrece más SettingsProvider que este, cuyo funcionamiento es tan indeseable. Para evitarlo, reutilizamos la instancia del provider que sí funciona en la aplicación para los Settings de las librerías, con una clase propia SettingsPersistenciaUnificada.
Por otro lado, pensando en una aplicación distribuida en una red (quizá local) que accede a una base de datos centralizada, los parámetros de aplicación también deben estar centralizados, por lo que la idea de guardarlos en la máquina en un archivo .config, válido para aplicaciones web, no tiene ningún sentido en aplicaciones winforms. Necesitamos un DbSettingsProvider que desarrollamos nosotros, y que almacena los parámetros de usuario localmente (hereda de LocalFileSettingsProvider para reusar su idea de los archivos .config), y los de aplicación en una base de datos a la que accede mediante un parámetro local llamado ConnectionString que está guardado localmente.
La ventaja de trabajar con un SettingsProvider es que en principio podríamos seguir usando los archivos .settings de Visual Studio, pero como se dijo en la entrada de anteayer, necesitamos que los parámetros de las librerías sean públicos para que los acceda, consulte y modifique la propia aplicación, por lo que optamos por crear los archivos Settings.cs manualmente (tanto los de las librerías como los de la aplicación, que necesitan indicar un SettingsProvider en un atributo, así como establecer la ConnectionString como parámetro de usuario, no de aplicación, para que se almacene en la máquina y no en la base de datos).
Por fin hemos echado a andar los Settings, tras algunos obstáculos no esperados.

Etiquetas:

11 diciembre 2006

Carencias de los Settings de Visual Studio 2005

Esta mañana me he mosqueado 3 veces con los archivos .settings del Nibi.Agenda que estamos haciendo (cada vez estamos más lejos de Sota y sigo escribiendo aquí, ya lo sé, pero ya volveremos, y además no tengo más blog, para lo que escribo). Comienzo escribiendo que me gusta bastante el modo de parametrización de las aplicaciones con los archivos .settings y los .config, además de que es muy fácil de usar. He necesitado crear un parámetro en Nibi.Agenda.dll con el que indicar si queremos que el nombre fiscal de una persona física se componga como Nombre de pila y Apellidos, o como Apellidos y Nombre. Me he encontrado los 3 siguientes problemas, que he resuelto en la forma que también comento:
  1. Con el editor de archivos .settings de Visual Studio 2005 no puedo definir un parámetro usando un tipo enumerado propio que defino en la misma librería, en este caso TipoOrdenNombreApellidos, que tiene 3 valores posibles. Sólo me salen disponibles los tipos de System, así como los de Nibi.Negocio.dll (cuyo proyecto no está incluido en la solución, sólo la dependencia de la dll).
    Lo he solventado editando manualmente el archivo .settings, que es un XML. Lo que sí hace bien es que al editarlo el Designer se actualiza automáticamente.
  2. Los parámetros creados no tienen método Set, son propiedades que sólo tienen un Get. No lo entiendo. ¿Cómo se supone que se asignan, editando a mano el archivo app.config?
    Lo resuelvo añadiendo al Designer el código del set, que es muy simple ya que se basa en la indexación ofrecida por la clase padre.
  3. La clase de los Settings es Internal, lo que significa que sólo puede accederse desde el interior del propio proyecto. ¿Por qué no puedo asignar los parámetros desde fuera, en este caso desde la aplicación principal, ya que el proyecto es una librería?
    Se soluciona cambiando ese internal por public, también en el Designer.

Hay que destacar que las dos últimas soluciones exigen editar el archivo Designer, por lo que cualquier cambio en los Settings destruirá estos cambios. No es normal que Visual Studio 2005 ofrezca un soporte tan deficiente para una característica tan potente ofrecida por el .net 2.0, pero saltaremos ante los anteriores obstáculos para seguir beneficiándonos de ese soporte para parámetros que ya digo que me parece bastante potente.

Tengo que profundizar un poco más en el manejo de parámetros entre librerías y aplicación principal: ¿debo repetir en los settings de la aplicación los parámetros que ya están en las librerías, o sólo los que necesite, o no los repito nunca y accedo directamente al settings correspondiente en cada caso? Quizá lo comente más adelante, cuando lo decida.

Etiquetas: