Ahora bien, antes de comenzar a describir como implementar y posiblemente mejorar la validación de nuestros objetos/entidades, voy a decir que todos o la mayoría de los ejemplos que publicaré estarán en VB.Net (porque aunque sepa C#, éste me dá güeva) y también les comentaré que al día de hoy (24/Agosto/2014) hay un pequeño bug en la plantilla para la generación de entidades, es algo muy sencillo y fácil de solucionar pero que puede causar cierta molestia a la hora de codificar la validación de las entidades. Este bug se refiere a que se omitió la siguiente sentencia al inicio del archivo donde se define cada clase:
Imports Cooperator.Framework.Core
Sin esta línea, al quitar el comentario en la línea que contiene la instrucción:
Implements IValidable
el editor de código nos marcará esta instrucción como errónea, ya que para poder implementar dicha interface necesitamos el Imports (o using en C#) que puse en el párrafo anterior. Así que ustedes sabrán si quieren estar incluyendo esa línea manualmente cada vez que implementen la validación de sus entidades o si prefieren modificar la plantilla de generación (Entity.cs) en el proyecto del Modeler para corregir el bug y que sus entidades se generen perfectamente. La plantilla para la creación de Objects sí contiene esta línea, así que no tendremos problemas con los objetos.
Ahora sí, vamos hablando sobre como validar objetos/entidades creados con Cooperator Framework.
Los objetos/entidades se encuentran dentro del proyecto Entities que se crea como parte de la solución que el Modeler genera por nosotros. Cada objeto/entidad esta formado por dos clases (gracias al uso de las clases parciales que se introdujeron como una característica del .Net Framework 2.0), una de las clases se encuentra en la subcarpeta Auto dentro del mismo proyecto y regularmente lleva un nombre con la siguiente nomenclatura NombreDeTabla.Auto.vb para las entidades o NombreDeTablaObject.Auto.vb para los Objects, esta clase no se debe modificar por nosotros, ya que cualquier cambio en ella se pierde al regenerar nuestro proyecto como producto de algún cambio en el modelo; la otra clase se encuentra dentro del proyecto Entities en la subcarpeta Entities o en la subcarpeta Objects dependiendo de como se haya decidido generarla y es la que los desarrolladores podemos modificar para realizar las validaciones pertinentes o extender la clase con propiedades y métodos nuevos sin temor de perder los cambios cuando se actualiza el proyecto con el Modeler.
Bien pues, entonces para poder validar nuestros objetos/entidades, primero se abre el archivo que contiene la definición de la clase en cuestión (el que no tiene Auto en el nombre) en el proyecto Entities. Tomemos como ejemplo el siguiente fragmento de código fuente:
- ''' <summary>
- ''' This class represents the Banco entity.
- ''' </summary>
- ''' <remarks></remarks>
- <Serializable> _
- Public Partial Class Banco
- ' Implements IValidable
- ' ''' <summary>
- ' ''' When IValidable is implemented, this method is invoked by Gateway before Insert or Update to validate Object.
- ' ''' </summary>
- ' ''' <remarks></remarks>
- ' Public Sub Validate() Implements IValidable.Validate
- ' 'Example:
- ' If String.IsNullOrEmpty(me.IdBanco) Then Throw New RuleValidationException("IdBanco can't be null")
- ' End Sub
- End Class
Retiramos los caracteres que ponen como comentario una línea de código ( ' en VB, // en C#) en la línea que dice:
' Implements IValidable
A los CSharperos les aparecerá algo como:
// : IValidable
que se encuentra justo debajo de la línea que declara la clase y después descomentando el método Validate y sus comentarios XML, quedando de la siguiente manera:
- ''' <summary>
- ''' This class represents the Banco entity.
- ''' </summary>
- ''' <remarks></remarks>
- <Serializable> _
- Public Partial Class Banco
- Implements IValidable
- ''' <summary>
- ''' When IValidable is implemented, this method is invoked by Gateway before Insert or Update to validate Object.
- ''' </summary>
- ''' <remarks></remarks>
- Public Sub Validate() Implements IValidable.Validate
- 'Example:
- If String.IsNullOrEmpty(Me.IdBanco) Then Throw New RuleValidationException("IdBanco can't be null")
- End Sub
- End Class
Recuerden que para quitar comentarios se pueden ayudar mucho usando la combinación de teclas <Ctrl> + <K> + <U> tanto en VB como C#.
Eso es suficiente para comenzar a validar nuestros objetos/entidades. Ahora solo hay que agregar las validaciones necesarias para asegurarnos que nuestro objeto/entidad contenga los datos correctos antes de agregarlo a la base de datos y en caso contrario arroje una excepción que pueda ser capturada en nuestro proyecto de presentación. Por ejemplo, podríamos tener algo como lo siguiente:
- Private Sub btnGuardar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGuardar.Click
- Try
- ' Si un objeto no se modifica y se intenta guardar hay que validarlo de todas formas.
- BancoRules.Validate(oBanco)
- ' Se muestra el cursor de espera mientras se graba la información en la BD.
- Me.Cursor = Cursors.WaitCursor
- Try
- ' Si es un objeto nuevo...
- If ObjectStateHelper.IsNew(oBanco) Then
- BancoRules.InsertBanco(oBanco) ' Se guarda (inserta) el objeto
- oBanco = New Banco ' Se crea un nuevo objeto.
- Else
- BancoRules.SaveBanco(oBanco) ' Se guarda (actualiza) el objeto
- Me.Close() ' Se cierra el formulario en caso de Update.
- End If
- Catch ex As Exception
- ' Se restaura el cursor de ratón
- Me.Cursor = Cursors.Default
- ' Si se captura una excepción, se muestran sus detalles.
- MessageBox.Show(String.Format("Detalles del error:{0}{1}", Environment.NewLine, ex.Message), _
- "Ocurrieron errores al intentar guardar la información", _
- MessageBoxButtons.OK, MessageBoxIcon.Warning)
- ' Si se estaban modificando los datos de un objeto, se cierra la ventana.
- If EditMode = FormEditModeEnum.Updating Then
- Me.Close()
- End If
- Finally
- ' Se muestra el cursor por defecto.
- Me.Cursor = Cursors.Default
- End Try
- Catch ex As Exception
- ' Si se captura una excepción, se muestran sus detalles.
- MessageBox.Show(String.Format("Detalles del error:{0}{1}", Environment.NewLine, ex.Message), _
- "Ocurrieron errores al validar la información", _
- MessageBoxButtons.OK, MessageBoxIcon.Warning)
- End Try
- End Sub
Al encerrar las operaciones de validación, insert y update en bloques Try...Catch, nos aseguramos de capturar las excepciones generadas en la validación o por cualquier error al grabar en la base de datos.
En este caso, tengo que aclarar que en lo personal trato de no llamar a los mappers directamente desde la capa de presentación, sino que prefiero hacerlo desde las reglas de negocio que se definen en el proyecto Rules de la solución ¿porqué? ahh bueno, pues porque ésto me ayuda mucho en los casos en los que se maneja otro motor de base de datos diferentes de SQL Server, donde por ejemplo en el caso de PostgreSQL se requiere que para algunas consultas se cree una transacción antes de realizar las operaciones que regresan "refcursors" y cuando se usa SQL Server ésto no es necesario, y al colocar toda esa lógica en la capa de Rules y no en la capa de presentación, el código del proyecto de presentación queda mucho más limpio, ya que a éste no le debe importar que motor de base de datos se esté usando y la responsabilidad de crear las transacciones (o no crearlas) queda a cargo de los métodos en los proyectos Data y Rules, que es donde se debe hacer todo eso.
Cabe mencionar, que esta forma de "arrojar" los errores de validación con excepciones no es muy de mi agrado (y creo que no soy el único que piensa de esta forma) ya que, al corregir un error en el objeto/entidad y volver a ejecutar la validación, posiblemente podría surgir otra excepción una y otra vez hasta que el usuario corrija uno por uno todos estos errores arrojados con excepciones y el objeto/entidad se encuentre completamente en un estado aceptable para grabarse en la base de datos. Derivado de ésto, se podría modificar el método de validación de los objetos/entidades para que en lugar de arrojar una excepción por cada propiedad validada y en estado erróneo, se concatenaran todos los mensajes de error "en una sola pasada" y se arrojara una sola excepción, con lo cual se lograría mostrar un solo mensaje al usuario con todos los errores en el objeto/entidad y se evitaría el que se muestre un mensaje por cada propiedad incorrecta, de esa manera el usuario podría corregír todos los errores antes de volver a intentar persistir el objeto/entidad en la base de datos, pero éste método tampoco es de mi total agrado ya que, si son muchos errores, seguramente el usuario no los recordará todos y es muy posible que tenga que intentar guardar de nuevo el objeto para ver si corrigió todos los errores o no.
Mi propuesta y de hecho, la manera en la que yo lo hago es un poco "más visual". En mi repositorio personal de Cooperator Framework he modificado la plantilla de generación Object.Auto.cs que se encuentra en la carpeta Templates/VisualBasicClasses (o CSharpClasses) del proyecto CooperatorModeler, para implementar las interfaces IDataErrorInfo e INotifyPropertyChanged, así como para añadir un par de métodos de ayuda para la interfaz IDataErrorInfo para agregar o remover los errores en las propiedades de los objetos, logrando con ésto poder utilizar un componente muy efectivo y visual que viene desde hace muchas versiones con Visual Studio para mostrar errores, el control ErrorProvider. Este control nos permite mostrar un ícono distintivo y llamativo para notificar al usuario de un error en una propiedad de un objeto enlazado a un control en un formulario (véase la imagen a continuación).
Como pueden ver en la imagen, los controles enlazados a las propiedades en estado erróneo muestran un pequeño ícono, el cual al ser señalado con el puntero del ratón, muestra un tooltip con la descripción del error. Ésto ayuda mucho al usuario a identificar qué errores hay aún en el objeto/entidad ya que si éstos son corregidos, el ícono desaparece.
Para lograr una implementación "correcta" de estas interfaces en la plantilla, necesitamos añadir lo que se muestra los siguientes 4 pasos a la plantilla:
1) Justo debajo de la declaración de la clase, añadir lo siguiente:
Implements IDataErrorInfo
Implements INotifyPropertyChanged
Por ejemplo:
- Public Partial Class <%Response.Write(currentEntity.GenerateAs);%>Object
- Inherits BaseObject
- Implements IMappeable<%Response.Write(currentEntity.GenerateAs);%>Object
- Implements IUniqueIdentifiable
- Implements IEquatable(Of <%Response.Write(currentEntity.GenerateAs);%>Object)
- Implements ICloneable
- Implements IDataErrorInfo
- Implements INotifyPropertyChanged
2) Se debe agregar la declaración de una variable Dictionary en la región de los campos:
- #Region "Fields"
- ...
- ...
- Protected _errors As New Dictionary(Of String, List(Of String))
- #End Region
3 ) Se agrega una línea para disparar el evento OnPropertyChanged al final en los "setter" de cada una de las propiedades:
OnPropertyChanged("<%Response.Write(currentProperty.GenerateAs);%>")
4) Y para finalizar se debe agregar el código de las implementaciones de las interfaces que les acabo de mencionar justo al final de la clase. Muestro el código a continuación:
- #Region " IDataErrorInfo Members "
- ''' <summary>
- ''' Returns an error description set for the current item
- ''' </summary>
- Public ReadOnly Property [Error]() As String Implements IDataErrorInfo.Error
- Get
- If _errors.Count > 0 Then
- Return String.Format("Los datos del objeto no son v{0}lidos.", ChrW(225))
- Else
- Return Nothing
- End If
- End Get
- End Property
- ''' <summary>
- ''' Returns an error description set for the item's property
- ''' </summary>
- ''' <param name="propertyName">The name of the property that has one or more error strings.</param>
- Public ReadOnly Property Item(ByVal propertyName As String) As String Implements IDataErrorInfo.Item
- Get
- If _errors.Count > 0 AndAlso _errors.ContainsKey(propertyName) Then
- Return String.Join(Environment.NewLine, _errors(propertyName).ToArray())
- Else
- Return String.Empty
- End If
- End Get
- End Property
- #End Region 'IDataErrorInfo Members
- #Region " INotifyPropertyChanged Members "
- ''' <summary>
- ''' Event to indicate that a property has changed.
- ''' </summary>
- Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
- ''' <summary>
- ''' Called when a property is changed
- ''' </summary>
- ''' <param name="propertyName">The name of the property that has changed.</param>
- Private Sub OnPropertyChanged(ByVal propertyName As String)
- ' Execute validate if applicable
- Dim validableObject As IValidable = TryCast(Me, IValidable)
- If (validableObject IsNot Nothing) Then
- validableObject.Validate()
- End If
- RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
- End Sub
- #End Region
- #Region " IDataErrorInfo Helper Procedures "
- ''' <summary>
- ''' Adds the specified error to the errors collection if it is not already present.
- ''' </summary>
- ''' <param name="propertyName">The name of the property that has the error.</param>
- ''' <param name="error">The error string on the specified property.</param>
- Public Sub AddError(ByVal propertyName As String, ByVal [error] As String)
- If Not _errors.ContainsKey(propertyName) Then
- _errors(propertyName) = New List(Of String)
- End If
- If Not _errors(propertyName).Contains([error]) Then
- _errors(propertyName).Add([error])
- End If
- End Sub
- ''' <summary>
- ''' Removes the specified error from the errors collection if it is present.
- ''' </summary>
- ''' <param name="propertyName">The name of the property that has the error.</param>
- ''' <param name="error">The error string to remove.</param>
- Public Sub RemoveError(ByVal propertyName As String, ByVal [error] As String)
- If _errors.ContainsKey(propertyName) AndAlso _errors(propertyName).Contains([error]) Then
- _errors(propertyName).Remove([error])
- If _errors(propertyName).Count = 0 Then
- _errors.Remove(propertyName)
- End If
- End If
- End Sub
- #End Region 'IDataErrorInfo Helper Procedures
Con todo esto en la plantilla, las clases generadas con el Modeler estarán dotadas con la capacidad de comunicarse con el control ErrorProvider para mostrar los errores. Pero ahora, falta un último detalle, hay que cambiar la manera de notificar el estado erróneo de las propiedades de nuestros objetos/entidades y ésto se hace mediante el uso de los nuevos métodos AddError y RemoveError en el método Validate de nuestros objetos/entidades como se muestra a continuación:
- <Serializable()> _
- Partial Public Class banco
- Implements IValidable
- ''' <summary>
- ''' This method is invoked by Gateway before Insert or Update to validate Object.
- ''' </summary>
- ''' <remarks></remarks>
- Public Sub Validate() Implements IValidable.Validate
- Dim msgNumberRequired As String = "{0} debe contener un valor."
- Dim msgRequired As String = "{0} no puede quedar vací{1}."
- Dim msgTooLarge As String = "{0} no puede exceder de {1} caracteres."
- If (Me.idbanco <= 0) Then
- AddError(bancoColumn.idbanco.ToString("G"), String.Format(msgNumberRequired, "El código"))
- Else
- RemoveError(bancoColumn.idbanco.ToString("G"), String.Format(msgNumberRequired, "El código"))
- End If
- If (String.IsNullOrEmpty(Me.nombre)) Then
- AddError(bancoColumn.nombre.ToString("G"), String.Format(msgRequired, "El nombre", "o"))
- Else
- RemoveError(bancoColumn.nombre.ToString("G"), String.Format(msgRequired, "El nombre", "o"))
- If (Me.nombre.Length > 100) Then
- AddError(bancoColumn.nombre.ToString("G"), String.Format(msgTooLarge, "El nombre", 100))
- Else
- RemoveError(bancoColumn.nombre.ToString("G"), String.Format(msgTooLarge, "El nombre", 100))
- End If
- End If
- If Not String.IsNullOrEmpty(Me.nombrecorto) AndAlso Me.nombrecorto.Length > 20 Then
- AddError(bancoColumn.nombrecorto.ToString("G"), String.Format(msgTooLarge, "El nombre corto", 20))
- Else
- RemoveError(bancoColumn.nombrecorto.ToString("G"), String.Format(msgTooLarge, "El nombre corto", 20))
- End If
- End Sub
- End Class
Con ésto ya queda todo listo para mostrar los errores de validación de una nueva forma más visual y creo que menos molesta en WinForms. Además, pueden notar el uso de las nuevas enumeraciones que se generan, refiriéndose a los nombres de los campos en la tabla relacionada con cada objeto/entidad, evitando así errores que se puedan producir en tiempo de ejecución cuando se cambia o elimina una columna en la tabla relacionada en la base de datos, ya que ésto se podrá detectar al compilar el programa. Estas enumeraciones son también muy útiles cuando se usa databinding para enlazar objetos/entidades generadas con Cooperator Framework a controles en WinForms o WPF y carga dinámica de columnas en DataGridViews, como se puede ver en esta otra entrada del blog.
También se debe notar que para cada llamado al método AddError debe haber también un llamado al método RemoveError. Esto es así porque si se agrega un mensaje de error asociado con una determinada propiedad del objeto/entidad usando el método AddError, debe haber un llamado al método RemoveError para que se encargue de remover dicho mensaje de error cuando el problema esté arreglado.
Ahora bien, para asociar el componente ErrorProvider con nuestros objetos/entidades, podemos hacer uso del componente BindingSource y facilitarnos la vida, ya que éste se encarga de administrar la actualización de cambios entre los controles y los objetos/entidades enlazados a dichos controles de manera transparente. Para ver cómo enlazar nuestros objetos/entidades creados con el Modeler de Cooperator Framework y el componente BindingSource así como la manera de hacer databinding, pueden ver esta estrada.
Para enlazar el componente ErrorProvider con nuestros objetos/entidades utilizando como intermediario el componente BindingSource, podemos hacer algo como lo siguiente:
- Private Sub BindControls()
- ' Se enlaza el ErrorProvider con el BindingSource para obtener los errores en la entidad subyacente.
- epBanco.DataSource = bsBanco ' Se enlaza el ErrorProvider al BindingSource
- bsBanco.DataSource = oBanco ' Se enlaza el BindingSource con la entidad Banco
- ' Se enlazan los controles con la entidad Banco por medio del BindingSource
- txtId.DataBindings.Add("Text", bsBanco, bancoColumn.idbanco.ToString("G"), True)
- txtNombre.DataBindings.Add("Text", bsBanco, bancoColumn.nombre.ToString("G"), True)
- txtNombreCorto.DataBindings.Add("Text", bsBanco, bancoColumn.nombrecorto.ToString("G"), True)
- chkStatus.DataBindings.Add("Checked", bsBanco, bancoColumn.status.ToString("G"), True)
- End Sub
Con lo que se hace en estas cuantas líneas es suficiente para que los errores se muestren de una manera mas visual en nuestra interfaz de usuario sin necesidad de generar excepciones en el método validate de los objetos/entidades generados con Cooperator, ya que el mecanismo de databinding dispara la validación de estos objetos/entidades al hacerse cualquier modificación de los mismos mediantes los controles en la interfaz de usuario.
En el caso en el que se haga un llamado explícito al método Validate() de los objetos/entidades como se hace en el código mostrado anteriormente para el botón btnGuardar, se debe hacer también un llamado explícito al método UpdateBinding() del componente ErrorProvider para que se lea nuevamente la propiedad Error del objeto/entidad y cada una de las propiedades enlazadas a los controles. Por ejemplo:
- Private Sub btnGuardar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGuardar.Click
- Try
- ' Si un objeto no se modifica y se intenta guardar hay que validarlo de todas formas.
- BancoRules.Validate(oBanco)
- epBanco.UpdateBinding()
- ' Si hay errores en el objeto...
- If (Not String.IsNullOrEmpty(oBanco.Error)) Then
- Exit Sub ' Salimos del procedimiento
- End If
- ' Se muestra el cursor de espera mientras se graba la información en la BD.
- Me.Cursor = Cursors.WaitCursor
- Try
- ' Si es un objeto nuevo...
- If ObjectStateHelper.IsNew(oBanco) Then
- BancoRules.InsertBanco(oBanco) ' Se guarda (inserta) el objeto
- Else
- BancoRules.SaveBanco(oBanco) ' Se guarda (actualiza) el objeto
- Me.Close() ' Se cierra el formulario en caso de Update.
- End If
- Catch ex As Exception
- ...
- ...
En la línea 4 se hace el llamado explícito al método Validate de la clase compartida en el proyecto Rules, la cual a su vez llama al método Validate del objeto Banco que se recibe como parámetro. Inmediatamente después se hace el llamado explícito al método UpdateBinding del componente ErrorProvider para que actualice los mensajes de error. También se puede ver en la línea 8 el uso de la nueva propiedad Error que se implementa en el código proporcionado anteriormente para verificar si existen errores en el objeto.
Eso es todo lo que tengo ahora que contar sobre la validación de objetos/entidades generados con el Modeler de Cooperator Framework. ya cada quien decide que método se le facilita más o le parece más conveniente y profesional para sus aplicaciones.
Para finalizar, dejo a continuación un archivo comprimido con las plantillas modificadas (para VB y C#) para que reemplacen las que tienen ahorita si es que les interesa este nuevo modo de mostrar los errores en los objetos cuando éstos se validan.
Plantillas
Hasta pronto.