ASP.NET 2.0 es un entorno de desarrollo de aplicaciones Web extenso
y complejo en sus entresijos. A pesar de que dispone de multitud de
características que nos ahorran muchas horas de trabajo, éstas no
siempre se adaptarán a nuestras necesidades. Por ejemplo, desde la
versión 1.0 del producto disponemos de tres formas de almacenar el
estado de sesión de nuestras aplicaciones. Por defecto, éste se
almacena en el mismo proceso de ejecución de ASP.NET, pero con un
simple cambio en el archivo de configuración Web.config podemos
conseguir que las variables de sesión se persistan en un servidor
central de estado o en una base de datos de SQL Server. Con estas
tres opciones tenemos para el 95% de los casos habituales. Pero,
¿qué pasa si necesitamos almacenar la información en otro lugar,
como por ejemplo una base de datos Oracle? ¿Y si aún usando uno de
los tres almacenamientos anteriores queremos cambiar su
comportamiento en ciertas circunstancias? En ASP.NET 1.x tendríamos
muy difícil cambiar cualquiera de estas cosas. No nos quedaría más
remedio que "trucar" la plataforma y escribir partes enteras de
ésta de nuevo para poder sustituirlas de modo no-nativo.
En ASP.NET 2.0 han aparecido muchas nuevas API que nos facilitan
las tareas más comunes, como la gestión de la seguridad a través de
Membership y Roles, el almacenamiento de preferencias con Profiles,
el registro de eventos de aplicaciones con WebEvents, y multitud de
cuestiones más. Cuando en alguna charla o curso enseño los
fundamentos de Membership y Roles, algo de uso muy habitual, los
oyentes suelen quedar encantados con las posibilidades que éstas
ofrecen. Sin embargo, siempre hay alguien que de repente cae en la
cuenta de lo mucho que tuvo que trabajar para conseguir algo
similar en ASP.NET 1.x, y la pregunta lógica no tarda en aparecer:
"Si uso Membership y Roles, ¿qué pasa con la inversión que he hecho
para desarrollar mi propio método? ¿Cómo puedo usar mi propia base
de datos de usuarios que ya había creado y aprovechar las
facilidades de la nueva API?". Se trata de una pregunta lógica y
relacionada directamente con el modelo de proveedores. Al final de
este artículo estaremos en disposición de contestarla.
El patrón de diseño de proveedores
Un proveedor es un componente de software que proporciona la
implementación de una interfaz estándar y que aísla a un servicio
del medio de almacenamiento u obtención de datos que utiliza para
su propósito. En lenguaje llano, se trata de un componente
intermedio que separa una funcionalidad del lugar donde almacenamos
su información. El funcionamiento de un servicio que sigue el
modelo de proveedores se puede modificar simplemente cambiando de
manera externa (por configuración) el proveedor que éste utiliza
por debajo. Es decir, el proveedor se sitúa entre la API final que
usa el programador y el medio de almacenamiento empleado. De este
modo, aunque se cambie el proveedor no hay que tocar el código y el
programa pasa a funcionar de una forma diferente sin que sea
perceptible para el programador.
Por ejemplo, en la API de seguridad Roles podemos hacer que los
perfiles a los que pertenecen nuestros usuarios se lean desde una
base de datos SQL Server en donde previamente los habremos
almacenado, o bien que se comprueben contra el Directorio Activo de
nuestra organización. Para ello no tendremos que tocar el código de
la aplicación ni recompilarla. Lo único que se debe hacer es
cambiar un ajuste en Web.config para que esta API haga uso de un
proveedor diferente en cada caso. Dicho proveedor se cargará de
forma transparente para nosotros por el motor de tiempo de
ejecución de .NET, y funcionará "de tapadillo", dando el soporte a
las funciones de la API.
El esquema de la figura 1 muestra de manera clara el concepto.
Cada una de las API representadas en la parte superior de la figura
dispone de uno o más proveedores nativos (capa del medio) que
utilizan su correspondiente medio de almacenamiento (capa
inferior). Los servicios representados en la figura son solo unos
pocos de los disponibles realmente. Casi toda funcionalidad de
ASP.NET 2.0 que tenga que ver con almacenar un estado o mover un
dato está implementada usando este modelo. Enseguida veremos qué
servicios tenemos disponibles de esta manera.
En definitiva, los objetivos que se persiguen con el modelo de
proveedores son:
• Aislar tanto nuestro código como el de ASP.NET de la
implementación y funcionamiento concreto de la mayor parte de las
API de alto nivel disponibles.
• Hacer que el almacenamiento sea flexible para adaptarlo a
nuestras necesidades, y extensible para poder usar otros medios de
persistencia alternativos. Tiene a todo el mundo contento: si nos
sirve la implementación por defecto de una API, simplemente la
usamos. Si no se nos adapta, podemos cambiarla o crear una
propia.
• Que sea muy fácil crear nuestros propios proveedores,
proporcionando un sistema de clases base bien documentado que nos
den parte del trabajo ya hecho.
• Permite separar de manera sencilla las labores de
desarrollo para trabajo en equipo: mientras unos trabajan en la
aplicación en sí con proveedores por defecto, otros pueden trabajar
en paralelo en la implementación concreta que se necesite de una
característica.
Tipos de proveedores
y proveedores disponibles
ASP.NET 2.0 ofrece soporte para el modelo de proveedores en todos
los servicios ilustrados en la figura 2. Cada uno de estos
servicios dispone de al menos un proveedor nativo integrado en la
plataforma, si bien todos ellos permiten la creación de proveedores
adicionales que nos permiten adaptar su funcionamiento a nuestras
necesidades. También se puede escribir código que en lugar de crear
un nuevo proveedor se limite a modificar el comportamiento
existente de cualquiera de los nativos.
En la tabla 1 se pueden ver los proveedores nativos disponibles
para cada uno de los servicios. Los que tienen un asterisco al
final son los proveedores por defecto que se usan en el servicio,
si no los modificamos mediante configuración. Tanto la API para
notificación de eventos Web como la de protección y cifrado de
secciones de configuración carecen de un proveedor por defecto
(ninguno tiene un asterisco en la tabla), ya que en ambos casos es
necesario establecerlos manualmente tocando la configuración si
queremos usarlos.
El funcionamiento
de los proveedores
Para ver mejor los entresijos del funcionamiento de los
proveedores, y antes de meternos de verdad en harina construyendo
uno propio, fijémonos en cómo están implementados los proveedores
de Membership de la plataforma (figura 3).
Como podemos observar, tenemos una clase llamada
MembershipProvider que hereda de la clase base abstracta
BaseProvider. Esta clase a su vez es también abstracta, de modo que
no es ésta la que define el funcionamiento del proveedor, sino que
se usa como plantilla de lo que debe implementar un proveedor de
este subsistema Membership. Es entonces cuando aparecen los
proveedores concretos que se van a utilizar. Así, el proveedor
SqlMembershipProvider hereda de MembershipProvider y sobrescribe la
mayor parte de los métodos de éste para crear una implementación
concreta de la funcionalidad requerida. Todo ello forma una
jerarquía que va de lo más general a lo concreto. La clase
BaseProvider forma los cimientos de todos los proveedores, y cada
subsistema o API concreto define su clase abstracta adaptada a sus
necesidades, de la cual heredan los proveedores concretos que se
van a utilizar.
La definición en C# de la clase BaseProvider es la
siguiente:
Solo define el nombre del proveedor, su descripción y un método de
inicialización que es lo verdaderamente importante. Al estar
declarados los métodos como virtuales, las clases que heredan de
ésta, como MembershipProvider o los proveedores finales, tienen la
opción de implementarlos para recoger desde la configuración los
parámetros de personalización que sean necesarios.
Por ejemplo, si escribimos en Web.config la definición siguiente
para el proveedor SqlMembershipProvider:
Durante la llamada que la API realiza automáticamente al método
Initialize de la clase SqlMembershipProvider, el código recupera
todos los atributos especificados dentro de la colección attributes
que se le pasa como segundo parámetro. Así se ajustan diversas
propiedades y se personaliza el comportamiento del proveedor desde
la configuración. Luego lo veremos. Dado que BaseProvider
proporciona su propia implementación también del método de
inicialización, suele ser conveniente llamarlo desde el nuestro.
Además, la clase intermedia (en este caso MembershipProvider),
aunque es abstracta, generalmente proporciona algunos servicios
comunes ya construidos. Por ejemplo, en el caso que nos ocupa el
cifrado de las claves para almacenarlas de forma segura no hace
falta que lo implementemos, pues disponemos de un método
EncodePassword que se encarga de ello por nosotros. Aunque, claro
está, no tenemos porqué usarlo.
Solo crearemos clases propias derivadas de ProviderBase si
necesitamos definir un modelo de proveedores para un servicio nuevo
creado desde cero por nosotros, pero no para un servicio existente.
En este caso, para definir un proveedor propio heredaremos de la
clase derivada de ProviderBase que se haya definido para el
servicio que nos interesa personalizar. En el caso de Membership
será de MembershipProvider, pero en el caso de la API de protección
de la configuración sería ProtectedConfigurationProvider, y de
forma análoga en los demás casos.
En la figura 4 se muestra la jerarquía de clases de proveedores
que vienen con ASP.NET 2.0. No vamos a analizar el código de
ninguna de estas implementaciones. No nos llegaría una docena de
artículos como éste. Aprenderemos creando un nuevo proveedor
propio. No obstante el lector interesado puede encontrar el código
fuente completo de todos los proveedores nativos de ASP.NET en la
siguiente URL:
http://download.microsoft.com/
download/a/b/3/ab3c284b-dc9a-473d-b7e3-33bacfcc8e98/ProviderToolkitSamples.msi
Microsoft los liberó hace unos meses para facilitar su estudio y
fomentar el uso del modelo de proveedores en aplicaciones propias.
Son dignos de pararse a estudiarlos. Se aprende mucho.
Proveedores de mapas
de sitios
Veamos ahora un ejemplo de cómo crear un proveedor propio para
personalizar el comportamiento de un subsistema de ASP.NET 2.0. El
caso típico que se suele encontrar por Internet es el que crea un
proveedor propio de Membership y/o Roles. El lector interesado en
ver un ejemplo sencillo de éstos (mucho menos complejo que el de
los proveedores nativos antes mencionados) puede descargar desde
CodePlex los "proveedores simples de Altairis":
http://www.codeplex.com/Wiki/View.aspx
?ProjectName=AltairisWebProviders
Dado que hay muchos ejemplos de este estilo por ahí, intentaremos
ser un poco más originales y hacer algo diferente a lo habitual y
que ilustre otro tipo de proveedor menos típico. Una cuestión
interesante nueva en ASP.NET 2.0 son los controles de navegación.
Éstos (menús, árboles y rutas) están enlazados a datos y beben
información de un control fuente de datos llamado
SiteMapDataSource. Basta con arrastrar sobre un formulario Web un
control SiteMapDataSource y un control de navegación como un
<asp:Menu> para tener implementada en segundos la navegación
de un sitio o aplicación.
Estos controles SiteMapDataSource obtienen la información
jerárquica de navegación a partir de un determinado proveedor de
estructura de sitio. Si nos fijamos en la figura 4, veremos que el
único proveedor nativo disponible para la navegación por un sitio
es el XmlSiteMapProvider. Éste es un proveedor especializado en
leer archivos XML que contienen información sobre la estructura de
páginas que queremos utilizar para la navegación. El archivo que se
utiliza por defecto se llama Web.sitemap, si bien es posible
definir diversos archivos XML diferentes para navegaciones
parciales con sólo añadir nuevos proveedores de este tipo.
La estructura de uno de estos archivos de navegación es análoga a
ésta:
Como podemos imaginar es un tanto tedioso de construir, sobre todo
porque Visual Studio no nos proporciona ningún tipo de editor para
facilitarnos la tarea.
Creación de un proveedor personalizado
En muchas ocasiones, sobre todo en aplicaciones sencillas, nos
resultaría de mucha más utilidad poder generar de forma automática
la estructura del sitio directamente a partir de los archivos y
carpetas que éste contiene. Así, bastaría con arrastrar un control
y ver cómo obtenemos la navegación "de la nada", con enlaces para
ir a las diferentes páginas y carpetas del sitio.
Eso es precisamente lo que vamos a construir. Nuestro proveedor
personalizado se llamará FSSiteMapProvider (de File System SiteMap
Provider), y no necesitará archivo alguno para trabajar, ya que
recorrerá la estructura de contenidos de la carpeta que le
indiquemos dentro de nuestro proyecto, y generará automáticamente
una jerarquía de navegación gemela. Nos resultará de mucha utilidad
a la hora de crear la navegación de aplicaciones de tamaño pequeño
o mediano. Es extraño que Microsoft no haya incluido algo así de
serie en ASP.NET.
Lo primero que debemos hacer es añadir una nueva clase a nuestra
carpeta AppCode. Le llamaremos igual que el proveedor,
FSSiteMapProvider:
Al heredar de StaticSiteMapProvider deberemos redefinir dos
métodos abstractos que es indispensable crear y que ya nos genera
Visual Studio por nosotros. El primero de ellos se llama
BuildSiteMap y sirve, como su propio nombre indica, para construir
el mapa del sitio a partir del medio de persistencia elegido, en
este caso una carpeta del disco duro. El motor de tiempo de
ejecución de ASP.NET llama automáticamente a este método cuando
necesita generar un mapa del sitio.
El listado 1 muestra el código para este método en nuestro ejemplo
(el código completo está disponible para descarga en la Web de
dotNetManía). Este código es bastante sencillo, ya que se ha
dividido por claridad en métodos auxiliares que hacen el trabajo
"de fondo". Esto nos permitirá además entender mejor lo que está
pasando.
He definido para la clase una serie de variables privadas que se
usan para almacenar estados internos necesarios para trabajar. El
nombre de todas ellas comienza con un guión bajo, que es la
notación que suelo usar en mi código para este tipo de
miembros.
Los mapas de sitio son una jerarquía de nodos de tipo SiteMapNode.
Cada uno de ellos contiene una referencia tanto en sentido
ascendente como descendente, es decir, a su nodo padre y a sus
hijos. Por este motivo, para obtener un mapa completo sólo es
necesaria la referencia al nodo raíz de la jerarquía, ya que
ASP.NET puede recorrerla en el sentido que prefiera para generar
los mapas. Es por ello que este primer método devuelve simplemente
una clase de tipo SiteMapNode.
Lo primero que se hace es comprobar si ya existe un mapa del sitio
previamente generado en la variable _nodoRaiz. Para no tener que
estar recorriendo el disco siempre que alguien haga una petición y
generar de nuevo el mapa en cada ocasión, lo que hacemos es
generarlo una primera vez y a partir de ese momento lo almacenamos
en una variable privada, que será lo que se devuelva en sucesivas
ocasiones. También comprobamos (con cortocircuito) si ha cambiado
la estructura de carpetas de la carpeta raíz de nuestro mapa. Más
tarde volveremos sobre este detalle, pero de momento vamos a
olvidarnos porque es accesorio a la creación del proveedor en
sí.
Si no hay ninguna copia del mapa en caché o ha cambiado la
estructura de disco, volvemos a generar la jerarquía de nuevo. El
método Resetear simplemente borra la caché en caso de que exista,
para empezar con la jerarquía desde cero. Se crea el primer nodo
indicando la ruta física y la virtual de la carpeta raíz que
usaremos para generar el mapa. Ésta se especifica mediante
configuración, como luego veremos, y se almacena en la variable
privada _carpetaRaiz.
El método CrearNodo se puede ver en el listado 2. Éste aísla la
lógica de creación de objetos SiteMapNode, y trata de manera
distinta a los nodos que van a apuntar a carpetas y a los que
apuntan a archivos. Si se trata de una carpeta hace caso omiso de
las que son de sistema, esto es, las que empiezan con el nombre
"App" (como App_Code) y la carpeta bin, que no deben aparecer en
la navegación. Además, cuando se añade una referencia a una carpeta
se le añade automáticamente el nombre de la página por defecto,
Default.aspx, que deberá existir (podría optimizarse este código
para comprobar su existencia, pero lo dejo para el lector). El nodo
se crea indicando el proveedor (Me), la ruta física y ruta virtual
a la que apunta, y como título para éste el nombre de la carpeta o
del archivo sin la extensión.
A este primer nodo creado directamente se le asigna el título que
se haya indicado en la configuración (luego lo veremos), y que se
lee a través de un propiedad personalizada llamada Inicio. Se añade
el nodo al mapa del proveedor usando una versión sobrecargada del
método AddNode que ya implementa la clase base. Con esto queda
establecida la raíz de la jerarquía.
Una vez que tenemos el primer nodo, generamos los subsiguientes
recorriendo la estructura de disco a partir de la carpeta raíz
elegida. He creado una función a tal efecto llamada
CreaMapaAPartirDe. Se trata de una función recursiva que toma como
parámetros el nodo actual y la ruta de una carpeta cuyos archivos
debemos incluir como hijos de éste en el mapa. El código de este
método se muestra en el listado 3. Usando la clase
System.IO.DirectoryInfo, añadimos como hijos del nodo actual tantos
nodos como archivos .aspx haya en la carpeta indicada. Además, por
cada carpeta contenida en éste se añade también un nodo hijo, y la
función se llama recursivamente a sí misma para generar las
entradas para sus archivos. Es la belleza de la
recursividad.
Volviendo al listado 1, lo único que resta tras haber generado la
jerarquía de nodos es devolver el nodo padre como resultado de
llamar a la función.
Destrucción de la caché
Dado que nuestro proveedor hace caché del mapa, si se producen
cambios en la estructura de disco una vez que éste haya sido
generado, quedaría obsoleto. La única forma de volver a refrescarlo
sería llamando por código al método Resetear que hemos definido.
Esto podría ser una buena solución, pues bastaría con colocar un
botón en alguna página de administración y pulsarlo cuando fuera
necesario (las actualizaciones no serían frecuentes seguramente).
De todos modos, he decidido mejorar el proveedor para que detecte
al menos los cambios que se produzcan en la carpeta raíz de nuestra
jerarquía. Para ello he aprovechado la clase CacheDependency de
ASP.NET que es la que usa el sistema de caché para determinar
cuándo debe hacer expirar los elementos que dependen de algún
archivo. El constructor de esta clase permite indicar un archivo o
carpeta a monitorizar y el momento en que queremos empezar a
controlarlo (si no lo indicamos, empieza inmediatamente).
Posteriormente se puede comprobar su propiedad HasChanged, que
devolverá true en caso de que haya cambiado algo. De hecho, también
se puede capturar un evento en el preciso instante en que se
produzca el cambio para destruir la caché, pero en nuestro caso
sólo necesitamos saber si ha cambiado cuando se nos solicite el
mapa de nuevo, no en tiempo real.
El único problema de este sistema elegido es que sólo detectará
cambios en la carpeta raíz, pero en la mayor parte de los casos
será suficiente. Podríamos construir un notificador más potente que
controlase las subcarpetas usando la clase FileSystemMonitor, pero
eso se sale por completo del ámbito de nuestro artículo.
A estas alturas, algún lector avispado se estará preguntando por
qué no hemos usado directamente el objeto Cache de ASP.NET para
almacenar el mapa que generamos, en lugar de guardarlo en una
variable privada. Enseguida veremos el motivo.
Otro detalle más
Si a estas alturas aún sigue con nosotros, recordará que dije que
era necesario otro método más para este tipo de proveedor. Este
otro método abstracto que debemos implementar para acabar de
definir nuestro flamante proveedor se llama GetRootNodeCore. Se
encuentra definido en la clase SiteMapProvider, de la que hereda
StaticSiteMapProvider, como se observa en la figura 4. Éste
es llamado automáticamente por los controles apropiados de ASP.NET
cuando se necesita obtener una referencia al nodo raíz del mapa
actual. Es funcionalmente idéntico al método anterior, por lo que
su código es bien fácil, ya que sólo hay que llamar al método
BuildSiteMap:
Protected Overrides Function _
GetRootNodeCore() As _
System.Web.SiteMapNode
Return BuildSiteMap()
End Function
Consideraciones sobre
seguridad para múltiples
subprocesos
Antes de continuar con los últimos detalles de nuestro proveedor
para a continuación poder probarlo, es necesario hacer unos
comentarios muy importantes sobre el modo en que trabajan
éstos.
En el listado 1 se observa que el código está contenido dentro de
una sentencia SyncLock. El motivo es que es necesario controlar el
acceso simultáneo de varios hilos a las variables privadas de
nuestro proveedor. En ASP.NET, generalmente todas las clases como
módulos o clases que representan páginas se instancian
individualmente por cada petición recibida (por cada hilo de
ejecución). De este modo, cada petición tiene sus propias clases, y
no es necesario tener en cuenta que puedan ser accedidas por varios
hilos al mismo tiempo. Los proveedores son una excepción a esta
norma, ya que una misma instancia de un proveedor es compartida por
todos los hilos de ejecución de la misma aplicación. Por este
motivo, es muy probable que haya varios subprocesos accediendo
simultáneamente a las mismas variables privadas de nuestro
proveedor, y es necesario por lo tanto regular el acceso a éstas
estableciendo bloqueos de sincronización con SyncLock.
Si no hubiésemos tenido en cuenta este detalle, seguramente
nuestro proveedor funcionaría perfectamente en los entornos de
pruebas y durante el desarrollo. Al ponerlo en producción y tener
muchos accesos es cuando empezarían los problemas y probablemente
nos costaría mucho averiguar qué está pasando. Así que no debemos
olvidar que todos los métodos y propiedades de un proveedor deben
estar preparados para trabajar bajo múltiples subprocesos.
Para ilustrar este concepto es por lo que hemos implementado la
caché en una variable privada de la clase. Su valor se mantiene
además entre las diferentes peticiones de un mismo o de diferentes
usuarios, por lo que tenemos una forma fácil y eficiente de
almacenar nuestros datos comunes sin recurrir al objeto Cache. Hay
multitud de formas de sincronizar el acceso multi-hilo, pero el uso
de SyncLock es el más sencillo para nuestro propósito.
Inicialización
Con lo visto hasta ahora tenemos casi todo lo necesario para dar
por terminado nuestro proveedor. Nos falta todavía una cuestión: la
inicialización del mismo.
Para poner en funcionamiento un proveedor, debemos agregarlo al
archivo de configuración de nuestra aplicación. En éste se
especifica la clase que define el proveedor y es posible indicar
los parámetros que sean necesarios para personalizar su
funcionamiento. En nuestro ejemplo podremos indicar cuál es la
carpeta raíz que usaremos para generar el mapa (por defecto "~/") y
también el nombre que queremos darle al nodo raíz ("Inicio" de
forma predeterminada). Estos parámetros se especifican en
Web.config, como veremos enseguida.
Cuando ASP.NET necesita usar el proveedor por primera vez llama a
su método Initialize, pasándole como parámetros el nombre del
proveedor y una colección de parejas nombre/valor con los atributos
especificados en la configuración. De este modo, resulta muy fácil
asignar configuraciones específicas como se ilustra en el listado
4.
Lo único que hacemos es verificar si se ha especificado o no el
parámetro, y en tal caso lo asignamos usando la propiedad
correspondiente que habremos definido en nuestra clase. Nótese que
los nombres de los parámetros distinguen entre mayúsculas y
minúsculas. Al fin y al cabo se trata de XML, y este tipo de cosas
importan.
Otra observación importante que debo hacer es que este método de
inicialización solo se llama una vez en el tiempo de duración del
dominio de aplicación de nuestro programa. Por lo tanto, este es el
único método del proveedor que no es necesario que sea seguro para
múltiples subprocesos.
También debemos asegurarnos de que llamamos al método de
inicialización de la clase base, la cual también debe hacer sus
ajustes. Y si debemos lanzar alguna excepción (algún parámetro
erróneo o ausente), lo mejor es siempre que tratemos de usar
excepciones específicas del espacio de nombres
System.Configuration.Provider, o heredadas de éstas. Como en toda
gestión de excepciones, cuanto más específico seamos, mejor.
¡Vamos a probarlo!
Por fin tenemos nuestro proveedor completamente definido. Ahora es
el momento de probarlo. Para ello debemos definirlo en Web.
config, en concreto dentro de la sección específica para
proveedores de mapas de sitios (listado 5).
En este ejemplo hemos añadido dos instancias de nuestro proveedor.
Una de ellas (ProveedorSistemaArchivos) deja los valores por
defecto para los parámetros de funcionamiento y se convierte en el
nuevo proveedor por defecto. La otra ("Admon") está parametrizada
para generar un mapa sólo con los contenidos de la carpeta
Administración de nuestro proyecto y para usar la frase "Home sweet
home" para mostrar el nodo raíz.
Una vez hecho esto, para probarlos vamos a agregar en una página
dos controles de tipo SiteMapDataSource. Uno de ellos lo dejaremos
tal cual se ha arrastrado y en el otro ajustaremos su propiedad
SiteMapProvider con el valor "Admon", para que use el segundo
proveedor que hemos definido. Añadiremos un control TreeView
enlazado al primero y un control Menu al segundo, dándoles un
aspecto bonito con las plantillas que incluyen.
Si ahora ejecutamos la aplicación veremos como no hay problema
alguno para utilizar ambas instancias del proveedor al mismo
tiempo, e incluso podríamos utilizar también alguna del proveedor
XmlSiteMapProvider que viene con ASP.NET 2.0.
Conclusión
En este artículo hemos estudiado el modelo de proveedores de
ASP.NET 2.0 y todas las ventajas que nos ofrece. Hemos visto los
distintos subsistemas que están basados en proveedores, con sus
correspondientes clases nativas. Finalmente, hemos desarrollado un
ejemplo completo de un proveedor para mapas de sitios que trabaja
generando directamente enlaces para los archivos del disco duro,
evitándonos el tener que definir engorrosos archivos XML de
navegación. Con todo esto, hemos sentado la base suficiente para
poder crear cualquier proveedor propio o modificar alguno de los
existentes heredando de las clases definidas en el sistema. Así
podremos personalizar fácilmente el comportamiento de las API más
importantes de ASP.NET 2.0.