Crear un proveedor personalizado de mapas del sitio controlado por base de datos (C#)

por Scott Mitchell

Descargar PDF

El proveedor de mapa de sitio predeterminado de ASP.NET 2.0 recupera sus datos de un archivo XML estático. Aunque el proveedor basado en XML es adecuado para muchos sitios web pequeños y medianos, las aplicaciones web más grandes requieren un mapa de sitio más dinámico. En este tutorial, crearemos un proveedor de mapa de sitio personalizado que recupere sus datos de la capa de lógica de negocios, que a su vez recupera datos de la base de datos.

Introducción

La característica de mapa de sitio de ASP.NET 2.0 permite a un desarrollador de página definir un mapa de sitio de una aplicación web en algún medio persistente, como en un archivo XML. Una vez definidos, se puede acceder a los datos del mapa del sitio mediante programación a través de la clase SiteMap del espacio de nombres System.Web o a través de una variedad de controles web de navegación, como los controles SiteMapPath, Menu y TreeView. El sistema de mapa de sitio usa el modelo de proveedor para que se puedan crear y conectar diferentes implementaciones de serialización de mapa de sitio a una aplicación web. El proveedor de mapa de sitio predeterminado que se incluye con ASP.NET 2.0 conserva la estructura del mapa de sitio en un archivo XML. En el tutorial Páginas maestras y navegación del sitio creamos un archivo denominado Web.sitemap que contenía esta estructura y hemos estado actualizando su XML con cada nueva sección del tutorial.

El proveedor de mapa de sitio basado en XML predeterminado funciona bien si la estructura del mapa de sitio es bastante estática, como para estos tutoriales. Sin embargo, en muchos escenarios se necesita un mapa de sitio más dinámico. Considere el mapa del sitio que se muestra en la Ilustración 1, donde cada categoría y producto aparecen como secciones en la estructura del sitio web. Con este mapa del sitio, visitar la página web correspondiente al nodo raíz podría mostrar todos los detalles del producto, mientras que visitar una página web de categoría determinada enumeraría los productos de esa categoría y ver una página web de producto determinada.

The Categories and Products Makeup the Site Map s Structure

Ilustración 1: Las categorías y los productos conforman la estructura del mapa del sitio (haga clic para ver la imagen de tamaño completo)

Aunque esta estructura basada en categorías y productos podría codificarse de forma rígida en el archivo Web.sitemap, el archivo tendría que actualizarse cada vez que se agrega, quita o cambia el nombre de una categoría o producto. Por lo tanto, el mantenimiento del mapa de sitio se simplificaría considerablemente si su estructura se recuperaba de la base de datos o, idealmente, de la capa lógica de negocios de la arquitectura de la aplicación. De este modo, a medida que se agregaron, cambiaron el nombre o eliminaron productos y categorías, el mapa del sitio se actualizaría automáticamente para reflejar estos cambios.

Dado que serialización del mapa de sitio de ASP.NET 2.0 se ha creado sobre el modelo de proveedor, podemos crear nuestro propio proveedor de mapa de sitio personalizado que tome sus datos de un almacén de datos alternativo, como la base de datos o la arquitectura. En este tutorial, crearemos un proveedor personalizado que recupere sus datos de BLL. ¡Comencemos!

Nota:

El proveedor de mapa de sitio personalizado creado en este tutorial está estrechamente acoplado a la arquitectura y el modelo de datos de la aplicación. Los artículos de Jeff Prosise Almacenamiento de mapas de sitio en SQL Server y El proveedor de mapas de sitio de SQL que ha estado esperando examinan un enfoque generalizado para almacenar datos de mapas de sitios en SQL Server.

Paso 1: Crear las páginas web del proveedor de mapa de sitio personalizado

Antes de empezar a crear un proveedor de mapa de sitio personalizado, vamos a agregar primero las páginas de ASP.NET que necesitaremos para este tutorial. Empiece agregando una nueva carpeta denominada SiteMapProvider. A continuación, agregue las siguientes páginas de ASP.NET a esa carpeta, asegurándose de asociar cada página a la página maestra Site.master:

  • Default.aspx
  • ProductsByCategory.aspx
  • ProductDetails.aspx

Agregue también una subcarpeta CustomProviders a la carpeta App_Code.

Add the ASP.NET Pages for the Site Map Provider-Related Tutorials

Ilustración 2: Agregar las páginas de ASP.NET para los tutoriales relacionados con el proveedor del mapa del sitio

Puesto que solo hay un tutorial para esta sección, no es necesario que Default.aspx enumere los tutoriales de la sección. En su lugar, Default.aspx mostrará las categorías en un control GridView. Abordaremos esto en el paso 2.

A continuación, actualice Web.sitemap para incluir una referencia a la página Default.aspx. En concreto, agregue el marcado siguiente después del almacenamiento en caché de <siteMapNode>:

<siteMapNode 
    title="Customizing the Site Map" url="~/SiteMapProvider/Default.aspx" 
    description="Learn how to create a custom provider that retrieves the site map 
                 from the Northwind database." />

Después de actualizar Web.sitemap, dedique un momento a ver el sitio web de tutoriales a través de un explorador. El menú de la izquierda ahora incluye un elemento para el único tutorial del proveedor de mapa de sitio.

The Site Map Now Includes an Entry for the Site Map Provider Tutorial

Ilustración 3: El mapa del sitio ahora incluye una entrada para el tutorial del proveedor de mapas de sitio

Este tutorial se centra principalmente en ilustrar la creación de un proveedor de mapa de sitio personalizado y la configuración de una aplicación web para usar ese proveedor. En concreto, crearemos un proveedor que devuelva un mapa de sitio que incluya un nodo raíz junto con un nodo para cada categoría y producto, como se muestra en la Ilustración 1. En general, cada nodo del mapa de sitio puede especificar una dirección URL. Para nuestro mapa del sitio, la dirección URL del nodo raíz será ~/SiteMapProvider/Default.aspx, que enumerará todas las categorías de la base de datos. Cada nodo de categoría del mapa del sitio tendrá una dirección URL que apunte a ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=categoryID, que enumerará todos los productos del categoryID especificado. Por último, cada nodo de mapa de sitio de producto apuntará a ~/SiteMapProvider/ProductDetails.aspx?ProductID=productID, que mostrará los detalles específicos del producto.

Para empezar, es necesario crear las páginas Default.aspx, ProductsByCategory.aspx y ProductDetails.aspx. Estas páginas se completan en los pasos 2, 3 y 4, respectivamente. Dado que el foco de este tutorial está en proveedores de mapas de sitio, y desde los tutoriales anteriores han tratado la creación de estos tipos de informes maestros y detalles de varias páginas, nos apresuraremos a través de los pasos 2 a 4. Si necesita un recordatorio sobre la creación de informes maestros y detallados que abarcan varias páginas, consulte el tutorial Filtrado de maestros y detalles entre dos páginas.

Paso 2: Mostrar una lista de categorías

Abra la página Default.aspx en la carpeta SiteMapProvider y arrastre un control GridView desde el cuadro de herramientas al Diseñador y establezca su ID en Categories. Desde la etiqueta inteligente de GridView, vincule a un nuevo ObjectDataSource denominado CategoriesDataSource y configúrelo para que recupere sus datos mediante el método GetCategories de la clase CategoriesBLL. Dado que GridView solo muestra las categorías y no proporciona funcionalidades de modificación de datos, establezca las listas desplegables en las pestañas UPDATE, INSERT y DELETE en (None).

Configure the ObjectDataSource to Return Categories Using the GetCategories Method

Ilustración 4: Configuración de ObjectDataSource para devolver categorías mediante el método GetCategories (haga clic para ver la imagen de tamaño completo)

Set the Drop-Down Lists in the UPDATE, INSERT, and DELETE Tabs to (None)

Ilustración 5: Establecer las listas desplegables en las pestañas UPDATE, INSERT y DELETE en (None) (Haga clic para ver la imagen de tamaño completo)

Después de completar el Asistente para configurar orígenes de datos, Visual Studio agregará un BoundField para CategoryID, CategoryName, Description, NumberOfProducts y BrochurePath. Edite GridView para que solo contenga los BoundFields CategoryName y Description y actualice la propiedad HeaderText del BoundField CategoryName a Category.

A continuación, agregue un HyperLinkField y colóquelo para que sea el campo más a la izquierda. Establezca la propiedad DataNavigateUrlFields en CategoryID y la propiedad DataNavigateUrlFormatString en ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID={0}. Establezca la propiedad Text en Ver productos.

Add a HyperLinkField to the Categories GridView

Ilustración 6: Agregar un HyperLinkField al GridView Categories

Después de crear ObjectDataSource y personalizar los campos de GridView, el marcado declarativo de los dos controles tendrá un aspecto similar al siguiente:

<asp:GridView ID="Categories" runat="server" AutoGenerateColumns="False" 
    DataKeyNames="CategoryID" DataSourceID="CategoriesDataSource" 
    EnableViewState="False">
    <Columns>
        <asp:HyperLinkField DataNavigateUrlFields="CategoryID" 
            DataNavigateUrlFormatString=
                "~/SiteMapProvider/ProductsByCategory.aspx?CategoryID={0}"
            Text="View Products" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            SortExpression="CategoryName" />
        <asp:BoundField DataField="Description" HeaderText="Description" 
            SortExpression="Description" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="CategoriesDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetCategories" 
    TypeName="CategoriesBLL"></asp:ObjectDataSource>

En la Ilustración 7 se muestra Default.aspx cuando se ve a través de un explorador. Al hacer clic en el vínculo Ver productos de una categoría, se le lleva a ProductsByCategory.aspx?CategoryID=categoryID, que se compilará en el paso 3.

Each Category is Listed Along with a View Products Link

Ilustración 7: Cada categoría aparece junto con un vínculo Ver productos (haga clic para ver la imagen de tamaño completo)

Paso 3: Enumerar los productos de la categoría seleccionada

Abra la página ProductsByCategory.aspx y agregue un control GridView y asígnele el nombre ProductsByCategory. Desde su etiqueta inteligente, enlace GridView a un nuevo ObjectDataSource denominado ProductsByCategoryDataSource. Configure ObjectDataSource para usar el método GetProductsByCategoryID(categoryID) de la clase ProductsBLL y establezca las listas desplegables en (None) en las pestañas UPDATE, INSERT y DELETE.

Use the ProductsBLL Class s GetProductsByCategoryID(categoryID) Method

Ilustración 8: Usar el método GetProductsByCategoryID(categoryID) de la clase ProductsBLL (haga clic para ver la imagen de tamaño completo)

El último paso del Asistente para configurar orígenes de datos solicita un origen de parámetros para categoryID. Dado que esta información se pasa a través del campo querystring CategoryID, seleccione QueryString en la lista desplegable y escriba CategoryID en el cuadro de texto QueryStringField, como se muestra en la Ilustración 9. Haga clic en Finalizar para completar el asistente.

Use the CategoryID Querystring Field for the categoryID Parameter

Ilustración 9: Usar el campo querystring CategoryID para el parámetro categoryID (haga clic para ver la imagen de tamaño completo)

Después de completar el asistente, Visual Studio agregará los campos BoundFields y CheckBoxField correspondientes a GridView para los campos de datos del producto. Quite todos los campos excepto los BoundFields ProductName, UnitPrice y SupplierName. Personalice estas tres propiedades BoundFields HeaderText para leer Producto, Precio y Proveedor, respectivamente. Aplique formato al BoundField UnitPrice como moneda.

A continuación, agregue un HyperLinkField y muévalo a la posición más a la izquierda. Establezca su propiedad Text en Ver detalles, su propiedad DataNavigateUrlFields en ProductID y su propiedad DataNavigateUrlFormatString en ~/SiteMapProvider/ProductDetails.aspx?ProductID={0}.

Add a View Details HyperLinkField that Points to ProductDetails.aspx

Ilustración 10: Agregar un HyperLinkField de Ver detalles que apunta a ProductDetails.aspx

Después de realizar estas personalizaciones, el marcado declarativo GridView y ObjectDataSource deben ser similares a los siguientes:

<asp:GridView ID="ProductsByCategory" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ProductsByCategoryDataSource" 
    EnableViewState="False">
    <Columns>
        <asp:HyperLinkField DataNavigateUrlFields="ProductID" 
            DataNavigateUrlFormatString=
                "~/SiteMapProvider/ProductDetails.aspx?ProductID={0}"
            Text="View Details" />
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}" 
            HeaderText="Price" HtmlEncode="False" 
            SortExpression="UnitPrice" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier" 
            ReadOnly="True" SortExpression="SupplierName" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsByCategoryDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProductsByCategoryID" TypeName="ProductsBLL">
    <SelectParameters>
        <asp:QueryStringParameter Name="categoryID" 
            QueryStringField="CategoryID" Type="Int32" />
    </SelectParameters>
</asp:ObjectDataSource>

Vuelva a ver Default.aspx a través de un explorador y haga clic en el vínculo Ver productos para Bebidas. Esto le llevará a ProductsByCategory.aspx?CategoryID=1, mostrando los nombres, precios y proveedores de los productos de la base de datos Northwind que pertenecen a la categoría Bebidas (vea la Ilustración 11). No dude en mejorar aún más esta página para incluir un vínculo para devolver usuarios a la página de descripción de categorías (Default.aspx) y un control DetailsView o FormView que muestre el nombre y la descripción de la categoría seleccionada.

The Beverages Names, Prices, and Suppliers are Displayed

Ilustración 11: Se muestran los nombres, los precios y los proveedores de bebidas (haga clic para ver la imagen de tamaño completo)

Paso 4: Mostrar los detalles de un producto

La página final, ProductDetails.aspx, muestra los detalles de los productos seleccionados. Abra ProductDetails.aspx y arrastre un control DetailsView desde el Cuadro de herramientas al Diseñador. Establezca la propiedad ID de DetailsView en ProductInfo y borre sus valores de propiedad Height y Width. Desde su etiqueta inteligente, enlaza DetailsView a un nuevo ObjectDataSource denominado ProductDataSource, configurando ObjectDataSource para extraer sus datos del método GetProductByProductID(productID) de la clase ProductsBLL. Al igual que con las páginas web anteriores creadas en los pasos 2 y 3, establezca las listas desplegables en las pestañas UPDATE, INSERT y DELETE en (None).

Configure the ObjectDataSource to Use the GetProductByProductID(productID) Method

Ilustración 12: Configuración de ObjectDataSource para usar el método GetProductByProductID(productID) (haga clic para ver la imagen de tamaño completo)

El último paso del Asistente para configurar orígenes de datos solicita el origen del parámetro productID. Dado que estos datos pasan por el campo querystring ProductID, establezca la lista desplegable en QueryString y el cuadro de texto QueryStringField en ProductID. Por último, haga clic en el botón Finalizar para completar el asistente.

Configure the productID Parameter to Pull its Value from the ProductID Querystring Field

Ilustración 13: Configurar el parámetro productID para extraer su valor del campo Querystring ProductID (haga clic para ver la imagen de tamaño completo)

Después de completar el Asistente para configurar orígenes de datos, Visual Studio creará los campos BoundFields y CheckBoxField correspondientes en DetailsView para los campos de datos del producto. Quite los BoundFields ProductID, SupplierID y CategoryID y configure los campos restantes a medida que considere oportuno. Después de una serie de configuraciones estéticas, el marcado declarativo de DetailsView y ObjectDataSource era similar al siguiente:

<asp:DetailsView ID="ProductInfo" runat="server" AutoGenerateRows="False" 
    DataKeyNames="ProductID" DataSourceID="ProductDataSource" 
    EnableViewState="False">
    <Fields>
        <asp:BoundField DataField="ProductName" HeaderText="Product" 
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier" 
            ReadOnly="True" SortExpression="SupplierName" />
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit" 
            SortExpression="QuantityPerUnit" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}" 
            HeaderText="Price" HtmlEncode="False" 
            SortExpression="UnitPrice" />
        <asp:BoundField DataField="UnitsInStock" HeaderText="Units In Stock" 
            SortExpression="UnitsInStock" />
        <asp:BoundField DataField="UnitsOnOrder" HeaderText="Units On Order" 
            SortExpression="UnitsOnOrder" />
        <asp:BoundField DataField="ReorderLevel" HeaderText="Reorder Level" 
            SortExpression="ReorderLevel" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" 
            SortExpression="Discontinued" />
    </Fields>
</asp:DetailsView>
<asp:ObjectDataSource ID="ProductDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProductByProductID" TypeName="ProductsBLL">
    <SelectParameters>
        <asp:QueryStringParameter Name="productID" 
            QueryStringField="ProductID" Type="Int32" />
    </SelectParameters>
</asp:ObjectDataSource>

Para probar esta página, vuelva a Default.aspx y haga clic en Ver productos para la categoría Bebidas. En la lista de productos de bebidas, haga clic en el vínculo Ver detalles del té Chai. Esto le llevará a ProductDetails.aspx?ProductID=1, que muestra los detalles del té Chai (véase la Ilustración 14).

Chai Tea s Supplier, Category, Price, and Other Information is Displayed

Ilustración 14: Se muestra el proveedor, la categoría, el precio y otra información del té Chai (Haga clic para ver la imagen de tamaño completo)

Paso 5: Descripción de los trabajos internos de un proveedor de mapa de sitio

El mapa de sitio se representa en la memoria del servidor web como una colección de instancias SiteMapNode que forman una jerarquía. Debe haber exactamente una raíz, todos los nodos no raíz deben tener exactamente un nodo primario y todos los nodos pueden tener un número arbitrario de elementos secundarios. Cada objeto SiteMapNode representa una sección de la estructura del sitio web; estas secciones suelen tener una página web correspondiente. Por lo tanto, la clase SiteMapNode tiene propiedades como Title, Url y Description, que proporcionan información para la sección que SiteMapNode representa. También hay una propiedad Key que identifica de forma única cada SiteMapNode de la jerarquía, así como las propiedades usadas para establecer esta jerarquía ChildNodes, ParentNode, NextSibling, PreviousSibling, etc.

En la Ilustración 15 se muestra la estructura general del mapa del sitio de la Ilustración 1, pero con los detalles de implementación dibujados con más detalle.

Each SiteMapNode has Properties Like Title, Url, Key, and So On

Ilustración 15: Cada SiteMapNode tiene propiedades como Title, Url, Key, etc. (Haga clic para ver la imagen de tamaño completo)

El mapa del sitio es accesible a través de la clase SiteMap del espacio de nombres System.Web. Esta propiedad de clase RootNode devuelve la instancia raíz SiteMapNode del mapa del sitio; CurrentNode devuelve SiteMapNode cuya propiedad Url coincide con la dirección URL de la página solicitada actualmente. Esta clase se usa internamente por los controles web de navegación de ASP.NET 2.0.

Cuando se accede a las propiedades de la clase SiteMap, debe serializar la estructura del mapa de sitio de algún medio persistente en la memoria. Sin embargo, la lógica de serialización del mapa de sitio no está codificada de forma rígida en la clase SiteMap. En su lugar, en tiempo de ejecución, la clase SiteMap determina qué proveedor de mapa de sitio se va a usar para la serialización. De forma predeterminada, se usa la clase XmlSiteMapProvider, que lee la estructura del mapa de sitio de un archivo XML con formato correcto. Sin embargo, con un poco de trabajo podemos crear nuestro propio proveedor de mapas de sitio personalizado.

Todos los proveedores de mapas de sitio deben derivarse de la clase SiteMapProvider, que incluye los métodos y propiedades esenciales necesarios para los proveedores de mapas de sitio, pero omite muchos de los detalles de implementación. Una segunda clase, StaticSiteMapProvider, extiende la clase SiteMapProvider y contiene una implementación más sólida de la funcionalidad necesaria. Internamente, el StaticSiteMapProvider almacena las instancias de SiteMapNode del mapa de sitio en un Hashtable y proporciona métodos como AddNode(child, parent), RemoveNode(siteMapNode), y Clear() que agregan y quitan SiteMapNode al Hashtable interno. La clase XmlSiteMapProvider se deriva de la clase StaticSiteMapProvider.

Al crear un proveedor de mapa de sitio personalizado que extiende StaticSiteMapProvider, hay dos métodos abstractos que se deben invalidar: BuildSiteMap y GetRootNodeCore. BuildSiteMap, como su nombre implica, es responsable de cargar la estructura del mapa de sitio desde el almacenamiento persistente y construirla en memoria. GetRootNodeCore devuelve el nodo raíz en el mapa del sitio.

Para que una aplicación web pueda usar un proveedor de mapa de sitio, debe registrarse en la configuración de la aplicación. De forma predeterminada, la clase XmlSiteMapProvider se registra con el nombre AspNetXmlSiteMapProvider. Para registrar proveedores de mapas de sitio adicionales, agregue el marcado siguiente a Web.config:

<configuration>
    <system.web>
        ...
        <siteMap defaultProvider="defaultProviderName">
          <providers>
            <add name="name" type="type" />
          </providers>
        </siteMap>
    </system.web>
</configuration>

El valor name asigna un nombre legible al proveedor, mientras que type especifica el nombre de tipo completo del proveedor de mapa de sitio. Exploraremos valores concretos para los valores de name y type en el paso 7, después de crear nuestro proveedor de mapas de sitio personalizado.

La clase de proveedor de mapa de sitio se crea una instancia de la primera vez que se obtiene acceso desde la clase SiteMap y permanece en memoria durante la vigencia de la aplicación web. Dado que solo hay una instancia del proveedor de mapa de sitio que se puede invocar desde varios visitantes del sitio web simultáneos, es imperativo que los métodos del proveedor sean seguros para subprocesos.

Por motivos de rendimiento y escalabilidad, es importante almacenar en caché la estructura del mapa del sitio en memoria y devolver esta estructura almacenada en caché en lugar de volver a crearla cada vez que se invoca el método BuildSiteMap. Se puede llamar varias veces a BuildSiteMap por solicitud de página por usuario, en función de los controles de navegación que se usen en la página y la profundidad de la estructura del mapa del sitio. En cualquier caso, si no almacenamos en caché la estructura del mapa de sitio en BuildSiteMap cada vez que se invoca, tendríamos que volver a recuperar la información de producto y categoría de la arquitectura (lo que daría lugar a una consulta a la base de datos). Como hemos descrito en los tutoriales de almacenamiento en caché anteriores, los datos almacenados en caché pueden quedar obsoletos. Para combatir esto, podemos usar expiraciones basadas en dependencias de la memoria caché de SQL o de tiempo.

Nota:

Un proveedor de mapa de sitio puede invalidar opcionalmente el método Initialize. Se invoca a Initialize cuando primero se crea una instancia del proveedor de mapa de sitio y se pasan los atributos personalizados asignados al proveedor en Web.config en el elemento <add> como: <add name="name" type="type" customAttribute="value" />. Resulta útil si desea permitir que un desarrollador de páginas especifique varias configuraciones relacionadas con el proveedor de mapas de sitio sin tener que modificar el código del proveedor. Por ejemplo, si estábamos leyendo los datos de categoría y productos directamente desde la base de datos en lugar de a través de la arquitectura, es probable que deseemos permitir que el desarrollador de páginas especifique la cadena de conexión de la base de datos a través de Web.config en lugar de usar un valor codificado de forma rígida en el código del proveedor. El proveedor de mapa de sitio personalizado que crearemos en el paso 6 no invalida este método Initialize. Para obtener un ejemplo de uso del método Initialize, consulte el artículo de Jeff ProsiseAlmacenamiento de mapas de sitio en SQL Server.

Paso 6: Crear el proveedor de mapa de sitio personalizado

Para crear un proveedor de mapa de sitio personalizado que compile el mapa de sitio a partir de las categorías y productos de la base de datos Northwind, es necesario crear una clase que extienda StaticSiteMapProvider. En el paso 1 le pedí que agregara una carpeta CustomProviders en la carpeta App_Code, agregue una nueva clase a esta carpeta denominada NorthwindSiteMapProvider. Agregue el siguiente código a la clase NorthwindSiteMapProvider:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Web.Caching;
public class NorthwindSiteMapProvider : StaticSiteMapProvider
{
    private readonly object siteMapLock = new object();
    private SiteMapNode root = null;
    public const string CacheDependencyKey = 
        "NorthwindSiteMapProviderCacheDependency";
    public override SiteMapNode BuildSiteMap()
    {
        // Use a lock to make this method thread-safe
        lock (siteMapLock)
        {
            // First, see if we already have constructed the
            // rootNode. If so, return it...
            if (root != null)
                return root;
            // We need to build the site map!
            
            // Clear out the current site map structure
            base.Clear();
            // Get the categories and products information from the database
            ProductsBLL productsAPI = new ProductsBLL();
            Northwind.ProductsDataTable products = productsAPI.GetProducts();
            // Create the root SiteMapNode
            root = new SiteMapNode(
                this, "root", "~/SiteMapProvider/Default.aspx", "All Categories");
            AddNode(root);
            // Create SiteMapNodes for the categories and products
            foreach (Northwind.ProductsRow product in products)
            {
                // Add a new category SiteMapNode, if needed
                string categoryKey, categoryName;
                bool createUrlForCategoryNode = true;
                if (product.IsCategoryIDNull())
                {
                    categoryKey = "Category:None";
                    categoryName = "None";
                    createUrlForCategoryNode = false;
                }
                else
                {
                    categoryKey = string.Concat("Category:", product.CategoryID);
                    categoryName = product.CategoryName;
                }
                SiteMapNode categoryNode = FindSiteMapNodeFromKey(categoryKey);
                // Add the category SiteMapNode if it does not exist
                if (categoryNode == null)
                {
                    string productsByCategoryUrl = string.Empty;
                    if (createUrlForCategoryNode)
                        productsByCategoryUrl = 
                            "~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=" 
                            + product.CategoryID;
                    categoryNode = new SiteMapNode(
                        this, categoryKey, productsByCategoryUrl, categoryName);
                    AddNode(categoryNode, root);
                }
                // Add the product SiteMapNode
                string productUrl = 
                    "~/SiteMapProvider/ProductDetails.aspx?ProductID=" 
                    + product.ProductID;
                SiteMapNode productNode = new SiteMapNode(
                    this, string.Concat("Product:", product.ProductID), 
                    productUrl, product.ProductName);
                AddNode(productNode, categoryNode);
            }
            
            // Add a "dummy" item to the cache using a SqlCacheDependency
            // on the Products and Categories tables
            System.Web.Caching.SqlCacheDependency productsTableDependency = 
                new System.Web.Caching.SqlCacheDependency("NorthwindDB", "Products");
            System.Web.Caching.SqlCacheDependency categoriesTableDependency = 
                new System.Web.Caching.SqlCacheDependency("NorthwindDB", "Categories");
            // Create an AggregateCacheDependency
            System.Web.Caching.AggregateCacheDependency aggregateDependencies = 
                new System.Web.Caching.AggregateCacheDependency();
            aggregateDependencies.Add(productsTableDependency, categoriesTableDependency);
            // Add the item to the cache specifying a callback function
            HttpRuntime.Cache.Insert(
                CacheDependencyKey, DateTime.Now, aggregateDependencies, 
                Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, 
                CacheItemPriority.Normal, 
                new CacheItemRemovedCallback(OnSiteMapChanged));
            // Finally, return the root node
            return root;
        }
    }
    protected override SiteMapNode GetRootNodeCore()
    {
        return BuildSiteMap();
    }
    protected void OnSiteMapChanged(string key, object value, CacheItemRemovedReason reason)
    {
        lock (siteMapLock)
        {
            if (string.Compare(key, CacheDependencyKey) == 0)
            {
                // Refresh the site map
                root = null;
            }
        }
    }
    public DateTime? CachedDate
    {
        get
        {
            return HttpRuntime.Cache[CacheDependencyKey] as DateTime?;
        }
    }
}

Comencemos con la exploración de este método de clase BuildSiteMap, que comienza con una instrucción lock. La instrucción lock solo permite escribir un subproceso a la vez, serializando así el acceso a su código y evitando que dos subprocesos simultáneos se ejecuten paso a paso entre sí.

La variable root de nivel de clase SiteMapNode se usa para almacenar en caché la estructura del mapa del sitio. Cuando el mapa de sitio se construye por primera vez, o por primera vez después de modificar los datos subyacentes, root será null y se construirá la estructura del mapa de sitio. El nodo raíz del mapa de sitio se asigna a root durante el proceso de construcción para que se llame a la próxima vez que se llame a este método, root no será null. Por lo tanto, siempre que root no sea null, la estructura del mapa del sitio se devolverá al autor de la llamada sin tener que volver a crearla.

Si root es null, la estructura del mapa de sitio se crea a partir de la información de producto y categoría. El mapa de sitio se crea mediante la creación de las instancias SiteMapNode y, a continuación, la formación de la jerarquía a través de llamadas al método AddNode de la clase StaticSiteMapProvider. AddNode realiza el mantenimiento interno de la contabilidad, almacenando las instancias más variadas SiteMapNode en un Hashtable. Antes de empezar a construir la jerarquía, empezamos llamando al método Clear, que borra los elementos del Hashtable interno. A continuación, el método GetProducts de la clase ProductsBLL y el ProductsDataTable resultante se almacenan en variables locales.

La construcción del mapa de sitio comienza creando el nodo raíz y asignándolo a root. La sobrecarga del constructor de SiteMapNode que se usa aquí y, a lo largo de BuildSiteMap, se pasa la siguiente información:

  • Referencia al proveedor de mapa de sitio (this).
  • El Key de SiteMapNode. Este valor necesario debe ser único para cada SiteMapNode.
  • El Url de SiteMapNode. Url es opcional, pero si se proporciona, cada valor Url de SiteMapNode debe ser único.
  • El Title de SiteMapNode, que es necesario.

La llamada al método AddNode(root) agrega SiteMapNoderoot al mapa del sitio como raíz. A continuación, se enumera cada ProductRow del ProductsDataTable. Si ya existe un SiteMapNode para la categoría del producto actual, se hace referencia a él. De lo contrario, se crea un nuevo SiteMapNode para la categoría y se agrega como elemento secundario de SiteMapNode``root a través de la llamada al método AddNode(categoryNode, root). Una vez encontrado o creado el nodo de categoría SiteMapNode adecuado, se crea SiteMapNode para el producto actual y se agrega como elemento secundario de la categoría SiteMapNode a través de AddNode(productNode, categoryNode). Tenga en cuenta que el valor de propiedad Url de la categoría SiteMapNode es ~/SiteMapProvider/ProductsByCategory.aspx?CategoryID=categoryID mientras que a la propiedad Url del producto SiteMapNode se le asigna ~/SiteMapNode/ProductDetails.aspx?ProductID=productID.

Nota:

Los productos que tienen un valor de base de datos NULL para su CategoryID se agrupan en una categoría SiteMapNode cuya propiedad Title se establece en None y cuya propiedad Url se establece en una cadena vacía. Decidí establecer Url en una cadena vacía, ya que el método GetProductsByCategory(categoryID) de la clase ProductBLL carece actualmente de la capacidad de devolver solo esos productos con un valor NULLCategoryID. Además, quería demostrar cómo los controles de navegación representan un SiteMapNode que carece de un valor para su propiedad Url. Te animamos a ampliar este tutorial para que la propiedad Url None de SiteMapNode apunte a ProductsByCategory.aspx, pero solo muestra los productos con valores NULLCategoryID.

Después de construir el mapa de sitio, se agrega un objeto arbitrario a la caché de datos mediante una dependencia de caché de SQL en las tablas Categories y Products a través de un objeto AggregateCacheDependency. Hemos explorado el uso de dependencias de caché de SQL en el tutorial anterior, Uso de dependencias de caché de SQL. Sin embargo, el proveedor de mapa de sitio personalizado usa una sobrecarga del método Insert de la memoria caché de datos que todavía no hemos explorado. Esta sobrecarga acepta como parámetro de entrada final un delegado al que se llama cuando se quita el objeto de la memoria caché. En concreto, pasamos un nuevo delegado CacheItemRemovedCallback que apunta al método OnSiteMapChanged definido más abajo en la clase NorthwindSiteMapProvider.

Nota:

La representación en memoria del mapa del sitio se almacena en caché a través de la variable de nivel de clase root. Puesto que solo hay una instancia de la clase de proveedor de mapa de sitio personalizada y, dado que esa instancia se comparte entre todos los subprocesos de la aplicación web, esta variable de clase actúa como caché. El método BuildSiteMap también usa la caché de datos, pero solo como medio para recibir notificaciones cuando cambian los datos de la base de datos subyacentes de las tablas Categories o Products. Tenga en cuenta que el valor que se coloca en la caché de datos es solo la fecha y hora actuales. Los datos reales del mapa del sitio no se colocan en la caché de datos.

El método BuildSiteMap se completa devolviendo el nodo raíz del mapa del sitio.

Los métodos restantes son bastante sencillos. GetRootNodeCore es responsable de devolver el nodo raíz. Dado que BuildSiteMap devuelve la raíz, GetRootNodeCore simplemente devuelve el valor devuelto de BuildSiteMap. El método OnSiteMapChanged establece root en null cuando se quita el elemento de caché. Con la raíz establecida de nuevo en null, la próxima vez que se invoque BuildSiteMap, se volverá a generar la estructura del mapa del sitio. Por último, la propiedad CachedDate devuelve el valor de fecha y hora almacenado en la caché de datos, si existe ese valor. Un desarrollador de páginas puede usar esta propiedad para determinar cuándo se almacenaron por última vez en caché los datos del mapa del sitio.

Paso 7: Registrar el NorthwindSiteMapProvider

Para que nuestra aplicación web use el proveedor de mapa de sitio NorthwindSiteMapProvider creado en el paso 6, es necesario registrarla en la sección <siteMap> de Web.config. En concreto, agregue el marcado siguiente dentro del elemento <system.web> en Web.config:

<siteMap defaultProvider="AspNetXmlSiteMapProvider">
  <providers>
    <add name="Northwind" type="NorthwindSiteMapProvider" />
  </providers>
</siteMap>

Este marcado hace dos cosas: en primer lugar, indica que el AspNetXmlSiteMapProvider integrado es el proveedor de mapa de sitio predeterminado; en segundo lugar, registra el proveedor de mapa de sitio personalizado creado en el paso 6 con el nombre descriptivo Northwind .

Nota:

En el caso de los proveedores de mapa de sitio ubicados en la carpeta App_Code de la aplicación, el valor del atributo type es simplemente el nombre de clase. Como alternativa, el proveedor de mapa de sitio personalizado podría haberse creado en un proyecto de biblioteca de clases independiente con el ensamblado compilado colocado en el directorio /Bin de la aplicación web. En ese caso, el valor del atributo type sería Namespace.ClassName, AssemblyName.

Después de actualizar Web.config, dedique un momento a ver cualquier página de los tutoriales de un explorador. Tenga en cuenta que la interfaz de navegación de la izquierda sigue mostrando las secciones y tutoriales definidos en Web.sitemap. Esto se debe a que dejamos AspNetXmlSiteMapProvider como proveedor predeterminado. Para crear un elemento de interfaz de usuario de navegación que use NorthwindSiteMapProvider, es necesario especificar explícitamente que se debe usar el proveedor de mapa de sitio Northwind. Veremos cómo hacerlo en el paso 8.

Paso 8: Mostrar información del mapa de sitio mediante el proveedor de mapa de sitio personalizado

Con el proveedor de mapa de sitio personalizado creado y registrado en Web.config, estamos listos para agregar controles de navegación a las páginas Default.aspx, ProductsByCategory.aspx y ProductDetails.aspx de la carpeta SiteMapProvider. Para empezar, abra la página Default.aspx y arrastre un elemento SiteMapPath desde el Cuadro de herramientas al Diseñador. El control SiteMapPath se encuentra en la sección Navegación del Cuadro de herramientas.

Add a SiteMapPath to Default.aspx

Ilustración 16: Agregar un SiteMapPath a Default.aspx (Haga clic para ver la imagen de tamaño completo)

El control SiteMapPath muestra una ruta de navegación, que indica la ubicación de la página actual dentro del mapa del sitio. Hemos agregado un SiteMapPath a la parte superior de la página maestra en el tutorial Páginas maestras y navegación del sitio.

Dedique un momento a ver esta página a través de un explorador. El SiteMapPath agregado en la Ilustración 16 usa el proveedor de mapa de sitio predeterminado, que extrae sus datos de Web.sitemap. Por lo tanto, la ruta de navegación muestra Inicio > Personalización del mapa del sitio, al igual que la ruta de navegación en la esquina superior derecha.

The Breadcrumb Uses the Default Site Map Provider

Ilustración 17: La ruta de navegación usa el proveedor de mapa de sitio predeterminado (haga clic para ver la imagen de tamaño completo)

Para que EL SiteMapPath agregado en la Ilustración 16 use el proveedor de mapa de sitio personalizado que creamos en el paso 6, establezca su propiedad SiteMapProvider en Northwind, el nombre asignado a NorthwindSiteMapProvider en Web.config. Desafortunadamente, el Diseñador sigue usando el proveedor de mapa de sitio predeterminado, pero si visita la página a través de un explorador después de realizar este cambio de propiedad, verá que la ruta de navegación ahora usa el proveedor de mapa de sitio personalizado.

Screenshot showing how the breadcrumb displays the custom site map provider.

Ilustración 18: La ruta de navegación usa ahora el proveedor de mapa de sitio personalizado NorthwindSiteMapProvider (haga clic para ver la imagen de tamaño completo)

El control SiteMapPath muestra una interfaz de usuario más funcional en las páginas ProductsByCategory.aspx y ProductDetails.aspx. Agregue un SiteMapPath a estas páginas, estableciendo la propiedad SiteMapProvider en Northwind. Desde Default.aspx, haga clic en el vínculo Ver productos para bebidas y, a continuación, en el vínculo Ver detalles del té Chai. Como se muestra en la Ilustración 19, la ruta de navegación incluye la sección actual del mapa del sitio (Té Chai) y sus antecesores: Bebidas y Todas las categorías .

Screenshot showing how the breadcrumb displays the current site map section (Chai Tea) and its ancestors (Beverages and All Categories).

Ilustración 19: La ruta de navegación usa ahora el proveedor de mapa de sitio personalizado NorthwindSiteMapProvider (haga clic para ver la imagen de tamaño completo)

Otros elementos de la interfaz de usuario de navegación se pueden usar además de SiteMapPath, como los controles Menu y TreeView. Las páginas Default.aspx, ProductsByCategory.aspx y ProductDetails.aspx de la descarga de este tutorial, por ejemplo, todas incluyen controles de menú (vea la Ilustración 20). Consulte las Características sofisticadas de navegación del sitio de ASP.NET 2.0 y la Uso de controles de navegación del sitio de las Guías de inicio rápido de ASP.NET 2.0 para obtener una visión más detallada de los controles de navegación y el sistema de mapa de sitio en ASP.NET 2.0.

The Menu Control Lists Each of the Categories and Products

Ilustración 20: El control de menú enumera cada una de las categorías y productos (haga clic para ver la imagen de tamaño completo)

Como se mencionó anteriormente en este tutorial, se puede acceder a la estructura del mapa del sitio mediante programación a través de la clase SiteMap. El código siguiente devuelve la raíz SiteMapNode del proveedor predeterminado:

SiteMapNode root = SiteMap.RootNode;

Dado que AspNetXmlSiteMapProvider es el proveedor predeterminado de nuestra aplicación, el código anterior devolvería el nodo raíz definido en Web.sitemap. Para hacer referencia a un proveedor de mapa de sitio distinto del valor predeterminado, use la propiedad Providers de la clase SiteMap de la siguiente manera:

SiteMapNode root = SiteMap.Providers["name"].RootNode;

Donde name es el nombre del proveedor de mapa de sitio personalizado ( Northwind, para nuestra aplicación web).

Para acceder a un miembro específico de un proveedor de mapa de sitio, use SiteMap.Providers["name"] para recuperar la instancia del proveedor y, a continuación, convertirlo al tipo adecuado. Por ejemplo, para mostrar la propiedad CachedDate de NorthwindSiteMapProvider en una página de ASP.NET, use el código siguiente:

NorthwindSiteMapProvider customProvider = 
    SiteMap.Providers["Northwind"] as NorthwindSiteMapProvider;
if (customProvider != null)
{
    DateTime? lastCachedDate = customProvider.CachedDate;
    if (lastCachedDate != null)
        LabelID.Text = "Site map cached on: " + lastCachedDate.Value.ToString();
    else
        LabelID.Text = "The site map is being reconstructed!";
}

Nota:

Asegúrese de probar la característica de dependencia de caché de SQL. Después de visitar las páginas de Default.aspx, ProductsByCategory.aspx y ProductDetails.aspx, vaya a uno de los tutoriales de la sección Edición, Inserción y Eliminación y edite el nombre de una categoría o producto. A continuación, vuelva a una de las páginas de la carpeta SiteMapProvider. Suponiendo que haya transcurrido suficiente tiempo para que el mecanismo de sondeo observe el cambio en la base de datos subyacente, el mapa del sitio debe actualizarse para mostrar el nuevo nombre de producto o categoría.

Resumen

Entre las características de mapa de sitio de ASP.NET 2.0 se incluye una clase SiteMap, una serie de controles web de navegación integrados y un proveedor de mapa de sitio predeterminado que espera que la información del mapa de sitio se conserve en un archivo XML. Para usar información de mapa de sitio de algún otro origen, como desde una base de datos, la arquitectura de la aplicación o un servicio web remoto, es necesario crear un proveedor de mapa de sitio personalizado. Esto implica la creación de una clase que deriva, directa o indirectamente, de la clase SiteMapProvider.

En este tutorial hemos visto cómo crear un proveedor de mapa de sitio personalizado que basa el mapa de sitio en la información de producto y categoría que se ha eliminado de la arquitectura de la aplicación. Nuestro proveedor extendió la clase StaticSiteMapProvider e implicaba crear un método BuildSiteMap que recuperase los datos, construyera la jerarquía del mapa de sitio y almacenara en caché la estructura resultante en una variable de nivel de clase. Usamos una dependencia de caché de SQL con una función de devolución de llamada para invalidar la estructura almacenada en caché cuando se modifican los datos Categories o Products subyacentes.

¡Feliz programación!

Lecturas adicionales

Para obtener más información sobre los temas tratados en este tutorial, consulte los siguientes recursos:

Acerca del autor

Scott Mitchell, autor de siete libros de ASP/ASP.NET y fundador de 4GuysFromRolla.com, ha estado trabajando con tecnologías web de Microsoft desde 1998. Scott trabaja como consultor independiente, entrenador y escritor. Su último libro es Sams Teach Yourself ASP.NET 2.0 en 24 Horas. Puede ponerse en contacto con él vía mitchell@4GuysFromRolla.com. o a través de su blog, que se puede encontrar en http://ScottOnWriting.NET.

Agradecimientos especiales a

Esta serie de tutoriales fue revisada por muchos revisores de gran ayuda. Los revisores principales de este tutorial fueron Dave Bernard, Zack Jones, Teresa Murphy y Bernadette Leigh. ¿Le interesa revisar mis próximos artículos de MSDN? Si es así, escríbame a mitchell@4GuysFromRolla.com.