martes, 24 de junio de 2008

Utilizar formularios abstractos desde Visual Studio 2008

El IDE de desarrollo de Visual Studio tiene ciertas limitaciones para diseñar formularios de Windows Forms que heredan de una clase abstracta; en este artículo se propone un patrón de diseño que rodea dichas limitaciones.






Implementación de Formularios Base
Una de las características más eficaces de la herencia es la posibilidad de realizar cambios en una clase base que se propagan a las clases derivadas. Cuando hablamos de aplicaciones de tipo Windows Forms, lo anterior implica que podemos tener un formulario base del que hereden el resto de formularios de la aplicación, facilitando que la aplicación sea homogénea, así como la reutilización de código.
En el presente artículo se va a tratar una problemática muy común con el uso de clases bases y heredadas en formularios y se van a ofrecer alternativas para facilitar estas operaciones.

Problema:
Cuando trabajamos con formularios heredados lo hacemos con el objeto de tener una serie de clases (formularios) base de los cuales hereden todos los formularios de nuestra aplicación, creando así pantallas homogéneas tanto en diseño como en el código asociado. Sin embargo, con esta arquitectura aparece un problema en tiempo de diseño cuando la clase base de la que heredamos es una clase abstracta. En la siguiente captura vemos el error que aparece al abrir en el diseñador de Visual Studio la clase ‘heredada’:




Como podemos observar en la captura, El diseñador de Visual Studio nos indica que no puede crear una instancia de la clase ‘base’ porque está declarada como una clase abstracta.


Detalle de la clase Base:
Public MustInherit Class Base
Inherits Form

Detalle de la clase heredada:
Public Class Inherited
Inherits Base

En este punto más de uno estará pensando ‘¿Por qué está intentando crear una instancia de la clase base, si yo estoy abriendo la clase heredada?’ En el blog de Brian Pepin tenemos las razones de este comportamiento, aunque más adelante en este documento se darán más detalles.
Una vez revisado el artículo de
Brian Pepin, alguien estará pensando ‘vale, pues no utilizaré formularios abstractos’ aquí no voy a intentar convencer a nadie de que la utilización de este tipo de clases es imprescindible, pero me vais a permitir que ponga un ejemplo:
Ejemplo:
Todos los formularios de tipo ‘Mantenimiento’ de la aplicación tienen un botón ‘Grabar’ que se encarga de guardar la información introducida en una Base de Datos. Para solucionar este requerimiento, tomamos la siguiente decisión:



  1. Genero una clase base que, heredando de la clase ‘Form’ incorpore el botón. De esta forma, cada vez que incorporemos un formulario de tipo ‘Mantenimiento’ a la aplicación, en lugar de que herede de ‘Form’, indicamos que hereda de nuestra clase base y ya tenemos el botón automáticamente en el formulario, con el diseño que le hayamos dado en la clase base. Si tenemos que cambiar el diseño de dicho botón, lo haremos en la clase base y el resto de formularios automáticamente aplicarán el nuevo diseño.

  2. Además, yo quiero que este botón en todos los formularios realice la misma operación (SaveData) y que además sólo la realice si se ha validado la posible información existente en la pantalla (ValidateData) para esto vamos a codificar el evento del botón en la clase base, de forma que ese comportamiento se ‘herede’ por todos los formularios derivados de nuestra clase base. En este punto no sabemos qué campos tenemos que validar, ni tampoco que objeto de la Base de Datos vamos a actualizar, con lo que decidimos generar la firma para las dos llamadas en la clase base, obligando mediante ‘MustOverride’ a todas las clases derivadas a que implementen esas funciones, ya que ellas son las que realmente saben con qué datos están trabajando:

    Protected MustOverride Function ValidateData() As Boolean
    Protected MustOverride Function SaveData() As Boolean

  3. En este punto, Visual Studio nos va a obligor a marcar la clase como ‘MustInherit’ ('Base' must be declared 'MustInherit' because it contains methods declared 'MustOverride'.) y esto ¿qué significa? Al marcar la clase base como ‘MustInherit’ la estamos convirtiendo en una clase abstracta y acabamos en la situación del error anterior.

Como veis en el ejemplo no es descabellado llegar a utilizar estas clases abstractas; sin embargo, como explicaba anteriormente no podemos editar en modo ‘Diseño’ los formularios que hereden de una clase abstracta.

Explicación:
(Interpretación libre del artículo de Brian Pepin) Cuando abrimos un formulario en la vista de diseño, el IDE de Visual Studio lo que hace es crear una instancia de la clase base de ese formulario, diseñar los controles existentes en la clase base mediante la rutina ‘InitializeComponents’ y a continuación mediante su propia rutina ‘InitializeComponents’ realizar su propio diseño personalizado.
Al trabajar de esta manera, cuando la clase base es una clase abstracta el diseñor devuelve la excepción anterior ya que las clases abstractas no pueden ser instanciadas directamente.

Solución Propuesta:
En múltiples foros, artículos y demás recursos de la red, se propone utilizar la compilación condicional para solucionar este problema, con estas soluciones, el inicio de la clase base sería:
#If CONFIG = "Release" Then
Public MustInherit Class Base
Inherits Form
Public MustOverride Function ValidateCode() As Boolean
Public MustOverride Function LoadData() As Boolean
#Else
Public Class Base
Inherits Form

Public Overridable Function ValidateCode() As Boolean
Throw New NotImplementedException
End Function
Public Overridable Function LoadData() As Boolean
Throw New NotImplementedException
End Function
#End If

Esta técnica funciona correctamente, pero implica tener siempre el componente donde definamos las clases base siempre en modo ‘Debug’ para poder diseñar los formularios de la aplicación; dificultaría la encapsulación de clases base en dll’s independientes, ya que si esa dll está compilada en modo ‘Release’ las clases son abstractas. Adicionalmente, al no marcar los métodos como ‘MustOverride’, corremos el riesgo de que los formularios heredados no implementen dicho método (en este caso daría una excepción, pero en determinadas circunstancias puede ser complicado detectar esas excepciones.
En el blog de Brian Pepin tenemos otra posible solución, que es la que yo he implementado y que explicaré un poco más adelante. Explicándola en pocas palabras:
El IDE de Visual Studio utiliza ‘Reflexión’ para pintar los formularios; la solución que vamos a utilizar consiste en aplicar un atributo personalizado a la clase base de forma que el IDE al encontrar ese atributo utilice una implementación personalizada de la clase para realizar el ‘pintado’ de los controles en la vista de ‘diseño’.

Descripción de la Solución:
En los fragmentos de código que se verán a continuación he utilizado la raíz ‘Base’



  • Atributo personalizado. Necesitamos un atributo para la clase Base que permitirá indicar qué implementación de la clase se utilizará al acceder en la vista de diseño. Este atributo puede ser único para todas las clases Bases.
    El código del atributo es:
    [AttributeUsage(AttributeTargets.Class)]_
    Friend Class BaseFormAttribute
    Inherits Attribute
    Private _designType As Type
    Public ReadOnly Property DesignType() As Type
    Get
    Return _designType
    End Get
    End Property
    Public Sub New(ByVal design As Type)
    _designType = design
    End Sub
    End Class

    La propiedad ‘DesignType’ especifica que clase (tipo) se utilizará para mostrar el formulario en la vista de diseño. El constructor se ejecutará cuando apliquemos el atributo a la clase base; adicionalmente hemos marcado esta clase con el atributo ‘AttributeUsage’ para indicar que este objeto sólo se aplicaría a tipos ‘Class’ más información en el sitio del
    MSDN.

  • Proveedor. Necesitamos una clase auxiliar que reemplace la lógica de reflexión utilizada por defecto por el IDE con las rutinas necesarias para aplicar la solución. El proveedor heradará de ‘TypeDescriptionProvider’ y se aplicará a la clase base como atributo. Para que cumpla su comentido tendremos que sobrecargar los métodos ‘GetReflectionType’ y ‘CreateInstance’. Este proveedor puede ser único para todas las clases Bases.
    El código del proveedor (simplificado, el código completo está en el proyecto que acompaña a este documento) es:
    Friend Class BaseFormProvider
    Inherits TypeDescriptionProvider
    Private _abstractType As Type
    Private _designType As Type
    Private Sub EnsureTypes(ByVal objectType As Type)
    .........
    End Sub
    Public Overloads Overrides Function CreateInstance(ByVal provider As IServiceProvider, ByVal objectType As Type, ByVal argTypes As Type(), ByVal args As Object()) As Object
    .........
    End Function
    Public Overloads Overrides Function GetReflectionType(ByVal objectType As Type, ByVal instance As Object) As Type
    .........
    End Function
    End Class

    Donde:

    * _abstractType es el tipo de la clase Base.
    * _designType es el tipo de la clase ‘puente’ para la vista de diseño.
    * EnsureTypes se encarga de establecer la clase Base y la clase para Diseño.
    * CreateInstance y GetReflectionType son las sobrecargas utilizadas para evitar que el diseñador llegue hasta la clase base y realice la reflexión en su lugar con la clase para Diseño.

  • Diseñador. Esta clase es la que se utilizará para ‘pintar’ los controles del formulario heredado en la vista de diseño. De esta clase heredará la clase base y contiene toda la parte de diseño de este formulario. Esta clase sólo se utiliza para diseñar; en él añadimos los controles que queremos en la clase base, su ubicación y estilo en pantalla, pero nada más. NO PONDREMOS NADA DE CÓDIGO EN ESTA CLASE. El nombre para esta clase terminará en ‘Designer’ y es necesario un diseñador para cada clase Base.

  • Clase Base. La clase final de la solución; esta clase hereda del diseñador (BaseFormDesigner) y en ella incluiremos TODO el código el código que necesitemos en la clase Base así como las declaraciones de los métodos ‘MustOverride’ que necesitemos. La clase irá decorada con los atributos que hemos generado anteriormente.
    Un ejemplo de código de la clase Base (simplificado, el código completo está en el proyecto que acompaña a este documento) es:
    [TypeDescriptionProvider(GetType(BaseFormProvider)), _
    BaseForm(GetType(BasicDataFormDesigner))]_
    Public MustInherit Class BasicDataForm
    Inherits BasicDataFormDesigner
    Protected MustOverride Function ValidateData() As Boolean
    Protected MustOverride Function SaveData() As Boolean

    Podéis observer la utilización del proveedor, del diseñador y del atributo personalizados en la decoración de la clase.
    Un ejemplo de código adicional en la clase Base (por ejemplo el del botón Grabar del ejemplo:
    Private Sub cmdOK_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnOK.Click
    If ValidateData() Then
    SaveData()
    End If
    End Sub

    Como he explicado antes, es en la clase heredada donde tendremos que implementar las dos rutinas, hasta el punto de que si no están implementadas no podremos compilar el proyecto.

Consideraciones:



  • Si no necesitáis formularios base abstractos (o dicho de otra forma, si podéis obviar el uso de métodos ‘MustOverride’) no merece la pena que implementéis esta solución.

  • He detectado un pequeño problema en el IDE de Visual Studio: Si modificas la clase base teniendo abierto un proyecto en el que hay formularios que heredan de esa clase base, al abrir uno de ellos en el modo diseño se produce el mismo error que hemos solucionado en este documento. Ahora ya os he rematado y os preguntáis ¿Entonces qué mejoro? No os preocupéis que he indicado que es un problema ‘pequeño’, ya que basta con cerrar el proyecto y volver a abrirlo para que la nueva versión de la clase base se cargue sin problemas (Salir y volver a entrar, el eterno ‘workaround’ de la informática) obviamente los proyectos que no estén cerrados se actualizarán correctamente (actualizando el componente de clases base). De todas formas, si alguien tiene alguna sugerencia al respecto, estaré encantado de escucharla / discutirla.

  • Los formularios heredados tienen que utilizar el último componente especificado como clase base.
    Código Fuente:
    Todo el código fuente que os he enseñado / explicado en este documento está disponible en CodePlex,
    La Release concreta es
    CTSI.Framework.Winforms 1.0.0.0.

No hay comentarios: