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:
- 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.
- 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 - 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
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)]_ MSDN.
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 - 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:
Código Fuente:
Todo el código fuente que os he enseñado / explicado en este documento está disponible en
La Release concreta es CTSI.Framework.Winforms 1.0.0.0.
No hay comentarios:
Publicar un comentario