domingo, 21 de septiembre de 2014 0 comentarios

Forzando el Lazy Load de Cooperator Framework

El patrón llamado “Lazy Load”  o “Carga diferida” (porque traducido literalmente al español sería “carga perezosa” y no me gusta como se escucha) es una técnica usada para retrasar la carga de un objeto anidado en otro hasta el momento en que éste es necesario, es decir – no se moleste señor, si lo necesito, sé como comunicarme con usted, así que yo lo llamo después – y es un gran placer saber y comunicar que los objetos/entidades creados mediante Cooperator Framework cuentan con la implementación de este patrón de diseño.

Este patrón se puede implementar de varias formas y Cooperator Framework utiliza una de ellas llamada “Lazy Initialization” combinada con otros patrones de diseño como “factory”, “factory method pattern” y “multiton pattern”. Este patrón de inicialización diferida (lazy initialization)  se basa comúnmente en el uso de una bandera que indica si el objeto anidado ha sido cargado con anterioridad y Cooperator simula esta bandera verificando si el objeto a cargar es Nulo o no.

Como todo en la vida, usar algo con responsabilidad y cautela puede ayudar mucho y abusar de ese algo puede causar grandes problemas y todo eso se explica en los videos que tratan el tema de la impedancia (4 y 5) así como en el documento de referencia del Framework y el Modeler, donde se ve como activar esta característica del lazy load en los objetos/entidades y también en las listas anidadas mediante el Modeler, por lo que en esta entrada del blog me centraré en el uso de esta implementación para resolver un problema que para mí digamos, es poco común.

Antes de mostrar el código, he de decir que recientemente encontré una forma al parecer más correcta de solucionar el problema y evitar el uso de la carga diferida para resolverlo, pero de todos modos voy a continuar con este tema porque tal vez a alguien le sirva de ayuda en algún momento, así que primeramente voy a hacer una breve descripción del problema para que se entienda mejor el código y el porqué tuve que resolverlo utilizando lazy load y antes de finalizar la entrada mostraré el código con la solución que definitivamente creo más correcta y conveniente.

Resulta que hace un tiempo se me presentó un pequeño problema que, aunque sé como resolverlo, no me había tocado hacerlo usando Cooperator Framework y tenía que ver con la longitud de algo que en México se hace llamar “Registro Federal de Contribuyentes” o más conocido por sus siglas como RFC. El RFC es un código de identificación tributaria equivalente al CUIT en Argentina, al NIF en España, al RUC de Perú y Paraguay, al RUT  de Uruguay, al NIT que se usa en varios países de centroamérica o al SSN en Estados Unidos. Y pues bien, este dichoso RFC puede tener una longitud no menor de 12 caracteres y no mayor de 13 y esta variación de longitud se debe a otra cosa llamada “Clasificación Fiscal” (según el SAT o Servicio de Administración Tributaria). Esta clasificación fiscal es la manera en que el SAT distingue a los contribuyentes que son “Personas Físicas de otro tipo de contribuyentes a los que llaman “Personas Morales”, de tal modo que, los RFC de las personas físicas tienen una longitud de 13 caracteres y los RFC de las personas morales tienen una longitud de 12 caracteres.

Pues bien,  tengo en una aplicación una entidad Proveedor la cual puede o no estar almacenada en la base de datos con un RFC, ya que muchas veces por hacer el proceso más rápido, se puede omitir su RFC al darlo de alta en el sistema y agregarlo posteriormente pero, de tener RFC, éste debe cumplir con el requisito de la longitud. La aplicación no valida la formación del RFC, solo su longitud y por tal motivo, la validación de esta característica en la entidad Proveedor se hace utilizando su clasificación fiscal.

Para solucionar el detalle de la longitud del RFC, se modeló la entidad Proveedor con un objeto anidado llamado ClasificacionFiscalEntity que por default se carga con este modo “lazy load” al igual que todos los objetos / entidades (xxxEntity) anidados que son agregados por asociación (véase el documento de Referencia del Framework páginas 15,16 y 18) pero primero, se crean mediante el constructor de la clase principal (en este caso Proveedor) de la siguiente forma:

  1. Public Partial Class Proveedor
  2.     Inherits Objects.ProveedorObject
  3.     Implements IMappeableProveedor
  4.     Implements IEquatable(Of Proveedor)
  5.     Implements ICloneable
  6.  
  7.     #Region "Ctor"
  8.  
  9.     Public Sub New()
  10.         MyBase.New()
  11.         If _ClasificacionFiscalEntity Is Nothing Then _ClasificacionFiscalEntity = New Objects.ClasificacionFiscalObject()
  12.         If _CondicionDePagoEntity Is Nothing Then _CondicionDePagoEntity = New Objects.CondicionDePagoObject()
  13.         If _DatosDeContactoEntity Is Nothing Then _DatosDeContactoEntity = New Objects.DatosDeContactoObject()
  14.         If _DomicilioEntity Is Nothing Then _DomicilioEntity = New Objects.DomicilioObject()
  15.         If _MonedaEntity Is Nothing Then _MonedaEntity = New Objects.MonedaObject()
  16.         If _TipoDeAutorizacionEntity Is Nothing Then _TipoDeAutorizacionEntity = New Objects.TipoDeAutorizacionObject()
  17.         If _TipoDeOperacionEntity Is Nothing Then _TipoDeOperacionEntity = New Objects.TipoDeOperacionObject()
  18.         If _TipoDeProveedorEntity Is Nothing Then _TipoDeProveedorEntity = New Objects.TipoDeProveedorObject()
  19.  
  20.     End Sub

En el fragmento de código anterior se puede ver que al crear la nueva entidad, se revisa si los objetos/entidades anidados han sido instanciados y en caso de no ser así, se crean mediante su constructor.

Ahora bien, todo esto funciona bien normalmente, pero en mi caso existe el problema de que la propiedad EsPersonaMoral de ClasificacionFiscalEntity es de tipo Boolean y por lo tanto se inicializa en Falso y es utilizando el valor de esta propiedad que se hace la validación. Por lo que si tengo un código como el siguiente:

  1. If (Me.RFC.Trim().Length <> 12 And Me.ClasificacionFiscalEntity.EsPersonaMoral) Then
  2.    AddError(ProveedorColumn.RFC.ToString("G"), String.Format(msgWrongLenght, "El R.F.C.", 12))
  3. Else
  4.    RemoveError(ProveedorColumn.RFC.ToString("G"), String.Format(msgWrongLenght, "El R.F.C.", 12))
  5. End If
  6.  
  7. If (Me.RFC.Trim().Length <> 13 And (Not Me.ClasificacionFiscalEntity.EsPersonaMoral)) Then
  8.     AddError(ProveedorColumn.RFC.ToString("G"), String.Format(msgWrongLenght, "El R.F.C.", 13))
  9. Else
  10.     RemoveError(ProveedorColumn.RFC.ToString("G"), String.Format(msgWrongLenght, "El R.F.C.", 13))
  11. End If

nunca validará correctamente la longitud de los RFC’s de las personas morales, porque la propiedad EsPersonaMoral siempre tendrá el valor de Falso.

Entonces, para resolver el problema de inicialización de esta propiedad se hace algo que yo llamo “EggedLoad” (o sea “Carga de a Huevo”); bueno no, ya en serio le llamo “Forced LazyLoad” ya que literalmente estoy forzando a que se cargue la entidad ClasificacionFiscal utilizando los métodos compartidos del mecanismo de LazyLoad. Y esto se muestra en el siguiente fragmento de código:

  1. ' Esto forza a que se recargue la propiedad ClasificacionFiscalEntity vía LazyLoad.
  2. ' El campo _ClasificacionFiscalEntity está declarado en Proveedor.Auto.vb
  3. Dim lazyProvider As LazyLoad.ILazyProvider = LazyLoad.LazyProviderFactory.Get(GetType(Objects.ClasificacionFiscalObject))
  4. _ClasificacionFiscalEntity = CType(lazyProvider.GetEntity(GetType(Objects.ClasificacionFiscalObject), _
  5.                                                           New Objects.ClasificacionFiscalObject(Me.IdClasificacionFiscal)),  _
  6.                                    Objects.ClasificacionFiscalObject)
  7.  
  8. If (Me.RFC.Trim().Length <> 12 And Me.ClasificacionFiscalEntity.EsPersonaMoral) Then
  9.    . . .

Con este par de líneas de código (divididas en varias para que no queden tan largas) se logra forzar la recarga de una entidad mediante el mecanismo de lazyload. Lo que sigue a esta recarga, son las validaciones que hacen referencia a la propiedad EsPersonaMoral la cual tendrá ahora el valor correcto.

Ahora bien, la desventaja que tiene hacer esta carga forzada, es que cada vez que se ejecute el método Validate del objeto/entidad se hará una consulta a la base de datos, lo cual podría afectar el rendimiento de la aplicación si se utiliza la técnica del databinding ya que, el método Validate se ejecuta prácticamente cada vez que se cambia el foco de entrada de un control a otro y si a eso se agregan los llamados explícitos a dicho método…pues ya sabrán.

La solución que encontré para evadir estas continuas consultas a la base de datos fue implementar el controlador del evento SelectedIndexChanged del combobox al que tengo enlazada la lista de clasificaciones fiscales haciendo lo siguiente:

  1. Private Sub cboClasificacionFiscal_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles cboClasificacionFiscal.SelectedIndexChanged
  2.     If bolLoaded Then
  3.         oProveedor.ClasificacionFiscalEntity = CType(cboClasificacionFiscal.SelectedItem, ClasificacionFiscalObject)
  4.  
  5.     End If
  6.  
  7. End Sub

En el fragmento de código anterior se puede ver que cada vez que se cambia el elemento del combobox, se asigna directamente el objeto/entidad asociado con este control a la propiedad ClasificacionFiscalEntity de la entidad Proveedor. La bandera “bolLoaded” se utiliza para que el código de este método no se ejecute durante la carga del control, sino solamente hasta que todo el formulario está cargado.

Con esta sencilla implementación se ahorra mucho tiempo comparado con la técnica del “EggLoad”, porque no hay consultas a la base de datos y además la entidad asociada (ClasificacionFiscalEntity) con el objeto principal (oProveedor) siempre tiene el objeto correcto con sus propiedades cargadas también correctamente.

Y eso es todo, en realidad es mucho rollo para mostrar un par de líneas que son las que importan en esta entrada, pero la explicación del problema era necesaria. Espero que algún día esta entrada le ayude a alguien que use Cooperator Framework y necesite forzar la carga de un objeto/entidad con él mecanismo de lazy load.

domingo, 24 de agosto de 2014 2 comentarios

Validando Objetos y Entidades con Cooperator Framework

Una de las primeras dudas que tuve cuando comencé a usar Cooperator Framework fué: ¿Cómo se validan los objetos y entidades? porque no importa que tipo de aplicación estemos desarrollando, siempre será necesario e importante validar la información que el usuario introduce en la aplicación, cosa que regularmente nos causa grandes dolores de cabeza. Así fué que gracias a Carlos Marcelo Santos supe que Cooperator Framework nos permite validar objetos y entidades mediante la implementación de la interface IValidable en dichos objetos/entidades generados con el Modeler.

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:

Code Snippet
  1. ''' <summary>
  2. ''' This class represents the Banco entity.
  3. ''' </summary>
  4. ''' <remarks></remarks>
  5. <Serializable> _
  6. Public Partial Class Banco
  7. ' Implements IValidable
  8.  
  9.     ' ''' <summary>
  10.     ' ''' When IValidable is implemented, this method is invoked by Gateway before Insert or Update to validate Object.
  11.     ' ''' </summary>
  12.     ' ''' <remarks></remarks>
  13.     ' Public Sub Validate() Implements IValidable.Validate
  14.     '    'Example:
  15.     '    If String.IsNullOrEmpty(me.IdBanco) Then Throw New RuleValidationException("IdBanco can't be null")
  16.     ' End Sub
  17. 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:


Code Snippet
  1. ''' <summary>
  2. ''' This class represents the Banco entity.
  3. ''' </summary>
  4. ''' <remarks></remarks>
  5. <Serializable> _
  6. Public Partial Class Banco
  7.     Implements IValidable
  8.  
  9.     ''' <summary>
  10.     ''' When IValidable is implemented, this method is invoked by Gateway before Insert or Update to validate Object.
  11.     ''' </summary>
  12.     ''' <remarks></remarks>
  13.     Public Sub Validate() Implements IValidable.Validate
  14.         'Example:
  15.         If String.IsNullOrEmpty(Me.IdBanco) Then Throw New RuleValidationException("IdBanco can't be null")
  16.     End Sub
  17. 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:


Code Snippet
  1. Private Sub btnGuardar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGuardar.Click
  2.     Try
  3.         ' Si un objeto no se modifica y se intenta guardar hay que validarlo de todas formas.
  4.         BancoRules.Validate(oBanco)
  5.  
  6.         ' Se muestra el cursor de espera mientras se graba la información en la BD.
  7.         Me.Cursor = Cursors.WaitCursor
  8.  
  9.         Try
  10.             ' Si es un objeto nuevo...
  11.             If ObjectStateHelper.IsNew(oBanco) Then
  12.                 BancoRules.InsertBanco(oBanco)     ' Se guarda (inserta) el objeto
  13.                 oBanco = New Banco                 ' Se crea un nuevo objeto.
  14.  
  15.             Else
  16.                 BancoRules.SaveBanco(oBanco)       ' Se guarda (actualiza) el objeto
  17.                 Me.Close()                         ' Se cierra el formulario en caso de Update.
  18.  
  19.             End If
  20.  
  21.         Catch ex As Exception
  22.             ' Se restaura el cursor de ratón
  23.             Me.Cursor = Cursors.Default
  24.  
  25.             ' Si se captura una excepción, se muestran sus detalles.
  26.             MessageBox.Show(String.Format("Detalles del error:{0}{1}", Environment.NewLine, ex.Message), _
  27.                             "Ocurrieron errores al intentar guardar la información", _
  28.                             MessageBoxButtons.OK, MessageBoxIcon.Warning)
  29.  
  30.             ' Si se estaban modificando los datos de un objeto, se cierra la ventana.
  31.             If EditMode = FormEditModeEnum.Updating Then
  32.                 Me.Close()
  33.  
  34.             End If
  35.  
  36.         Finally
  37.             ' Se muestra el cursor por defecto.
  38.             Me.Cursor = Cursors.Default
  39.  
  40.         End Try
  41.  
  42.     Catch ex As Exception
  43.         ' Si se captura una excepción, se muestran sus detalles.
  44.         MessageBox.Show(String.Format("Detalles del error:{0}{1}", Environment.NewLine, ex.Message), _
  45.                         "Ocurrieron errores al validar la información", _
  46.                         MessageBoxButtons.OK, MessageBoxIcon.Warning)
  47.  
  48.     End Try
  49.  
  50. 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:

Code Snippet
  1.     Public Partial Class <%Response.Write(currentEntity.GenerateAs);%>Object
  2.         Inherits BaseObject
  3.         Implements IMappeable<%Response.Write(currentEntity.GenerateAs);%>Object
  4.         Implements IUniqueIdentifiable
  5.         Implements IEquatable(Of <%Response.Write(currentEntity.GenerateAs);%>Object)
  6.         Implements ICloneable
    1.        Implements IDataErrorInfo
    2.        Implements INotifyPropertyChanged


2) Se debe agregar la declaración de una variable Dictionary en la región de los campos:


  1.  #Region "Fields"
  2.          ...
  3.          ...
    1.         Protected _errors As New Dictionary(Of String, List(Of String))
    2. #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:

IDataErrorInfo
  1. #Region " IDataErrorInfo Members "
  2.     ''' <summary>
  3.     ''' Returns an error description set for the current item
  4.     ''' </summary>
  5.     Public ReadOnly Property [Error]() As String Implements IDataErrorInfo.Error
  6.         Get
  7.             If _errors.Count > 0 Then
  8.                 Return String.Format("Los datos del objeto no son v{0}lidos.", ChrW(225))
  9.             Else
  10.                 Return Nothing
  11.             End If
  12.         End Get
  13.     End Property
  14.  
  15.     ''' <summary>
  16.     ''' Returns an error description set for the item's property
  17.     ''' </summary>
  18.     ''' <param name="propertyName">The name of the property that has one or more error strings.</param>
  19.     Public ReadOnly Property Item(ByVal propertyName As String) As String Implements IDataErrorInfo.Item
  20.         Get
  21.             If _errors.Count > 0 AndAlso _errors.ContainsKey(propertyName) Then
  22.                 Return String.Join(Environment.NewLine, _errors(propertyName).ToArray())
  23.             Else
  24.                 Return String.Empty
  25.             End If
  26.         End Get
  27.     End Property
  28. #End Region  'IDataErrorInfo Members


INotifyPropertyChanged
  1. #Region " INotifyPropertyChanged Members "
  2.     ''' <summary>
  3.     ''' Event to indicate that a property has changed.
  4.     ''' </summary>
  5.     Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
  6.  
  7.     ''' <summary>
  8.     ''' Called when a property is changed
  9.     ''' </summary>
  10.     ''' <param name="propertyName">The name of the property that has changed.</param>
  11.     Private Sub OnPropertyChanged(ByVal propertyName As String)
  12.         ' Execute validate if applicable
  13.         Dim validableObject As IValidable = TryCast(Me, IValidable)
  14.         If (validableObject IsNot Nothing) Then
  15.             validableObject.Validate()
  16.         End If
  17.  
  18.         RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
  19.    End Sub 
  20.  
  21. #End Region

IDataErrorInfo Helper Procedures
  1. #Region " IDataErrorInfo Helper Procedures "
  2.     ''' <summary>
  3.     ''' Adds the specified error to the errors collection if it is not already present.
  4.     ''' </summary>
  5.     ''' <param name="propertyName">The name of the property that has the error.</param>
  6.     ''' <param name="error">The error string on the specified property.</param>
  7.     Public Sub AddError(ByVal propertyName As String, ByVal [error] As String)
  8.         If Not _errors.ContainsKey(propertyName) Then
  9.             _errors(propertyName) = New List(Of String)
  10.         End If
  11.  
  12.         If Not _errors(propertyName).Contains([error]) Then
  13.             _errors(propertyName).Add([error])
  14.         End If
  15.     End Sub
  16.  
  17.     ''' <summary>
  18.     ''' Removes the specified error from the errors collection if it is present.
  19.     ''' </summary>
  20.     ''' <param name="propertyName">The name of the property that has the error.</param>
  21.     ''' <param name="error">The error string to remove.</param>
  22.     Public Sub RemoveError(ByVal propertyName As String, ByVal [error] As String)
  23.         If _errors.ContainsKey(propertyName) AndAlso _errors(propertyName).Contains([error]) Then
  24.             _errors(propertyName).Remove([error])
  25.  
  26.             If _errors(propertyName).Count = 0 Then
  27.                 _errors.Remove(propertyName)
  28.             End If
  29.         End If
  30.     End Sub
  31.  
  32. #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:

Code Snippet
  1. <Serializable()> _
  2. Partial Public Class banco
  3.     Implements IValidable
  4.  
  5.     ''' <summary>
  6.     ''' This method is invoked by Gateway before Insert or Update to validate Object.
  7.     ''' </summary>
  8.     ''' <remarks></remarks>
  9.     Public Sub Validate() Implements IValidable.Validate
  10.         Dim msgNumberRequired As String = "{0} debe contener un valor."
  11.         Dim msgRequired As String = "{0} no puede quedar vací{1}."
  12.         Dim msgTooLarge As String = "{0} no puede exceder de {1} caracteres."
  13.  
  14.         If (Me.idbanco <= 0) Then
  15.             AddError(bancoColumn.idbanco.ToString("G"), String.Format(msgNumberRequired, "El código"))
  16.         Else
  17.             RemoveError(bancoColumn.idbanco.ToString("G"), String.Format(msgNumberRequired, "El código"))
  18.         End If
  19.  
  20.         If (String.IsNullOrEmpty(Me.nombre)) Then
  21.             AddError(bancoColumn.nombre.ToString("G"), String.Format(msgRequired, "El nombre", "o"))
  22.         Else
  23.             RemoveError(bancoColumn.nombre.ToString("G"), String.Format(msgRequired, "El nombre", "o"))
  24.  
  25.             If (Me.nombre.Length > 100) Then
  26.                 AddError(bancoColumn.nombre.ToString("G"), String.Format(msgTooLarge, "El nombre", 100))
  27.             Else
  28.                 RemoveError(bancoColumn.nombre.ToString("G"), String.Format(msgTooLarge, "El nombre", 100))
  29.             End If
  30.  
  31.         End If
  32.  
  33.         If Not String.IsNullOrEmpty(Me.nombrecorto) AndAlso Me.nombrecorto.Length > 20 Then
  34.             AddError(bancoColumn.nombrecorto.ToString("G"), String.Format(msgTooLarge, "El nombre corto", 20))
  35.         Else
  36.             RemoveError(bancoColumn.nombrecorto.ToString("G"), String.Format(msgTooLarge, "El nombre corto", 20))
  37.         End If
  38.  
  39.     End Sub

  40. 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:

  1. Private Sub BindControls()
  2.     ' Se enlaza el ErrorProvider con el BindingSource para obtener los errores en la entidad subyacente.
  3.     epBanco.DataSource = bsBanco ' Se enlaza el ErrorProvider al BindingSource
  4.     bsBanco.DataSource = oBanco  ' Se enlaza el BindingSource con la entidad Banco
  5.  
  6.     ' Se enlazan los controles con la entidad Banco por medio del BindingSource
  7.     txtId.DataBindings.Add("Text", bsBanco, bancoColumn.idbanco.ToString("G"), True)
  8.     txtNombre.DataBindings.Add("Text", bsBanco, bancoColumn.nombre.ToString("G"), True)
  9.     txtNombreCorto.DataBindings.Add("Text", bsBanco, bancoColumn.nombrecorto.ToString("G"), True)
  10.     chkStatus.DataBindings.Add("Checked", bsBanco, bancoColumn.status.ToString("G"), True)
  11.  
  12. 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:

  1. Private Sub btnGuardar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnGuardar.Click
  2.     Try
  3.         ' Si un objeto no se modifica y se intenta guardar hay que validarlo de todas formas.
  4.         BancoRules.Validate(oBanco)
  5.         epBanco.UpdateBinding()
  6.  
  7.         ' Si hay errores en el objeto...
  8.         If (Not String.IsNullOrEmpty(oBanco.Error)) Then
  9.             Exit Sub  ' Salimos del procedimiento
  10.         End If
  11.  
  12.         ' Se muestra el cursor de espera mientras se graba la información en la BD.
  13.         Me.Cursor = Cursors.WaitCursor
  14.  
  15.         Try
  16.             ' Si es un objeto nuevo...
  17.             If ObjectStateHelper.IsNew(oBanco) Then
  18.                 BancoRules.InsertBanco(oBanco) ' Se guarda (inserta) el objeto
  19.             Else
  20.                 BancoRules.SaveBanco(oBanco)   ' Se guarda (actualiza) el objeto
  21.                 Me.Close()                     ' Se cierra el formulario en caso de Update.
  22.  
  23.             End If
  24.  
  25.         Catch ex As Exception
  26.             ...
  27.             ...

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.

domingo, 17 de agosto de 2014 0 comentarios

DataBinding & Cooperator Framework

El enlace a datos o  mejor conocido por su nombre en inglés Databinding, es una técnica que sirve para asociar y sincronizar objetos de negocios con elementos visuales (controles) en la interfaz de usuario para mostrar u obtener datos. Este mecanismo de enlace está disponible en la mayoría de los controles de Windows Forms, Windows Presentation Fundation (WPF) y WebForms ya que, todos heredan de la clase Controls, que implementa la interfaz IBindableComponent la cual nos permite que los controles se asocien a un Contexto de Enlace (Binding Context) que se crea por defecto en los formularios Windows Forms.

El enlace a datos ha estado presente desde hace mucho tiempo, pero desde la aparición de la versión 2.0 de .Net Framework ha sido mucho más fácil de usar gracias a la incorporación del componente BindingSource. Gracias a éste es más fácil enlazar controles a orígenes de datos y administrar la concurrencia y la notificación de cambios entre estos controles y los orígenes de datos, simplificando significativamente el desarrollo de aplicaciones que tengan que recuperar o persistir datos en algún tipo de base de datos.

Ahora bien, antes de comenzar a escribir sobre como hacer databinding usando los objetos/entidades que se generan con Cooperator Framework, voy a hablar un poco sobre algunos conceptos de databinding.

El enlace a datos o databinding puede hacerse de dos formas:

  • Enlace a datos sencillo (Simple Databinding), y
  • Enlace a datos complejo (Complex Databinding)

El enlace a datos sencillo es cuando se enlaza una propiedad de un mismo objeto u origen de datos a una propiedad de un control. La gran mayoría de los controles emplean este tipo de enlace utilizando la propiedad DataBindings y entre ellos, podemos mencionar controles como el Textbox, Label, Checkbox, Radiobutton, DateTimePicker, NumericUpDown, y Picturebox, por mencionar algunos.

El enlace a datos complejo, es el que nos permite enlazar una lista de objetos a un control para mostrar una o más propiedades de dichos objetos.  La lista de controles con soporte para este tipo de databinding no tiene tanta variedad como el enlace simple, pero podemos mencionar los más importantes que son, el DataGridView, ListView, ListBox y ComboBox. Estos controles permiten el enlace a datos a través de sus propiedades DataSource y DataMember. Algunos de estos controles también necesitan las propiedades DisplayMember y ValueMember como es el caso del ListBox y el ComboBox.

Ahora bien, para que un objeto/entidad pueda servir como origen de datos y ser utilizado en el enlace a datos, éste necesita implementar al menos una de las interfaces estándar relacionadas con el enlace a datos, como lo son IList, Typed IList, IComponent, IListSource, ITypedList, IBindingList, IBindingListView,  IEditableObject e IDataErrorInfo. Ejemplos de este tipo de objetos con la implementación de una o más de estas interfaces son el Dataset, Datatable, DataView, DataViewManager, DataColumn, Array y por supuesto, los objetos /entidades generados con Cooperator Framework ya que éstos implementan la interfaz IEditableObject y sus listas de objetos heredan de la clase genérica List(Of T) que a su vez implementa la interfaz IList, por lo que soportan perfectamente el databinding. Además, las ListViews implementan IBindindListView que sirve para ordenar y filtrar los elementos de la listas.

En fin, hablar del enlace a datos con más profundidad me tomaría muchas líneas en el blog ya que, tendría que hablar de muchísimas cosas como DataProviders, DataConsumers, objetos Binding, BindingContext, CurrencyManager, PropertyManager y además, hablar de para qué sirve implementar tal o cual interface relacionada con el databinding. Por tal motivo dejo este enlace que a mí me resultó bastante clarificador para entender mejor todo esto que acabo de mencionar. Está en inglés, así que si no saben inglés o les da güeva usar un traductor online ¡pues se chingaron! porque no me voy a poner a traducir ese texto nomás para solapar su flojera.

Ahora sí. Esperando que ya entiendan lo que es y con qué se come el databinding, voy a comenzar a escribir sobre como hacer databinding usando los objetos/entidades generados con el Modeler que nos proporciona Cooperator Framework.

Primero voy a hablar sobre cómo realizar el enlace a datos sencillo. Para esto utilizaré una entidad de una de mis aplicaciones (que en realidad es más compleja, por eso es una entidad, pero para facilitar la lectura del código la simplifiqué) con la que le pongo el nombre a un banco y el formulario es como el siguiente:

Edicion de Banco

Y el código de carga de éste formulario es el siguiente:

  1. Public Class frmEdicionDeBanco
  2.     Dim oBanco As Banco
  3.  
  4.     Private Sub frmEdicionDeBanco_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
  5.         ' Si no se asignó un objeto...
  6.         If oBanco Is Nothing Then
  7.             ' Se quiere agregar un nuevo objeto, por lo tanto hay que crearlo así como sus objetos relacionados.
  8.             oBanco = New Banco
  9.             ' Cuando se trata de un objeto nuevo, se inicializa su Status a verdadero (Activo).
  10.             oBanco.Status = True
  11.  
  12.             Try
  13.                 ' Se obtiene un nuevo código consecutivo para cada objeto nuevo.
  14.                 oBanco.idbanco = BancoRules.GetNextID()
  15.  
  16.             Catch ex As Exception
  17.                 MessageBox.Show("Ocurrió un error al intentar obtener el número consecutivo para el nuevo banco.", _
  18.                                 "Error al buscar consecutivo", MessageBoxButtons.OK, MessageBoxIcon.Information)
  19.  
  20.             End Try
  21.  
  22.             Me.Text = "Agregar Banco"
  23.  
  24.         Else
  25.             ' De lo contrario, se prepara el formulario para editar un objeto existente.
  26.  
  27.             Me.Text = "Modificar Banco"
  28.             Me.txtId.ReadOnly = True
  29.  
  30.         End If
  31.  
  32.         ' Se enlazan controles a los BindingSources.
  33.         BindControls()
  34.  
  35.     End Sub
  36.     Private Sub BindControls()
  37.         ' Se enlaza el BindingSource
  38.         bsBanco.DataSource = oBanco
  39.  
  40.         ' Se enlazan los datos generales
  41.         txtId.DataBindings.Add("Text", bsBanco, bancoColumn.idbanco.ToString("G"), True)
  42.         txtNombre.DataBindings.Add("Text", bsBanco, bancoColumn.nombre.ToString("G"), True)
  43.         txtNombreCorto.DataBindings.Add("Text", bsBanco, bancoColumn.nombrecorto.ToString("G"), True)
  44.         chkStatus.DataBindings.Add("Checked", bsBanco, bancoColumn.status.ToString("G"), True)
  45.  
  46.     End Sub

Como se puede ver, al cargarse el formulario se revisa si existe alguna instancia de la entidad Banco para configurar dicha entidad y también para saber qué cadena mostrar como título del formulario, además, al final se hace lo que más nos interesa de todo el código, el llamado al procedimiento BindControls.

En el procedimiento BindControls se enlaza la entidad Banco al componente BindingSource (bsBanco), el cual no se ve en la imagen de arriba porque solamente se muestra en la bandeja de componentes del IDE de Visual Studio y es invisible en tiempo de ejecución porque no tiene interfaz de usuario. Al enlazarse, el componente BindingSource se encargará de gestionar el enlace, sincronización y notificación de cambios entre la entidad Banco y los controles y viceversa. En las siguientes líneas se hace el databinding entre las propiedades de la entidad Banco y los controles en el formulario mediante la propiedad DataBindings de cada control (como se menciona en el párrafo donde se explica que es el enlace a datos sencillo de esta entrada). El método Add() tiene muchas sobrecargas pero yo regularmente uso la que pide 4 argumentos:

  1. Cadena con el nombre de la propiedad del control a la que se enlazará la propiedad del objeto/entidad.
  2. Origen de datos u objeto/entidad que actúa como fuente de datos. En este caso se usa la instancia del componente BindingSource ya que previamente se enlazó con la entidad Banco.
  3. Cadena con el nombre de la propiedad del objeto subyacente que se enlaza con la propiedad del control. En este caso se usa la enumeración correspondiente a la entidad banco, la cual nos proporciona una lista con todos los nombres de las columnas de la tabla Banco en la base de datos así como el nombre de otras propiedades adicionales en el caso de las entidades (propiedades xxxString), ésta es una aportación al framework de su servilleta (o sea yo) y me apoyo en el método ToString() para convertir el nombre del elemento de la enumeración en una cadena. Esto sería equivalente a hacer lo siguiente:
    [Enum].GetName(GetType(BancoColumn), BancoColumn.IdBanco
    pero el método GetName() de las enumeraciones tiene muchos chequeos internos y al usar la función GetType() estamos usando reflection y, quienes saben lo que es reflection también conocen sus consecuencias, lo cual nos da como resultado una reducción del desempeño de las aplicaciones y además ¿a poco no es más sencillo utilizar el método ToString()? (tal vez use reflection internamente, pero es más fácil de usar).
  4. Es un valor booleano. True para aplicar formato a los valores mostrados, Falso en caso contrario.

En algunos casos, como por ejemplo, cuando se hace databinding con Comboboxes, utilizo una sobrecarga que admite un quinto (sin albur) argumento, que sirve para que el BindingSource sepa cuando propagar los cambios hechos en el control hacia el objeto subyacente, y normalmente se ve como algo así:

cboPais.DataBindings.Add("SelectedValue", bsDomicilio, DomicilioColumn.IdPais.ToString("G"), True, DataSourceUpdateMode.OnPropertyChanged)

Con estas pocas líneas de código, la entidad banco esta enlazada a los controles en el formulario y cualquier cambio que se haga en ellos se aplicará inmediatamente en dicha entidad.

Ahora bien, todo esto es muy bonito pero hay que recordar que el enlace a datos también se debe deshacer antes de cerrar el formulario o la aplicación para que no queden objetos abiertos o basura en memoria y se produzcan las molestas fugas de memoria (memory leaks). Por lo que a continuación muestro el código para desenlazar los controles:

  1. Private Sub frmEdicionDeBanco_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
  2.     ClearBindings()
  3.     oBanco = Nothing
  4.  
  5. End Sub
  6.  
  7. Private Sub ClearBindings()
  8.     ' Se quita el enlace entre el bindingsource y los controles.
  9.     txtId.DataBindings.Clear()
  10.     txtNombre.DataBindings.Clear()
  11.     txtNombreCorto.DataBindings.Clear()
  12.     chkStatus.DataBindings.Clear()
  13.  
  14.     ' Se quita la referencia de enlace entre la entidad y el bindingsource.
  15.     bsBanco.DataSource = Nothing
  16.  
  17. End Sub 
  18.  

El procedimiento ClearBindings es el que realiza todo el desenlace, primero utilizando el método Clear() de la propiedad DataBindings de los controles con lo que se eliminan todos los objetos Bindings creados previamente mediante el uso del método Add() y luego restableciendo la propiedad DataSource del componente BindingSource para desconectar la entidad Banco de este componente. De esta forma, sea cual sea la razón por la que se cierre el formulario, siempre se realizará primero el desenlace de datos y luego se cerrará el formulario.

Para finalizar esta tema del enlace a datos sencillo, voy a mostrar como persistir la instancia de la entidad Banco en la base de datos, almacenando así los nuevos valores o los cambios hechos a dicha entidad. Esto lo hago en el procedimiento controlador del evento Click del botón Aceptar.

  1.  
  2. Private Sub btnAceptar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAceptar.Click
  3.     Try
  4.         ' Si un objeto no se modifica y se intenta guardar hay que validarlo de todas formas.
  5.         BancoRules.Validate(oBanco)
  6.  
  7.         ' Se muestra el cursor de espera mientras se graba la información en la BD.
  8.         Me.Cursor = Cursors.WaitCursor
  9.  
  10.         Try
  11.             ' Si es un objeto nuevo...
  12.             If ObjectStateHelper.IsNew(oBanco) Then
  13.                 BancoRules.InsertBanco(oBanco)                              ' Se guarda (inserta) el objeto
  14.                 ClearBindings()                                             ' Se quitan los enlaces al objeto.
  15.                 oBanco = New Banco                                          ' Se crea un nuevo objeto.
  16.                 Try
  17.                     oBanco.idbanco = BancoRules.GetNextID()     ' Se obtiene un nuevo código consecutivo para cada objeto nuevo.
  18.  
  19.                 Catch ex As Exception
  20.                     ' Se restaura el cursor de ratón
  21.                     Me.Cursor = Cursors.Default
  22.  
  23.                     MessageBox.Show("Ocurrió un error al intentar obtener el número consecutivo para el nuevo banco.", _
  24.                                     "Error al buscar consecutivo", MessageBoxButtons.OK, MessageBoxIcon.Information)
  25.  
  26.                 End Try
  27.  
  28.                 oBanco.Status = True                                        ' Se inicializa su Status a verdadero (Activo).
  29.                 BindControls()                                              ' Se enlazan los controles al objeto recién creado.
  30.                 txtId.SelectAll()                                           ' Se selecciona todo el texto.
  31.                 txtId.Focus()                                               ' Se otorga el foco al primer control del formulario.
  32.             Else
  33.                 BancoRules.SaveBanco(oBanco)                                ' Se guarda (actualiza) el objeto
  34.                 Me.Close()                                                  ' Se cierra el formulario en caso de Update.
  35.  
  36.             End If
  37.  
  38.         Catch ex As Exception
  39.             ' Se restaura el cursor de ratón
  40.             Me.Cursor = Cursors.Default
  41.  
  42.             ' Si se captura una excepción, se muestran sus detalles.
  43.             MessageBox.Show(String.Format("Detalles del error:{0}{1}", Environment.NewLine, ex.Message), _
  44.                             "Ocurrieron errores al intentar guardar la información", _
  45.                             MessageBoxButtons.OK, MessageBoxIcon.Warning)
  46.  
  47.         Finally
  48.             ' Se muestra el cursor por defecto.
  49.             Me.Cursor = Cursors.Default
  50.  
  51.         End Try
  52.  
  53.     Catch ex As Exception
  54.         ' Si se captura una excepción, se muestran sus detalles.
  55.         MessageBox.Show(String.Format("Detalles del error:{0}{1}", Environment.NewLine, ex.Message), _
  56.                         "Ocurrieron errores al validar la información", _
  57.                         MessageBoxButtons.OK, MessageBoxIcon.Warning)
  58.  
  59.     End Try
  60.  
  61. End Sub

En el fragmento de código anterior, se puede ver que todo el código del procedimiento esta encerrado en un bloque Try… Catch para asegurarnos de capturar cualquier error que pueda surgir en él. Después se puede ver que se llama al método compartido (Shared) Validate de la clase BancoRules, que a su vez llama al método Validate de la clase Banco el cual se encarga de validar las propiedades de la instancia de la entidad. Este tipo de validación no está disponible por defecto en los objetos/entidades generados con Cooperator Modeler, hay que activarla quitando unos cuantos comentarios en el código generado, pero para eso escribiré otra entrada al respecto. Este método genera excepciones cuando encuentra una propiedad en la instancia con un valor incorrecto o no válido y es por eso que este llamado también se encuentra encerrado en el bloque Try … Catch del procedimiento. Después se ven una serie de instrucciones donde se revisa si la instancia de la entidad Banco es una entidad nueva o no, para saber a qué método de persistencia llamar. Esto se hace mediante un método compartido de la clase ObjectStateHelper de la librería Core de Cooperator Framework. Una vez que determina si es un objeto nuevo o no, se llama al método de persistencia pertinente (Insert o Save) a través de unos métodos compartidos en la clase de la capa Rules, aunque también se puede hacer el llamado directo usando los Mappers, pero prefiero hacerlo desde la capa Rules para poder validar permisos o hacer alguna otra operación necesaria y mantener el código en la capa de presentación más “limpio”.  Pero para quien desee ver más o menos lo que se hace en uno de estos métodos compartidos de la clase BancoRules, les muestro este fragmento de código:

  1. ''' <summary>
  2. ''' Inserta una entidad banco en la base de datos.
  3. ''' </summary>
  4. ''' <param name="entity">La entidad <see cref="Banco">Banco</see> que se va a insertar.</param>
  5. Public Shared Sub InsertBanco(ByVal entity As Banco)
  6.     If entity Is Nothing Then
  7.         Throw New ArgumentNullException("entity")
  8.  
  9.     End If
  10.     ' Aquí se puede hacer una verificación de permisos antes de grabar en la base de datos. P. Ej.
  11.     ' If Not SecurityLayer.IsAuthorized(oUsuario, BancoOperations.AddBanco.ToString(“G”)) Then
  12.     '    Throw New Exception(“El usuario no esta autorizado para realizar esta operación.”)
  13.     ' End If
  14.  
  15.     Try
  16.         BancoMapper.Instance().Insert(entity)  ' Se inserta en la base de datos.
  17.  
  18.     Catch ex As Exception
  19.         Throw
  20.  
  21.     End Try
  22.  
  23. End Sub
  24.  

Volviendo al procedimiento del botón Aceptar, se puede ver como se hace uso directo del objeto/entidad para validarlo y persistirlo ya que, cualquier cambio hecho mediante los controles en el formulario lo afectarán inmediatamente y dicho cambio estará disponible para ser guardado en la base de datos. Así que con esto, se termina la parte del enlace a datos o databinding simple.

Ahora, voy a mostrar como hacer el databinding complejo, pero en lugar de usar un objeto/entidad, usaré una lista de éstos. Veamos el formulario de ejemplo: Catalogo Banco Y ahora veamos el código de cómo se carga el formulario:

  1. Public Class frmCatalogoDeBanco
  2.     Dim oBancoListView As BancoListView
  3.     Dim strIdColumn As String = BancoColumn.IdBanco.ToString("G")
  4.  
  5.     Private Sub frmCatalogoDeBanco_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
  6.         ' Se obtienen y cargan los datos en la cuadrícula.
  7.         CargarGrid()
  8.  
  9.     End Sub
  10.  
  11.     ''' <summary>
  12.     ''' Carga la cuadrícula con datos.
  13.     ''' </summary>
  14.     Private Sub CargarGrid()
  15.         Dim strSortColumn As String = strIdColumn   ' Se ordena por default con el Id
  16.  
  17.         If (dgvBancos.DataSource IsNot Nothing) AndAlso (dgvBancos.SortedColumn IsNot Nothing) Then
  18.             strSortColumn = dgvBancos.SortedColumn.DataPropertyName
  19.  
  20.         End If
  21.  
  22.         Try
  23.             ' Se cambia el cursor (puntero del ratón) a cursor de espera.
  24.             Me.Cursor = Cursors.WaitCursor
  25.             ' Se crea una vista.
  26.             oBancoListView = New bancoListView(BancoRules.GetAll())
  27.             ' Se ordena la vista.
  28.             oBancoListView.Sort(strSortColumn, True)
  29.             ' Se asigna la vista como origen de datos de la cuadrícula.
  30.             dgvBancos.DataSource = oBancoListView
  31.             ' Se configuran las columnas.
  32.             FormateaColumnasDeGrid()
  33.  
  34.         Catch ex As Exception
  35.             ' Se restaura el cursor de ratón
  36.             Me.Cursor = Cursors.Default
  37.  
  38.             ' Se muestra el mensaje de error.
  39.             MessageBox.Show(String.Format("Detalles del error:{0}{1}", vbNewLine, ex.Message), _
  40.                             "Ocurrieron errores al intentar obtener los datos", _
  41.                             MessageBoxButtons.OK, MessageBoxIcon.Warning)
  42.         Finally
  43.             ' Se restaura el cursor de ratón
  44.             Me.Cursor = Cursors.Default
  45.  
  46.         End Try
  47.  
  48.     End Sub
  49.  
  50.     ''' <summary>
  51.     ''' Formatea las columnas de la cuadrícula.
  52.     ''' </summary>
  53.     Private Sub FormateaColumnasDeGrid()
  54.         ' Se obtiene el total de columnas en la cuadrícula
  55.         Dim intTotalColumnas As Integer = dgvBancos.Columns.Count - 1
  56.  
  57.         With dgvBancos
  58.             For intIndex As Integer = 0 To intTotalColumnas
  59.                 Select Case .Columns(intIndex).DataPropertyName
  60.                     Case bancoColumn.idbanco.ToString("G")
  61.                         .Columns(intIndex).HeaderText = "Código"
  62.                         .Columns(intIndex).AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader
  63.  
  64.                     Case bancoColumn.nombre.ToString("G")
  65.                         .Columns(intIndex).HeaderText = "Nombre"
  66.                         .Columns(intIndex).AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
  67.  
  68.                     Case bancoColumn.status.ToString("G")
  69.                         .Columns(intIndex).HeaderText = "Estatus"
  70.                         .Columns(intIndex).AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader
  71.  
  72.                     Case Else
  73.                         .Columns(intIndex).Visible = False
  74.  
  75.                 End Select
  76.  
  77.             Next
  78.  
  79.             .Refresh()
  80.  
  81.         End With
  82.  
  83.     End Sub

En este fragmento de código se puede ver que al cargarse el formulario se llama al procedimiento CargarGrid(). Este procedimiento determina cual será la columna por la que la cuadrícula será ordenada, después crea una ListView de entidades Banco, que no es mas que una Lista tipada de entidades Banco pero que tiene implementados métodos y propiedades que permiten que la lista se pueda ordenar y filtrar. Después se ordena dicha lista y posteriormente se hace el enlace a datos complejo de acuerdo a como se mencionó en los primeros párrafos de esta entrada, es decir, usando la propiedad DataSource del control DataGridView. Para finalizar, se llama al procedimiento FormateaColumnasDeGrid() que se encarga de colocar los encabezados y establecer el ancho de las columnas de la cuadrícula basándose en la propiedad que está enlazada a cada una de ellas, de nuevo, haciendo uso de la enumeración BancoColumn.

Hay que aclarar que en este caso estoy utilizando una ListView para hacer el databinding, pero se puede utilizar perfectamente una List, es decir, que en lugar de haber usado una instancia de la clase BancoListView, pude haber usado una instancia de la clase BancoList, la cual también se puede usar en el databinding, solo que, esta última clase no tiene métodos de ordenamiento o filtrado incorporados entre sus características. También hay que aclarar que en el código, al crear la ListView, se hace un llamado al método GetAll de una clase compartida en el proyecto Rules, y que éste a su vez hace el llamado al método GetAll del mapper BancoMapper. Este método GetAll() devuelve una instancia de la clase BancoList con todos los datos de la tabla Banco en la base de datos y que es necesaria pasar como argumento al constructor de la clase BancoListView. Como ya mencioné antes, yo llamo a métodos de clases en el proyecto Rules para todo el acceso a datos, pero se puede los mismo llamando directamente a los métodos de los mappers, por ejemplo, para crear la ListView, pude haber hecho algo como lo siguiente:

  1. oBancoListView = New BancoListView(BancoMapper.Instance.GetAll())

Una última aclaración es necesaria. Cuando se establece la propiedad DataSource del control DataGridView. El mecanismo de databinding se encarga de obtener las propiedades de los elementos en la ListView y los va asignando a los objetos DataGridViewColumn que contiene éste control, ahorrándonos todo ese trabajo, pero como los títulos de las columnas pueden quedar un poco raros, prefiero darles un toque de formateo, el cual en el código creo que es bastante claro.

Ahora, solo falta ver como se agregan, modifican y eliminan elementos de la lista. Para agregar/modificar hago uso del formulario de edición que mostré en el ejemplo del enlace a datos sencillo, éste cuenta con una propiedad que  permite establecer la entidad Banco que se va a editar en el formulario, el código de dicha propiedad se ve a continuación:

  1. Public Class frmEdicionDeBanco
  2.     Dim oBanco As Banco
  3.  
  4.     ''' <summary>
  5.     ''' Devuelve o establece el Banco.
  6.     ''' </summary>
  7.     ''' <value>El Banco.</value>
  8.     Public Property Banco() As Banco
  9.         Get
  10.             Return oBanco
  11.  
  12.         End Get
  13.         Set(ByVal value As Banco)
  14.             oBanco = value
  15.  
  16.         End Set
  17.  
  18.     End Property
  19.  

Y para pasar una entidad que se encuentra en la lista al formulario de edición, se usa el siguiente código:

  1. Private Sub btnModificar_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnModificar.Click
  2.     ' Se cambia el cursor (puntero del ratón) a cursor de espera.
  3.     Me.Cursor = Cursors.WaitCursor
  4.     ' Se crea una nueva instancia del formulario de edición.
  5.     Dim frmBanco As New frmEdicionDeBanco
  6.     ' Se le asigna el formulario MDI padre.
  7.     frmBanco.MdiParent = Me.MdiParent
  8.     ' Se le pasa el objeto a editar.
  9.     frmBanco.Banco = oBancoListView.Find(strIdColumn, Me.dgvBancos.CurrentRow.Cells(strIdColumn).Value)
  10.     ' Finalmente se muestra el formulario de edición.
  11.     frmBanco.Show()
  12.     ' Se restaura el cursor de ratón
  13.     Me.Cursor = Cursors.Default
  14.  
  15. End Sub
  16.  

En la línea 9 del fragmento de código anterior se puede apreciar el uso del método Find() de la ListView el cual nos permite obtener un elemento de la lista realizando una búsqueda basada en el nombre-valor de una de sus propiedades. Este método no estaba implementado en las primeras versiones de Cooperator y fue hasta la versión 1.4 o 1.4.5 que ésta fue implementada.

En el caso de que se desee agregar un nuevo objeto, solo se debe abrir el formulario de edición sin pasarle ningún objeto/entidad y en el método de carga de dicho formulario se detectará que no se pasó ningún objeto/entidad y creará uno nuevo, el cual se podrá recuperar mediante la propiedad (Banco) que se ve en el código de arriba.

Ahora bien, para eliminar objetos/entidades que se encuentran en ListViews enlazadas a DataGridViews, utilizo el siguiente código:

  1. Private Sub btnEliminar_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnEliminar.Click
  2.     Dim strMessage As String
  3.     Dim intSelected As Integer
  4.  
  5.     ' Se obtiene el número de renglones seleccionados.
  6.     intSelected = dgvBancos.SelectedRows.Count
  7.     ' Se genera el mensaje dependiendo del número de renglones seleccionados.
  8.     If intSelected = 1 Then
  9.         strMessage = "¿Está seguro de eliminar este registro?"
  10.  
  11.     Else
  12.         strMessage = "¿Está seguro de querer eliminar los registros seleccionados?"
  13.  
  14.     End If
  15.  
  16.     ' Se solicita confirmación de eliminación.
  17.     If MessageBox.Show(strMessage, "Confirme eliminación...", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) = Windows.Forms.DialogResult.Yes Then
  18.         Dim oBanco As New Banco
  19.  
  20.         ' Se recorre la colección de renglones seleccionados.
  21.         For Each oRow As DataGridViewRow In dgvBancos.SelectedRows
  22.             ' Se cambia el cursor (puntero del ratón) a cursor de espera.
  23.             Me.Cursor = Cursors.WaitCursor
  24.  
  25.             ' Se obtiene un objeto a la vez para eliminar.
  26.             oBanco = oBancoListView.Find(strIdColumn, oRow.Cells(strIdColumn).Value)
  27.  
  28.             Try
  29.                 ' Se intenta eliminar
  30.                 Rules.CustomRules.BancoRules.DeleteBanco(oBanco)
  31.                 ' Si hay éxito, se quita el objeto de la lista.
  32.                 oBancoListView.Remove(oBanco)
  33.  
  34.             Catch exNotExist As Cooperator.Framework.Data.Exceptions.RowDoNotExistException
  35.                 Dim strExMessage As String = String.Empty
  36.  
  37.                 strExMessage = String.Format("El banco {0} - {1} ya no existe en la base de datos.{2}Quizá otro usuario lo eliminó anteriormente.", oBanco.IdBanco, oBanco.Nombre, vbNewLine)
  38.  
  39.                 ' Se restaura el cursor de ratón
  40.                 Me.Cursor = Cursors.Default
  41.  
  42.                 MessageBox.Show(strExMessage, "Banco inexistente", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
  43.                 ' Se quita el objeto de la lista puesto que ya no existe.
  44.                 oBancoListView.Remove(oBanco)
  45.  
  46.  
  47.             Catch ex As Exception
  48.                 Dim strExMessage As String = String.Empty
  49.  
  50.                 strExMessage = String.Format("Detalles del error:{0}{1}", vbNewLine, ex.Message)
  51.  
  52.                 ' Se restaura el cursor de ratón
  53.                 Me.Cursor = Cursors.Default
  54.  
  55.                 MessageBox.Show(strExMessage, String.Format("Error al intentar eliminar el Banco {0} - {1}", oBanco.IdBanco, oBanco.Nombre), MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
  56.  
  57.             Finally
  58.                 ' Se restaura el cursor de ratón
  59.                 Me.Cursor = Cursors.Default
  60.  
  61.             End Try
  62.  
  63.         Next
  64.  
  65.         ' Se libera la memoria que ocupaba el objeto.
  66.         oBanco = Nothing
  67.  
  68.     End If
  69.  
  70. End Sub

El código de este procedimiento es bastante sencillo y creo que con los comentarios en él será fácil de entender, solo haré referencia a unas cuantas líneas. Primero, se puede ver nuevamente el uso del método Find() de la ListView combinado con la propiedad SelectedRows del control DataGridView para obtener los elementos seleccionados para ser eliminados. Segundo,  se debe poner atención al código encerrado en el broque Try…Catch, donde primero se intenta eliminar el objeto/entidad de la base de datos y después se remueve de la lista. Yo lo hago así para facilitarme el trabajo, porque si no se elimina el objeto/entidad de la base de datos, tampoco se elimina de la lista, además de que esto me permite no tener que recargar todos los datos de la lista desde la base de datos o después de aplicar un filtro. Pero si se desea, se puede hacer algo como lo siguiente:

  1. ' Se recorre la colección de renglones seleccionados.
  2. For Each oRow As DataGridViewRow In dgvBancos.SelectedRows
  3.     ' Se obtiene un objeto a la vez para eliminar.
  4.     oBanco = oBancoListView.Find(strIdColumn, oRow.Cells(strIdColumn).Value)
  5.     ' Se marca el objeto/entidad como borrado.
  6.     ObjectStateHelper.SetAsDeleted(oBanco)
  7. Next
  8. BancoMapper.Instance().Update(oBancoListView) 

solo que esto obligaría a hacer una de dos cosas; poner el filtro de objetos marcados como “deleted” (eliminados) por medio de la propiedad Filter de la ListView con algo como:

  1. oBancoListView.Filter = "deleted=false"

o recargar la lista desde la base de datos (con un GetAll()) para que la lista me quede sin objetos/entidades eliminados en la cuadrícula.

¡Y eso es todo! Con este código se consigue controlar las operaciones básicas más conocidas como el ABC de un catálogo, utilizando el enlace a datos complejo de una ListView de entidades generadas con el Modeler de Cooperator Framework y un DataGridView.

Como nota final, debo aclarar a aquellos curiosos que lo hayan notado, que en el formulario que se muestra en el enlace a datos sencillo aparecen unos pequeños íconos rojos. Éstos se muestran cuando se usa el componente ErrorProvider y que, con una modificación a Cooperator se puede usar para informar de los errores de validación en los objetos/entidades enlazados a los controles en los formularios, ya sea por enlace sencillo o complejo. Esta modificación será comentada en la entrada del blog que se dedicará a la validación de objetos/entidades.

 
;