Validaciones de Formularios en Wicket
Introducción: validaciones de negocio
Antes que nada, ya hablamos bastante de las validaciones de negocio cuando trabajamos con Arena. Así que pueden releer la sección Desarrollo de UI con componentes: Arena. Validación y manejo de errores.
Por otro lado ya vimos también como adaptar Wicket a fin de que entienda nuestras UserExceptions ya que nuestros objetos ya implementan reglas de negocio. Esto está en UserException en Wicket.
Teniendo en cuenta esas dos cosas, que son bastante importantes seguir recordando, vamos a ver ahora, otros mecanismos que provee Wicket para validaciones, que tomaremos con pinzas, ya veremos por qué :)
Validators
Wicket como muchos otros frameworks de UI modelan la idea de validadores, y así provee un punto de extensión o parametrización. En particular existen dos tipos de validadores, ambos asociados a la idea de formularios y sus componentes.
- IValidator: para validar el valor de un componente del form
- IFormValidator: para validar el formulario en sí, es decir, no un único componente, sino tal vez dos, o más.
IValidators
A cualquier FormComponent le podemos agregar un objeto IValidator. Éste es un pequeño strategy con una única responsabilidad:
public interface IValidator<T> extends IClusterable { /** * Validates the <code>IValidatable</code> instance. Validation errors should be reported using * the {@link IValidatable#error(IValidationError)} method. * @param validatable the <code>IValidatable</code> instance being validated */ void validate(IValidatable<T> validatable); }
Es decir que dado un valor, debe validarlo. En caso de detectar un error lo puede reportar al mismo objeto que se pasa por parámetro, de tipo IValidatable, que tiene un método error(..) al cual le podemos indicar el/los errores. Además, este validatable tiene al valor a validar con getValue()
Siguiendo nuestro ejemplo de la CalculadorDivision, agregamos un IValidator a través de una clase anónima al TextField bindeado a la property "divisor".
Ejemplo:
TextField<Double> divisorTextField = new TextField<Double>("divisor"); divisorTextField.add(new IValidator<Double>() { public void validate(IValidatable<Double> validatable) { double valor = validatable.getValue(); if (valor == 0) { ValidationError error = new ValidationError(); error.setMessage("El divisor no puede ser cero!"); validatable.error(error); } } });
Ahora al probar la calculadora:
En principio el efecto es el mismo que lográbamos cuando teníamos la validación dentro del setter. Sin embargo hay una sutil diferencia.
Si probamos ingresar un valor inválido en términos de tipo en el dividendo, es decir escribimos texto en lugar de números, pero además, ingresamos el "0" en el divisor veremos ambos errores todos a la vez:
En cambio, si volvemos a la solución anterior, con la validación en el setDivisor, y hacemos lo mismo, veremos solo el error del dividendo..
Recién una vez que arreglamos el valor del dividendo es que nos aparecerá el otro error de la validación del setter. También sucede que si tenemos validaciones en varios setters, se va a ver sólo el error del primero que falle.
¿Por qué es esto? Para entenderlo, tenemos que entender cómo funciona el ciclo de ejecución de los formularios en wicket
Ciclo de Ejecución de un Form (Workflow)
Lo que explica la diferencia de comportamiento en el ejemplo anterior es el ciclo de ejecución o workflow por el que pasa el submit de un formulario.
Wicket tiene ciertos pasos que se ejecutan uno a continuación del otro. Los vemos en la imagen a continuación:
Esto quiere decir que Wicket:
- Primero checkea que hayan llegado todos los valores como parámetros del request (recuerden que hay FormComponentes que se pueden marcar como requeridos)
- Luego convierte los parámetros que llegan en formato String obviamente, desde HTTP: aquí podría suceder que el texto "abc" como el que escribimos en "dividendo" no se pueda convertir a Double !
- Luego ejecuta todos los IValidators asociados a los componentes: con lo cual si alguno falla, no se sigue al siguiente paso:
- Luego setea todos los valores en cada IModel correspondiente. Si tenemos IModels que estan bindeados contra una propiedad del modelo, esto significará que se llamará a los setters.
- Recién después de esto se ejecuta el onSubmit del Form y del Button: y solo en caso en que no haya fallado ningún paso anterior.
Conversiones y Validaciones
Las conversiones y validaciones se hacen sobre todos los controles. Errores en la conversión no cortan el flujo de ejecución (está mal el dibujo), las validaciones de los demás componentes se efectúan de igual manera.
Esto quiere decir que wicket:
- intenta convertir el parametro 1, por ejemplo dividendo. Si existe un error, lo recordará, pero sigue adelante con los demás parámetros
- intenta convertir el valor del parámetro 2 , ejemplo divisor.
- Luego valida el divisor (que no falló su conversión). Si existe un error, también lo recuerda y continúa con los validadores de los otros componentes (si existieran).
- Finalmente, antes de pasar al paso de "Actualizar el Modelo", Wicket sabe que hubo errores entonces, no lo hace y corta la ejecución ahí.
El efecto práctico y visible de esto es que todos los errores aparecen de una sola vez, juntos, acumulados. Así la experiencia de usuario es mucho más amigable.
Diferencia entre validaciones en setters y el uso de IValidators
Entonces ahora sí podemos terminar de entender la diferencia entre tener la validación en el setter o en un Validator.
Como vimos si está en el validator, el usuario podrá ver todos los errores agrupados, porque siempre los ejecuta todos.
En cambio, la validación dentro del setter se ejecuta en el último paso, cuando Wicket intenta actualizar el dominio. Y como vimos antes solo se llega a este paso si no hubo errores en los pasos anteriores (Conversión y Validación). Por eso es que la experiencia de usuario es un poco mas fea, porque los errores aparecen por etapas. Primero los de formato, y luego los de los setters.
Por otro lado, si un setter falla, y produce un error, no continúa intentando popular las demás properties (porque podría dejar el objeto por la mitad). Eso explica el segundo problema de que ante el primer error en un setter no continua con los demás.
De hecho en este caso verán que es bastante problemático tener la validación solo en los setters, porque al fallar uno, wicket "resetea" todo lo que escribió el usuario en los siguientes controles.
Entonces comparemos las soluciones:
- Validación en Setters:
- Ventajas:
- Queda en el modelo, no "saco" la lógica a la vista/controller
- Se ejecuta siempre y se puede testear fácilmente
- La UI me queda más sencilla (Solo binding)
- Desventajas:
- La experiencia de usuario es mala:
- muestra los errores de a uno
- wicket "se olvida" de los siguientes valores luego del error
- La experiencia de usuario es mala:
- Ventajas:
- IValidator
- Ventajas:
- Muestra todos los errores agrupados
- Mantiene la consistencia del dominio
- No se olvida los valores correctos
- Desventaja:
- Me fuerza a especificar algo más en la vista/controllers
- Me fuerza a escribir la validación fuera del dominio
- Ventajas:
El problema más grave, es el último, subrayado.
Sin embargo, tenemos una forma de evitar la repetición. Veamos:
Solución: Validaciones con IValidators sin duplicación y con lógica en el dominio
Entonces el IValidator lo vamos a necesitar, porque es el punto de enganche para hacer validaciones de un valor (ojo, es de un valor, ¡no de la property! es de un valor que potencialmente se va a utilizar para setear la property).
Sin embargo, que tengamos que codificar un IValidator que ejecute la validación NO quiere decir que las lineas de código de la regla de negocio deban estar en el IValidator. Tranquílamente el IValidator puede delegar en el objeto de negocio.
Veamos como quedaría nuestro ejemplo del "divisor" ahora. En lugar de esto:
TextField<Double> divisorTextField = new TextField<Double>("divisor"); divisorTextField.add(new IValidator<Double>() { public void validate(IValidatable<Double> validatable) { double valor = validatable.getValue(); if (valor == 0) { ValidationError error = new ValidationError(); error.setMessage("El divisor no puede ser cero!"); validatable.error(error); } } });
Podemos tener la validación escrita en el modelo, aunque no en el setter, sino en un nuevo método específico para validar divisores:
public void validarDivisor(double unDivisor) { if (unDivisor == 0) { throw new UserException("No se puede dividir por cero!"); } }
Y ahora, la llamamos desde el IValidator:
final TextField<Double> divisorTextField = new TextField<Double>("divisor"); divisorTextField.add(new IValidator<Double>() { public void validate(IValidatable<Double> validatable) { try { CalculadoraDivision calculadora = (CalculadoraDivision) divisorTextField.getForm().getModelObject(); calculadora.validarDivisor(validatable.getValue()); } catch (UserException e) { ValidationError error = new ValidationError(); error.setMessage(e.getMessage()); validatable.error(error); } } });
Ven que el validator ahora se parece mucho a los botones. Símplemente envían un mensaje a métodos "validarXXX" del modelo y maneja UserExceptions.
Además, como la lógica de validación está en el modelo, podríamos si quisiéramos mantener la idea de ejecutarla en el setter, sin repetir código:
public void setDivisor(double divisor) { validarDivisor(divisor); this.divisor = divisor; }
Generalización de los IValidators
Es posible mejorar aún más la solución, sobre todo si, como podemos ver ahora involucra repeticiones. Si notan el código del IValidator anterior, se darán cuenta que se puede generalizar, porque es muy probable que también tengamos validaciones de otras propiedades en otros objetos de negocio de mi aplicación. El IValidator siempre hace lo siguiente:
- Obtiene el modelo contenedor
- Leinvoca al método que tiene la validación, pasándole como parámetro el nuevo valor potencial
- Maneja la UserException mostrando el error.
Vemos que la parte que va a cambiar es en realidad
- El tipo de objeto modelo
- El método al que llamamos.
Entonces ¡si pudiéramos establecer un contrato para dado un objeto y una propiedad poder saber cuál método llamar! Y si pudiéramos llamar a un método en forma dinámica, sin saber cuál es de antemano podríamos hacer un solo IValidator y luego reutilizarlo.
Entonces vamos a definir un contrato o convención
- Toda property podrá tener, además de un getter y un setter, un método público
- sin valor de retorno (void)
- que reciba un único parámetro del mismo tipo que la property,
- cuyo nombre comience con "validar" y siga con el nombre de la property con la primer letra en mayúscula.
- dicha función deberá lanzar UserException en caso en que el valor dado no cumpla con las reglas que definen la validez de los valores de la property.
Ejemplos:
- property "saldo" de tipo Double -> validarSaldo(Double saldo)
- property "edad" de tipo "Integer" -> validarEdad(Integer edad)
- property "telefono" de tipo "String" -> validarTelefono(String telefono)
Ahora sí, podríamos cambiar nuestro ejemplo del divisor así:
final TextField<Double> divisorTextField = new TextField<Double>("divisor"); divisorTextField.add(new PropertyValidator(divisorTextField, "divisor")); form.add(divisorTextField);
La magia acá la hace nuestra nueva clase PropertyValidator que utiliza reflection para invocar el método validarDivisor(), o en realidad validarXXX() según lo que le pasemos por parámetro en el constructor.
Necesita que le pasemos por parámetro también el mismo divisorTextField porque le tiene que obtener el modelo, es decir el objeto al cual invocarle el método validarXXX().
Sin embargo, si investigamos un poco mejor Wicket veremos que hay una forma de simplificar el PropertyValidator aún mas!
Resulta que si nuestro IValidator implementa la interfaz IValidatorAddListener el control al cual lo agregamos le va a invocar un método avisándole "eh!! mirá que te agregaron para validarme a mí". Con lo cual con esto nos ahorramos pasarle en el constructor la referencia a divisorTextField.
final TextField<Double> divisorTextField = new TextField<Double>("divisor"); divisorTextField.add(new PropertyValidator("divisor")); form.add(divisorTextField);
Bien ! Ahora, pensemos mejor. Si el validator ya puede conocer al control, al textfield, éste ya tiene como "id" el nombre de la property ! De hecho si ven acá estamos repitiendo "divisor" en el id del textfield y al pasarlo a PropertyValidator.
Entonces ¡saquemos también eso! ¡no es necesario!
TextField<Double> divisorTextField = new TextField<Double>("divisor"); divisorTextField.add(new PropertyValidator()); form.add(divisorTextField);
Como verán ahora quedó una solución casi casi perfecta !! Cumple con que:
- La lógica de negocio, está en el negocio. De hecho ahora le hicimos su propio método fácil de leer y entender qué es y qué hace.
- Igualmente tenemos la mejor "experiencia de usuario" mostrando todos los errores agrupados y no uno por uno, además de sin resetear los valores.
- Por otro lado, casi no tuvimos que escribir código en la vista.
Para que esta solución sea aún mejor, podríamos de hecho evitar esa linea que vamos a tener que repetir en todos los controles:
divisorTextField.add(new PropertyValidator());
Existen varias formas de solucionar eso:
- En lugar de instanciar los componentes en cada página con "new TextField()" podemos crear nuestro propio objeto WicketFactory cuya función será exclusivamente la de crear objetos, en particular crear componentes Wicket. Entonces ahora desde cada página hacemos:
TextField<Double> divisorTextField = factory.crearTextField("divisor"); form.add(divisorTextField);
Y la factory siempre le seteará un nuevo PropertyValidator automáticamente.
- Otras opciones más complejas sería, luego de construir una página, siempre hacerla pasar por un objeto que le visite todos los componentes, y le agregue a éstos un nuevo PropertyValidator
En fin, podrían existir muchas formas, pero la del Factory parece ser la más sencilla.
Factory es, de hecho un Patrón de diseño creacional. Pueden leer más acá
Implementación del PropertyValidator genérico con Reflection
Reflection y metaprogramación es en realidad un tema que verán en otra materia (Objetos III en UNQui, PHM en UNSAM), pero por si les interesa ver cómo está hecho el PropertyValidator, o bien utilizarlo en sus aplicaciones, acá les pasamos el código:
public final class PropertyValidator implements IValidator<Double>, IValidatorAddListener { private Component component; private String propertyName; public void onAdded(Component component) { this.component = component; this.propertyName = component.getId(); } public void validate(IValidatable<Double> validatable) { try { Object model = component.getParent().getDefaultModelObject(); ReflectionUtils.invokeMethod(model, getValidatePropertyMethodName(), validatable.getValue()); } catch (RuntimeException e) { if (isUserException(e)) { ValidationError error = new ValidationError(); error.setMessage(e.getCause().getCause().getMessage() + " desde el validator"); validatable.error(error); } else { throw e; } } } protected String getValidatePropertyMethodName() { return "validar" + Character.toUpperCase(this.propertyName.charAt(0)) + this.propertyName.substring(1); } protected boolean isUserException(RuntimeException e) { return e.getCause() instanceof InvocationTargetException && e.getCause().getCause() instanceof UserException; } }
IFormValidator
El segundo tipo de validador que tiene wicket es el IFormValidator. Similar el IValidator, pero que no se utiliza para validar un único valor, sino más de uno.
Acá hay un ejemplo de EqualInputValidator que ya viene con Wicket y sirve para validar que el valor de dos componentes es igual. Por ejemplo para implementar la idea de escribir un password y otro campo de "confirmación", o la "confirmación" de un email.
public void validate(Form<?> form) { // we have a choice to validate the type converted values or the raw // input values, we validate the raw input final FormComponent<?> formComponent1 = components[0]; final FormComponent<?> formComponent2 = components[1]; if (!Objects.equal(formComponent1.getInput(), formComponent2.getInput())) { error(formComponent2); } }
Como ven este validator es un poco más complejo y "rústico" porque opera con el Form.
En general aplicaremos la misma idea de tratar de mantener la lógica de negocio en el objeto, y no en estos lugares.
Feedback Panel
En todos los casos es necesario tener un componente que pueda visualizar los mensajes de error. Para eso existe el componente FeedbackPanel
Existe una variante que se utiliza para poder mostrar los mensajes de error perteneciente a cada control, justo al lado de él (o bueno, donde nosotros digamos).
Para eso ver ComponentFeedbackPanel.