WebpartsNOTA: Estas técnicas no están documentadas y se basan en mis indagaciones sobre cómo conseguir crear catálogos dinámicos de WebParts. Todo surgió a raíz de la pregunta de un alumno de mi curso de preparación del examen de certificación 70-515 de ASP.NET.

¿Cómo puedo crear un catálogo de WebParts para una página personalizada al cual le pueda añadir yo los controles que quiera dinámicamente?. Es decir, que los controles que aparecerán en el catálogo en lugar de estar determinados de antemano como en los casos habituales (en un catálogo estático de WebParts), se puedan cargar desde una base de datos o similar.

La solución

Primero, para esto hay que crear nuestra propia clase plantilla de catálogo que es la que se encargará de contener a los controles que voy a ir añadiendo. Esta clase debe implementar la interfaz ITemplate. Ello implica implementar el método InstantiateIn de esta interfaz que será llamado por la infraestructura de página cuando se deba inicializar el catálogo. Es en ese momento cuando se deben añadir los controles que deseemos a la colección de controles de la plantilla.

Dado que no tenemos forma de añadirlos desde este método tendremos que definir una colección de controles para almacenarlos temporalmente y añadirlos antes de esa instanciación. Después en la inicialización los añadiremos a la colección de controles real de la plantilla. En definitiva el código de esta clase queda así (en VB):

   1: Public Class miPlantillaDeCatalogo
   2:   Implements ITemplate
   3:  
   4:   Private _controles As New Collection()
   5:  
   6:   Public Sub InstantiateIn(ByVal container As System.Web.UI.Control) 
   7:     Implements System.Web.UI.ITemplate.InstantiateIn
   8:     If _controles.Count > 0 Then
   9:       Dim ctl As Control
  10:       For Each ctl In _controles
  11:         container.Controls.Add(ctl)
  12:       Next
  13:     End If
  14:   End Sub
  15:  
  16:   'Expongo la propiedad Controls
  17:   Public ReadOnly Property Controls() As Collection
  18:     Get
  19:       Return _controles
  20:     End Get
  21:   End Property
  22: End Class

Luego entenderemos mejor su funcionamiento.

De acuerdo. Los controles catálogo están contenidos en controles de zona. Éstos son también plantillas que se usan simplemente para mostrar los catálogos. A su vez estos controles de zona están contenidos en controles de tipo CatalogZone que son los que podemos arrastrar y soltar desde la barra de herramientas de ASP.NET.

Sabiendo esto debemos crear un control de zona personalizado que nos permita contener al catálogo antes definido. Al igual que antes tenemos que implementar la interfaz ITemplate, y como antes tampoco tenemos forma de indicar en la inicialización qué catálogo vamos a usar, por lo que tendremos que establecerlo mediante una propiedad personalizada. El código de la clase queda así:

   1: Public Class MiPlantillaDeZona
   2:   Inherits WebPart
   3:   Implements ITemplate
   4:  
   5:   Private _catalogo As miPlantillaDeCatalogo
   6:  
   7:   Public Property Catalogo() As miPlantillaDeCatalogo
   8:     Get
   9:       Return _catalogo
  10:     End Get
  11:     Set(ByVal value As miPlantillaDeCatalogo)
  12:       _catalogo = value
  13:     End Set
  14:   End Property
  15:  
  16:   Public Sub InstantiateIn(ByVal container As System.Web.UI.Control) 
  17:      Implements System.Web.UI.ITemplate.InstantiateIn
  18:     Dim dcp As New DeclarativeCatalogPart()
  19:     dcp.WebPartsTemplate = _catalogo
  20:     dcp.ID = "Mi catálogo"
  21:     dcp.Title = "Catálogo personalizado"
  22:     container.Controls.Add(dcp)
  23:   End Sub
  24: End Class

Como vemos lo que se hace es exponer una propiedad para almacenar el catálogo que queremos usar, y luego durante la inicialización de la plantilla, lo añadimos a los controles de ésta.

Ahora ya tenemos todo lo de base que necesitamos. Nos falta añadir controles a nuestro catálogo y asociarlo a un control CatalogZone que ya tendremos previamente en la página. Podemos hacerlo por ejemplo en el evento Load del formulario Web:

   1: Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)_ 
   2:    Handles Me.Load
   3:   Dim tmpl As New miPlantillaDeCatalogo()
   4:   Dim wp1 As New MiWebpart()
   5:   wp1.ID = "Usuario actual WP"
   6:   tmpl.Controls.Add(wp1)
   7:   Dim mpz As New MiPlantillaDeZona()
   8:   mpz.Catalogo = tmpl
   9:   CatalogZone1.ZoneTemplate = mpz
  10: End Sub

Como vemos lo que se hace es instanciar un catálogo personalizado. Luego se instancia el control Webpart que queremos añadir al catálogo y se añade a la colección de controles del catálogo. Se crea nuestra plantilla de zona personalizada, asignándole el catálogo para su correcta inicialización. Finalmente se asigna esta zona como plantilla de zona para el control catálogo de la página, que es el que se encarga de gestionar la infraestructura subyacente para poder añadir los controles.

Por cierto, es necesario ponerle un identificador al Webpart o romperá la aplicación.

Controles de servidor

El código anterior funciona bien si lo que tenemos en el catálogo son controles que heredan de Webpart y por lo tanto son Webarts "puras". Pero ¿qué pasa si quiero añadir a mi catálogo personalizado controles de servidor normales (que no son webParts) o incluso controles de usuario (que tampoco son Webparts)?.

La infraestructura de Webparts de ASP.NET usa la clase GenericWebpart para envolver este tipo de controles y poder utilizarlos, pero nosotros no lo tenemos tan fácil para usar esto directamente, así que para conseguirlo he tenido que hacer también mucha prueba y error. El resultado es el siguiente:

   1: Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
   2:     Handles Me.Load
   3:   Dim tmpl As New miPlantillaDeCatalogo()
   4:   Dim wp1 As GenericWebPart
   5:   Dim uc1 As UserControl
   6:   uc1 = LoadControl("~/usuarioActual.ascx")
   7:   uc1.ID = "uc1"
   8:   uc1.Attributes.Add("Title", "Usuario actual")
   9:   wp1 = WebPartManager1.CreateWebPart(uc1)
  10:   wp1.ID = "Usuario actual UC"
  11:  
  12:   tmpl.Controls.Add(wp1)
  13:   Dim mpz As New MiPlantillaDeZona()
  14:   mpz.Catalogo = tmpl
  15:   CatalogZone1.ZoneTemplate = mpz
  16: End Sub

En este caso la cosa se complica un poco.

Lo primero es instanciar el control que queremos usar. En mi caso he usado un control de usuario que se limita a mostrar el nombre del usuario actual. Para ello usamos el método LoadControl de la página. Ahora nos toca envolver ese control en un Webpart genérico. Pero para ello no podemos usar el constructor de esta clase, que es lo que hace ASP.NET por debajo, ya que éste es interno y protegido. Así que tenemos que usar el método CreateWebPart del WebPartManager que tengamos en la página. Para ello antes, hay que asignarle un identificador al control u obtendremos un error. Además le añadiremos un atributo "Title" con la descripción que queremos que aparezca en el catálogo (esta última me costó mucho Reflector para llegar a ella), o de otro modo el usuario no sabrá distinguir unos controles de otros en el catálogo.

Una vez creado el Webpart genérico es necesario asignarle un identificador, y ya podemos añadirlo a nuestro catálogo personalizado y proceder como antes.

¡Espero que te sea útil!

💪🏻 ¿Este post te ha ayudado?, ¿has aprendido algo nuevo?
Pues NO te pido que me invites a un café... Te pido algo más fácil y mucho mejor

Escrito por un humano, no por una IA