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: ,