16 septiembre 2008

Falta grave de recursos en librerias

Ya tengo muy delimitado el funcionamiento de los temas y estilos, basado en los diccionarios de recursos de WPF. En líneas generales, tenemos una librería Temas que ofrece un tema genérico aplicable a cualquier aplicación, el cual básicamente no modifica el estilo básico del sistema pero añade nuevos estilos y plantillas genéricas, como EstiloVentana (para poder modificar todas las ventanas, ya que establecer la key a Window no funciona), EstiloTitulo (para los label que se usan como título), EstiloEtiquetaDato (para los label que se usan como dato y no como etiqueta)... Y muchos otros estilos comunes.
A continuación, cada librería que incluye interfaz complementa este diccionario con nuevos estilos que necesita, a la vez que hace uso de los recursos "heredados" de Temas. Así la librería publica a su vez un diccionario, normalmente de nombre Generico.xaml.
Por último, en la aplicación se reunen estos diccionarios en uno propio Generico.xaml, y se establece en App.xaml como diccionario de recursos de la aplicación, de forma que se utilizará en diseño en VS2008 automáticamente.
Y aquí reside el problema: en que esto que podemos hacer en la aplicación no podemos hacerlo en las librerías, es decir, no puede definirse el diccionario de recursos global de la librería para ser utilizado en diseño, por lo que si incluimos algo tan simple como {StaticResource EstiloVentana} como estilo de una ventana, esta ya no se muestra en el diseñador, alegando que ese recurso no se encuentra.
Según leo aquí, Marco Zhou dice que esa función debe realizarla Themes\generic.xaml en las librerías, un archivo que debe residir en una carpeta Themes, y que la librería debe contener un atributo ThemeInfo para informar de que se utilice. No sé si esto funciona en tiempo de ejecución ni me importa, la cuestión es que en tiempo de diseño sigo recibiendo el mismo error de recurso no encontrado.
Por lo cual... a la MIERDA. Para salir del paso, usaremos DynamicResource en lugar de StaticResource, lo cual evita el error, pero es algo que yo quería evitar, así que sigo a la espera de una solución. Creo que se lo voy a escribir al chino Zhou, a ver si sabe algo.

P.D. Si el otro día dije que me cargué CommandBinder para integrarlo en las clases que lo necesitaban, finalmente he vuelto a una clase equivalente pero "ehpañola": Gancho. Aunque las causas dan para otra entrada...

Etiquetas: , ,

02 septiembre 2008

Planteamiento final para estilos y temas

Voy a resumir cómo queda finalmente la infraestructura de recursos de nuestra aplicación Precove sobre WPF. Va a tener un bonito soporte para temas. En resumen, cada aplicación incluirá un tema genérico de serie, que luego podrá ser sustituido por otro tema (que podrá venir en una librería externa); este tema no sustituye completamente el tema genérico, sino que redefine los elementos que desee y deja inalterados los que no redefina. Hay involucrados diferentes niveles, así que lo explico mejor por capas:
  1. En la base contamos con dos librerías comunes: Nibi.Presenta y Nibi.Presenta.Temas. En la segunda se ofrece una clase Gestor para aplicar un tema dado (un diccionario de recursos) a una aplicación, basándose normalmente en un tema genérico que la aplicación ofrecerá (si no se proporciona ese tema genérico, el nuevo tema se aplica sobre el tema actual, lo que puede llevar a mezclas curiosas). Además, en esta librería también se ofrece un tema Generico.xaml utilizable en cualquier aplicación y que más bien lo que hace es definir qué claves pueden redefinirse, normalmente sin afectar a la apariencia (sólo con estilos vacíos, o con colores que referencian a los de Windows). Esta definición aparentemente innecesaria sirve para que las referencias StaticResource y DynamicResource no produzcan errores ni advertencias en diseño ni en compilación.
    En Nibi.Presenta se ofrece una serie de comandos básicos, cuyos iconos por defecto se ofrecen en otro diccionario de recursos Iconos.xaml.
  2. En cada librería de interfaz (que nosotros llamamos Forms) se podrán definir los recursos necesarios para visualizar correctamente los elementos de dicha interfaz. Estos recursos contendrán plantillas, estilos, iconos, quizá cadenas e incluso menús de contexto, viniendo a complementar los ofrecidos en Nibi.Presenta.Temas. Cada uno de estos recursos podrá redefinirse posteriormente de forma aislada en un tema personalizado para la aplicación.
  3. En la aplicación se agruparán todos los estilos de sus librerías en un Generico.xaml propio, donde también podrán agregarse (o redefinirse) recursos si fuera necesario. Este será el tema genérico global, el que estará definido en App.xaml y el que se pasará a Gestor para que lo use como base al aplicar un nuevo tema.
  4. En Nibi.Presenta.Temas se ofrecerán más temas, que redefinirán los elementos que define la propia librería, ya que no tendrá conocimiento de los elementos definidos por otras librerías. Así, establecerá elementos como el estilo de los controles comunes (ventanas, botones, cajas de texto...) y una paleta de colores.
  5. Podremos completar los temas básicos ofrecidos con elementos concretos de la interfaz de la aplicación definiendo un tema nuevo que lo extienda, lo que podemos hacer en una librería externa o en la misma aplicación.

El algoritmo de fusión de un nuevo tema con el tema genérico ha sido programado en Gestor (es muy sencillo, pero tiene en cuenta todos los diccionarios que incluye el tema). Al poder fusionar incluso sobre un tema ya aplicado (no siempre sobre el genérico de la aplicación), se abren posibilidades como la de utilizar esta infraestructura para traducir la aplicación, incluyendo los textos de comandos, etiquetas... como recursos que también pueden ser redefinidos. Aunque es un poco engorroso hacer esto ahora, pero to'sandará.

Etiquetas: , , ,

28 agosto 2008

Comandos en WPF

Aunque hace tiempo que comencé a aplicar los comandos de WPF en Precove, cada vez le veo más color. Al principio usaba el comportamiento básico ofrecido por WPF: los comandos son una abstracción intermedia entre un botón (elemento visual) y su manejador (código). En una ventana defines un botón asociado al comando (en lugar de capturar su evento Click), y en esa misma ventana estableces un CommandBinding que dice qué hacer cuando se ejecute el comando (con un manejador cuyo código está en la misma ventana). También puedes decirle cuándo se puede ejecutar el comando, con CanExecute, lo que automáticamente deshabilita los botones asociados con él. Por supuesto, esta es una de las ventajas, puede haber distintos botones, menús... asociados con un comando; incluso se pueden ejecutar por código, necesario por ejemplo al hacer doble clic en un elemento de una tabla.
Ahora lo he ampliado, al descubrir que el código que ejecuta un comando suele ser el mismo independientemente del contexto. Realmente tenemos dos casos: el típico ejemplo del botón Copiar, o más concretamente el botón Nuevo, que según la ventana o control que tenga el foco se comportarán de forma distinta; y el de un botón como Dar Presupuesto, que siempre muestra el asistente como una nueva ventana. Para este segundo caso, he incluido en Nibi.Presenta una clase abstracta SmartRoutedUICommand, en la que al heredar podemos definir un código estándar para los métodos CanExecute y Execute. Y para que este código se ejecute sin necesidad de añadir un trivial CommandBinding, se ofrece una clase CommandBinder que ofrece una propiedad Command que además de asignar el comando a un botón, crea el CommandBinding con los métodos del comando. Obviamente, sólo se puede usar con comandos SmartRoutedUICommand, que son los que ofrecen un código estándar.
Otra ampliación adicional ha sido la utilización de RoutedUICommand, y su ampliación a ImageRoutedUICommand añadiendo una descripción y una imagen (ImageSource para WPF, realmente es una ruta para cargar la imagen sólo cuando el motor gráfico necesite mostrarla). SmartRoutedUICommand hereda de ImageRoutedUICommand, con lo que ofrece esta información adicional para automatizar su visualización en la barra de botones, como botón suelto, como menú... Para esta visualización, se define un estilo con la clave ToolBar.ButtonStyleKey para que se aplique a todos los botones que se muestren en una barra de herramientas, y que utilice los datos del Command asociado al botón. Así lo botones sólo deben definir esa propiedad, Command, y de ahí se sacan sus propiedades como Imagen, Título y Descripción (para el ToolTip). Este estilo estará por defecto en la aplicación, pero si en un tema se redefine, deberá utilizarse el mismo sistema para obtener y establecer las propiedades a partir del comando.
Por último, comentar una pega que me he encontrado con el grid de Xceed. Aunque ellos en su página web informan de que son compatibles con los comandos de WPF, y que incluso ofrecen nuevos comandos como Editar celda que uno puede asociar a sus botones y el grid responde a su ejecución, yo le encuentro una pega mucho más sencilla: la documentación de WPF afirma que los comandos son actualizados en cada cambio en la interfaz. Pues bien, mientras nos movemos por el grid no se actualiza nada. En cambio, si me meto en un textbox que he puesto junto a él, nada más entrar el foco se actualizan los comandos (se ejecutan sus CanExecute). Puede ser culpa de que el grid se encuentra dentro de un control de usuario y el botón fuera, pero el hecho y verdad es que he tenido que repetir el CommandBinding (este no es un Smart...) dentro y fuera de ese control de usuario para que afecte en los dos sitios.

Etiquetas: ,

06 agosto 2008

Sincronización completa con Binding de WPF

Para empezar, aclarar que los binding de WPF están mucho mejor que los de Windows.Forms, no sé si ya lo había dicho antes. Ni punto de comparación. Pero son mejorables en muchos aspectos, y sobre todo en el que voy a contar ahora de una forma muy fácil.
Estamos usando converters (que implementan IValueConverter) para dar formato a los datos que se muestran a través de los binding, y también para procesar de vuelta el valor introducido por el usuario y llevarlo al negocio. En concreto tenemos un FormatoConverter que permite un parámetro con la cadena de formato, y que usamos para dar formato de moneda, de número, de fecha... así como para parsear el valor de cualquiera de estos tipos escrito por el usuario.
El valor ofrecido por el conversor se asigna al objeto de negocio, y si hay algún error, se notifica al usuario mediante la propiedad ValidatesOnDataErrors del Binding (nuestro negocio implementa IDataError para esto). Hasta aquí muy bien.
Si durante el parseo detectamos un error, por ejemplo que se escriba texto cuando esperamos un número, mi intención era lanzar un error que sea detectado como los de negocio, con lo que se muestre de la misma forma al usuario. Esto no es posible puesto que ConvertBack (el método donde se parsea en IValueConverter) no ofrece una vía para notificar este tipo de errores, y la documentación dice claramente que cualquier excepción disparada en su cuerpo no será controlado y saltará como error en tiempo de ejecución. Por esto decía que era muy fácil de solucionar por parte de Microsoft: basta con esperar este error, y mostrarlo tal como se hace con los errores producidos al asignar el binding.
Al no contar con este simple mecanismo, lo único que podemos hacer (y que hace todo el mundo) es devolver Binding.DoNothing o DependencyProperty.UnsetValue, los cuales anulan la escritura en el objeto de negocio pero no notifican error... con lo cual el efecto es muy malo: queda un valor escrito en el control, que al usuario no le aparece que sea incorrecto, y en negocio queda el valor antiguo que posiblemente sea correcto, con lo que si preguntamos dice que puede guardarse... ¡pero no se guarda lo que el usuario está viendo! De ahí que yo haya renunciado al DoNothing cuando no se reconoce la cadena introducida, y en cambio devuelvo null (que posiblemente será un valor incorrecto para el negocio).
Para quedarme con esta solución, solo necesito que cumpla con un requisito que ya he comentado de pasada: los valores de negocio y de los controles mostrados al usuario deben estar siempre sincronizados. Aunque parece algo obvio, ya he comentado que no sucede si usamos DoNothing, y tampoco si devolvemos null, porque en el control queda el valor incorrecto. Así que necesitamos refrescar el control con el valor del negocio (para mostrar al usuario que su valor no se ha admitido, lo cual es mucho peor que dar un mensaje de error o destacar en rojo el control, pero es la solución más sencilla dentro del marco del Binding de WPF). Este refresco también es necesario en otro caso distinto: cuando el negocio corrige el valor escrito, como al escribir un NIF (se quitan puntos y guiones e incluso puede que se añada la letra) o al escribir una matrícula (se quitan guiones), o si se corrigen mayúsculas... La cuestión es que esto no sucede, de forma que al realizar esta corrección queda el valor corregido en negocio y el original escrito por el usuario en el control, y hay que forzar un refresco del binding para actualizar el dato mostrado. Vamos a especificar de forma global un manejador LostFocus que haga esta actualización al abandonar un control (igual que lo estamos haciendo en el GotFocus para seleccionar todo el texto), localizando todos los binding del control y usando su método UpdateTarget.
Y ahí lo dejo, sin conclusión ni nada. Hala.

[Cambio 12:18] Bueno, venga, añadiré algo... En primer lugar matizar y cambiar lo que he comentado del null en lugar de DoNothing: esto está bien para algunos casos, pero sólo se aplica cuando usamos nuestro converter, por lo que el resto de binding actúan como por defecto, con el DoNothing. Como esto es un poco inconsistente (si pones texto en un control que es numérico, al salir en un caso se cambia por un cero, y en otro se queda el número que había antes), vamos a optar por el comportamiento implementado por WPF: usar el DoNothing. Esto hace que nunca se marque error al escribir texto (salvo si con el valor anterior ya había un error, que se mantiene).
Y una conclusión: al final no he solucionado el primer problema, sólo el segundo de mantener sincronizado el control con el negocio al abandonar el control; aunque esta solución ayuda un poco al primer problema, al quitar valores que no se han podido parsear. Pero queda pendiente un mecanismo que avise del error cuando escribamos algo que no pueda convertirse al tipo de la propiedad de negocio. Esperemos a Microsoft.

Etiquetas: ,

21 julio 2008

Nuestro propio Main en WPF

Como sabemos el método Main es el punto de entrada de un programa en C#. Cuando creamos una nueva aplicación de WinForms por defecto crea un fichero Program.cs en el cual encontramos dicho método y donde podemos realizar las tareas que creamos oportunas antes de iniciar la aplicación. Sin embargo, en WPF en lugar de dicho fichero encontramos un fichero App.xaml (ApplicationDefinition) en el cual no encontraremos el método Main, siendo el compilador el que generará dicho método, y únicamente podemos indicar la ventana que se mostrará al iniciar la aplicación.

En nuestro caso necesitamos realizar una serie de tareas antes de iniciar la aplicación, como son la búsqueda de la base de datos y mostrar una ventana Splash mientras se realiza dicha búsqueda y la carga de la aplicación. De esta forma necesitábamos crear nuestro propio método Main, para ello crearemos nuestra clase App que heredará de System.Windows.Application y en la cual definiremos el método Main, indicandole al compilador que use el nuevo fichero App.cs como objeto de inicio y eliminaremos el fichero App.xaml (si tenemos algún recurso definido en el fichero debemos moverlo a un nuevo fichero y añadirlo a los diccionarios de la aplicación en el Main).

En la siguiente web podemos ver los pasos a seguir para mostrar una ventana Splash antes de cargar la aplicación.

11 julio 2008

Primer contacto serio con Linq

He hecho mi primer proyectito con Linq to SQL, se trata de una pasarela de sincronización entre 2 sistemas: nuestra BD en SQL y un AS/400 que nos genera archivos de texto. Os cuento mi experiencia, la principal conclusión es que no me ofrece un buen rendimiento (y eso que he conseguido mejorarlo varias veces, pero aún así no es suficiente) y por eso quiero compartirlo por si hay algo que se pueda hacer de otra forma (no con SqlCommand, eso ya sé hacerlo, he probado en este proyecto con Linq por aprender y porque realmente el rendimiento no es necesario, se ejecuta durante la noche).
Veréis: para la prueba de fuego he hecho una carga de 125.000 artículos que se escribían en 2 tablas. Pues bien, la principal pregunta es: ¿se hace todo con un DataContext abierto, o voy cerrando y abriendo para cada artículo? Es que mantengo en memoria muchas entidades para acelerar las búsquedas, y si establezco una de esas entidades en un nuevo artículo, como se guarda en otro DataContext distinto a donde se cargó, pues dice que no es aplicable. Así que sólo me funciona manteniendo abierto el mismo DataContext desde el principio. Lo cual no sería problemático si no fuera porque el rendimiento se va degradando conforme más se usa el DataContext, parece que al llevar el seguimiento de tantas entidades se ralentiza. Comienza a actualizar a más de 30 artículos por segundo, y cuando lleva 40.000 artículos escritos va por 3 artículos por segundo, lo cual es de pena (cada artículo son 2 INSERT).
La otra cuestión es acerca de los INSERT que genera Linq: es especialmente lento porque el comando Insert espera a recibir la respuesta de la identidad del nuevo registro insertado. ¿Hay alguna forma en Linq de generar nuevas entidades pero no recuperar su Id? Esto lo veo mucho más improbable, y no he encontrado nada al respecto, sólo críticas a la falta de procesos por lote en Linq pero no referentes a esta cuestión puntual (sino a UPDATEs de múltiples registros).

Mientras no resolvamos la cuestión de si uno o varios DataContext, y en el segundo caso de cómo usar una entidad de un DataContext sobre otro, no veo viable utilizar Linq para otros proyectos de mayor envergadura. Supongo que todas estas cuestiones serán equivalentes en Linq to Entities, que es lo que realmente aspiramos a utilizar.

Etiquetas:

09 julio 2008

Publicar una aplicación WPF

Hay un problema en algunos casos al publicar una aplicación hecha en WPF. Dice que no puede generar la aplicación porque no encuentra los archivos xxx.g.i.cs (lo dice una vez por cada XAML). He encontrado que es un error reconocido por Microsoft, y que se solucionará en SP1, pero mientras no puedo generar la versión de la Pasarela Domlez con Motorwin. Ya la original me costó mucho generarla, por fallos como este, pero tras mucho probar lo conseguí.
En los comentarios a ese informe de fallo, alguien cuenta en español que matando las instancias del proyecto que estés compilando (en el taskmgr) sí tira, pero a mí no me sirve.
Tras muchas pruebas con otros más de los rodeos que dan en la página, sólo consigo que me publique en modo Debug, no en Release. Y además esa publicación no tira, porque al ejecutar da un error que depurando descubro que es: No se encuentra App.xaml. Así que como tampoco soy capaz de instalar la Beta de VS 2008 SP1, paso del tema y me genero un RAR autoextraíble de toda la vida.

Tengo otro problema y es que una aplicación instalada desde una publicación a) no sé en qué carpeta está; b) no puedo machacarla porque el manifiesto de la aplicación no será válido para los nuevos archivos; y c) no sé cómo generar una actualización, así que tengo que desinstalar y volver a instalar siempre. Este problema con el RAR también me lo quito. Así que en esas estamos, tecnología punta ClickOnce... RAR.