Agora que o ASP .NET 2.0 está pronto para distribuição, parece apropriado abordar um assunto que está no topo da lista das novas funcionalidades desejadas pelos desenvolvedores: o provedor de mapa de sites (site map provider) SQL o Server.

Como você provavelmente já sabe, o ASP .NET 2.0 simplifica imensamente o processo de construir interfaces de navegação em sites orientados a dados. Você constrói um mapa do site, coloca um controle SiteMapDataSource na página e o vincula a um controle Menu ou TreeView. O SiteMapDataSource usa o provedor padrão de mapa site (normalmente o XmlSiteMapProvider) para ler o mapa do site, e então passa os nodos do mapa para o Menu ou TreeView, que produzem os nodos em HTML. Você pode ainda adicionar um controle SiteMapPath à página. O SiteMapPath exibe um elemento que mostra o caminho para a página atual. A Figura 1 mostra os componentes do subsistema de navegação de site e ilustra como se relacionam.

 

Imagem 

Figura 1. Sistema de Navegação

A única desvantagem para a navegação de site, é que o XmlSiteMapProvider é o único provedor de mapa de site incluído no ASP .NET 2.0, o que significa que os mapas de site devem ser armazenados em arquivos XML. Antes mesmo de que o ASP .NET 2.0 fosse distribuído, os desenvolvedores clamavam por um meio de armazenar mapas de site em bancos de dados.

Em junho de 2005 a empresa Wicked Code (msdn.microsoft.com/msdnmag/issues/05/06/WickedCode/), apresentou uma solução na forma de um provedor de mapa de sites personalizado, chamado SqlSiteMapProvider. Diferentemente do XmlSiteMapProvider, o SqlSiteMapProvider lê mapas de site de bancos de dados SQL Server. E tira proveito da arquitetura de provedor do ASP .NET 2.0, para integrar-se completamente com o subsistema de navegação do site. Apenas criamos o banco de dados do mapa do site e registramos o SqlSiteMapProvider como o provedor padrão, e então, como em um passe de mágica, todo o resto passa a funcionar.

 

Então, por que desejamos rever um assunto que já foi abordado antes? Por três razões: em primeiro lugar, depois de passar a maior parte do verão analisando em profundidade a arquitetura do provedor, percebi que minha implementação do SqlSiteMapProvider original, precisava de algumas melhorias para ficar mais consistente com o XmlSiteMapProvider embutido; em segundo lugar, o ASP .NET 2.0 teve algumas alterações significativas entre as versões Beta 2 e RTM, e quis atualizar o SqlSiteMapProvider para a nova plataforma sendo distribuída; e finalmente; e muito importantemente; quis adicionar uma funcionalidade que vários leitores me solicitaram via e-mail: a recarga automática do mapa do site, após uma alteração do mesmo no banco de dados. Sem esta funcionalidade, parece que muitos leitores consideram um mapa de site armazenado em SQL Server, tão útil quanto um avião sem asas — uma coisa que descobri este verão, quando meu avião rádio-controlado favorito, cortou uma cerca. Mas essa é uma história para outra ocasião.

O Novo e Melhorado Provedor de Mapa de Sites SQL

A vantagem desta nova e melhorada versão do SqlSiteMapProvider, é que atende a todos meus objetivos e ainda mais. Sua arquitetura é consistente com os provedores embutidos; compila e roda no ASP .NET 2.0; e usa a classe SqlCacheDependency do ASP .NET 2.0, para monitorar o banco de dados do mapa de site e refrescá-lo caso aconteçam alterações no mesmo. O XmlSiteMapProvider tem uma funcionalidade semelhante que recarrega o mapa do site caso o haja alterações no arquivo subjacente XML.

A Listagem 1 mostra o código fonte para o novo SqlSiteMapProvider.

 

Listagem 1. SQL Site Map Provider

 

 [SqlClientPermission(SecurityAction.Demand, Unrestricted=true)]

public class SqlSiteMapProvider : StaticSiteMapProvider

{

    ... // static error messages omitted

 

    const string _cacheDependencyName = "__SiteMapCacheDependency";

 

    private string _connect;

    private string _database, _table;

    private bool _2005dependency = false;

    private int _indexID, _indexTitle, _indexUrl, _indexDesc,

        indexRoles, _indexParent;

    private Dictionary _nodes =

        new Dictionary(16);

    private SiteMapNode _root;

    private readonly object _lock = new object();

 

    public override void Initialize (

        string name, NameValueCollection config)

    {

        // Verify parameters

        if (config == null) throw new ArgumentNullException("config");

        if (String.IsNullOrEmpty(name)) name = "SqlSiteMapProvider";

 

        // Add a default "description" attribute to config if the

        // attribute doesn't exist or is empty

        if (string.IsNullOrEmpty(config["description"]))

        {

            config.Remove("description");

            config.Add("description", "SQL site map provider");

        }

 

        // Call the base class's Initialize method

        base.Initialize(name, config);

 

        // Initialize _connect

        string connect = config["connectionStringName"];

        if (String.IsNullOrEmpty(connect))

            throw new ProviderException(_errmsg5);

        config.Remove("connectionStringName");

 

        if (WebConfigurationManager.ConnectionStrings[connect] == null)

            throw new ProviderException(_errmsg6);

        _connect = WebConfigurationManager.ConnectionStrings[

            connect].ConnectionString;

        if (String.IsNullOrEmpty(_connect))

            throw new ProviderException(_errmsg7);

       

        // Initialize SQL cache dependency info

        string dependency = config["sqlCacheDependency"];

 

        if (!String.IsNullOrEmpty(dependency))

        {

            if (String.Equals(dependency, "CommandNotification",

                StringComparison.InvariantCultureIgnoreCase))

            {

                SqlDependency.Start(_connect);

                _2005dependency = true;

            }

            else

            {

                // If not "CommandNotification", then extract

                // database and table names

                string[] info = dependency.Split(new char[] { ':' });

                if (info.Length != 2)

                    throw new ProviderException(_errmsg8);

                _database = info[0];

                _table = info[1];

            }

 

            config.Remove("sqlCacheDependency");

        }

       

        // Throw an exception if unrecognized attributes remain

        if (config.Count > 0)

        {

            string attr = config.GetKey(0);

            if (!String.IsNullOrEmpty(attr))

                throw new ProviderException(

                    "Unrecognized attribute: " + attr);

        }

    }

 

    public override SiteMapNode BuildSiteMap()

    {

        lock (_lock)

        {

            // Return immediately if this method has been called before

            if (_root != null) return _root;

 

            // Query the database for site map nodes

            using(SqlConnection connection = new SqlConnection(_connect))

            {

                SqlCommand command = new SqlCommand(

                    "proc_GetSiteMap", connection);

                command.CommandType = CommandType.StoredProcedure;

 

                // Create a SQL cache dependency if requested

                SqlCacheDependency dependency = null;

                if (_2005dependency)

                    dependency = new SqlCacheDependency(command);

                else if (!String.IsNullOrEmpty(_database) &&

                         !String.IsNullOrEmpty(_table))

                    dependency = new SqlCacheDependency(_database,

                        _table);

 

                connection.Open();

                SqlDataReader reader = command.ExecuteReader();

                _indexID = reader.GetOrdinal("ID");

                _indexUrl = reader.GetOrdinal("Url");

                _indexTitle = reader.GetOrdinal("Title");

                _indexDesc = reader.GetOrdinal("Description");

                _indexRoles = reader.GetOrdinal("Roles");

                _indexParent = reader.GetOrdinal("Parent");

 

                if (reader.Read())

                {

                    // Create the root SiteMapNode and add it to site map

                    _root = CreateSiteMapNodeFromDataReader(reader);

                    AddNode(_root, null);

 

                    // Build a tree of SiteMapNodes under the root node

                    while (reader.Read())

                    {

                        // Create another site map node and add it

                        AddNode(CreateSiteMapNodeFromDataReader(reader),

                            GetParentNodeFromDataReader(reader));

                    }

 

                    // Use the SQL cache dependency

                    if (dependency != null)

                    {

                        HttpRuntime.Cache.Insert(_cacheDependencyName,

                            new object(), dependency,

                            Cache.NoAbsoluteExpiration,

                            Cache.NoSlidingExpiration,

                            CacheItemPriority.NotRemovable,

                            new CacheItemRemovedCallback(

                              OnSiteMapChanged));

                    }

                }

            }

 

            // Return the root SiteMapNode

            return _root;

        }

    }

 

    protected override SiteMapNode GetRootNodeCore ()

    {

        return BuildSiteMap();

    }

 

    ... // Helper methods CreateSiteMapNodeFromDataReader and

        // GetParentNodeFromDataReader

}

 

O método Inicialize, que está presente em todos os provedores, é um método especial que o ASP .NET chama depois de carregar o provedor. O ASP .NET passa ao Inicialize para um NameValueCollection chamado config, que contém todas as atribuções de configuração (e os seus valores), achados no elemento de configuração que registrou o provedor. A função do método Inicialize, é aplicar as configurações e fazer tudo o que for necessário para inicializar o provedor. O método Inicialize do SqlSiteMapProvider executa as seguintes tarefas:

Exige que o SqlClientPermission se certifique que tem permissão para acessar o banco de dados. Sem esta permissão, o SqlSiteMapProvider não poderá operar.

Chama a classe base do método Inicialize que, entre outras coisas, processa o atributo de configuração securityTrimmingEnabled, caso exista.

 

Processa o connectionStringName e os atributos de configuração do sqlCacheDependency, caso exista.

Levanta uma exceção, caso o elemento que registra o provedor contiver atributos de configuração não reconhecidos.

O atributo do sqlCacheDependency é o que permite tirar proveito da capacidade do SqlSiteMapProvider de refrescar o mapa do site, quando houver alterações no banco de dados subjacente. Ao configurar o SqlCacheDependency para "SiteMapDatabase:SiteMap", instruímos o provedor para refrescar o mapa do site, caso haja alterações em uma tabela chamada SiteMap, em um banco de dados SQL Server 7.0 ou SQL Server 2000 ("SiteMapDatabase" especifica o nome do banco de dados indiretamente, recorrendo a uma entrada na seção <DATABASE> da seção de configuração <SQLCACHEDEPENDENCY>). Se o mapa do site reside em um banco de dados SQL Server 2005, configuramos o sqlCacheDependency igual a "CommandNotification". Esta é a visão de alto nível; vermos a seguir a visão detalhada.

O núcleo do SqlSiteMapProvider é o método BuildSiteMap. Este método é chamado em algum momento pelo ASP .NET, depois que o provedor foi carregado para construir o mapa do site, que é simplesmente uma coleção de SiteMapNodes vinculados para formar uma árvore.

 

Cada SiteMapNode representa um nodo no mapa do site, e é distinguido pelas seguintes propriedades: Title, que especifica o texto que o controle de navegação exibe para o nodo; Url que especifica a URL que é enviada para o usuário quando o nodo for clicado; Description, que especifica o texto descritivo que é exibido quando o cursor passa por cima do nodo; e Roles, que especifica o papel ou papéis que são permitidos ao nodo ver, caso o security trimming for habilitado ("*" se qualquer um pode ver isto). Podem ser especificados papéis múltiplos usando vírgulas ou ponto e vírgulas como separadores.

A implementação do SqlSiteMapProvider do BuildSiteMap, consulta o banco de dados do mapa do site. A seguir, percorre todos os registros e os transforma em SiteMapNodes. Finalmente, entrega o mapa do site para o ASP .NET, retornando uma referência para o nodo raiz do mapa do site. E desde que todo código de provedor fora do método Inicialize deve ser thread-safe, o SqlSiteMapProvider empacota todo o conteúdo do BuildSiteMap, em um lock statement, para serializar acessos de thread simultâneos.

 

Além de consultar o banco de dados e construir o mapa do site, o BuildSiteMap cria também a infra-estrutura base que permite ao SqlSiteMapProvider refrescar o mapa do site, caso haja alterações no banco de dados do mesmo. Se o elemento de configuração que registrou o provedor contiver um atributo sqlCacheDependency = "CommandNotification", o BuildSiteMap cria um objeto SQL Server compatível com o SqlCacheDependency 2005, que empacota o SqlCommand usado para consultar o banco de dados do mapa do site:

 

// In Initialize

SqlDependency.Start(_connect);

 

// In BuildSiteMap

dependency = new SqlCacheDependency(command);

 

Por outro lado, se o elemento de configuração contiver o tipo de string de configuração sqlCacheDependency usado no SQL Server 7.0 ou no SQL Server 2000, (por exemplo, "SiteMapDatabase:SiteMap"), o BuildSiteMap cria um objeto SqlCacheDependency que empacota o nome do banco de dados provido e o nome da tabela:

 

// Initialize

_database = info[0];

_table = info[1];

 

// BuildSiteMap

dependency = new SqlCacheDependency(_database, _table);

 

Indiferentemente de qual tipo de SqlCacheDependency for criado, o BuildSiteMap insere posteriormente um objeto simples no cache da aplicação ASP .NET, e cria uma dependência entre aquele objeto e o banco de dados, via inclusão do SqlCacheDependency na chamada a Cache.Insert:

 

if (dependency != null)

{

    HttpRuntime.Cache.Insert(_cacheDependencyName,

        new object(), dependency,

        Cache.NoAbsoluteExpiration,

        Cache.NoSlidingExpiration,

        CacheItemPriority.NotRemovable,

        new CacheItemRemovedCallback(OnSiteMapChanged));

}

 

O parâmetro final Cache.Insert, instrui o ASP .NET para que chame o método OnSiteMapChanged do provedor, caso o SqlCacheDependency dispare um cache removal — caso haja alterações no banco de dados do mapa do site. O OnSiteMapChanged esvazia o mapa do site antigo, e chama o BuildSiteMap para construir um novo.

Pode parecer estranho que o SqlSiteMapProvider use o cache da aplicação ASP .NET, não havendo realmente nada para colocar no cache (afinal de contas, o objeto inserido no cache é apenas um marcador que não contém nenhum dado significativo), mas fazendo deste modo, permitimos que o SqlSiteMapProvider, aproveite uma funcionalidade fundamental do ASP .NET 2.0.

 

O ADO.NET 2.0 tem uma classe SqlDependency, que permite que o código da aplicação, consulte o banco de dados SQL Server 2005, e receba callbacks caso os dados subjacentes tenham sido alterados, mas não existe nenhuma funcionalidade semelhante no SQL Server 7.0 ou no SQL Server 2000. A classe SqlCacheDependency do ASP .NET 2.0, no entanto, funciona tanto com o SQL Server 7.0, quanto com o SQL Server 2000 e o SQL Server 2005. Colocar um objeto marcador acompanhado por um SqlCacheDependency no cachê, e registrar o cache removals callbacks, é um modo conveniente de tirar proveito dos experts extra contidos no SqlCacheDependency. Se o banco de dados subjacente mudar, o objeto marcador é removido do cache e o método callback é chamado, podendo ser realizada qualquer ação julgada apropriada — em este caso, refrescar o mapa do site.

 

Você pode distribuir o SqlSiteMapProvider copiando SqlSiteMapProvider.cs para a pasta App_Code do Web site (no ASP .NET 2.0, o arquivo do código fonte contido neste diretório, é compilado automaticamente). Uma vez que o provedor for distribuído, você precisa registrá-lo e tornar padrão o provedor do mapa do site. Se você quiser usar a funcionalidade de dependência de SQL cache, tem que configurá-la também. O modo como será feita a configuração, depende de se o mapa do site será armazenado em um banco de dados SQL Server 7.0, em um SQL Server 2000 ou em um SQL Server 2005.

Usando o SqlSiteMapProvider

O arquivo web.config na Listagem 2, mostra como configurar o SqlSiteMapProvider para usar as dependências de cache SQL, caso o mapa do site for armazenado em uma tabela chamada SiteMap, em um banco de dados SQL Server 7.0 ou SQL Server 2000.

 

Listagem 2. Cache

<CONFIGURATION>

Imagem   

 

A seção <CONNECTIONSTRINGS>, define o string de conexão chamada SiteMapConnectionString, que identifica o banco de dados. O provedor a usa para consultar o banco de dados e monitorar as alterações nesta parte do mapa do site. Obviamente, você precisará substituir a elipsis ("…") por um string de conexão real.

A seção <SITEMAP> registra o SqlSiteMapProvider, e o torna o provedor padrão do mapa do site. Também inclui um atributo sqlCacheDependency, que identifica o banco de dados e a tabela onde as informações do mapa do site foram armazenadas. A presença deste atributo instrui o SqlSiteMapProvider, para criar um SQLCacheDependency de monitoração das alterações no banco de dados do mapa do site; se você quiser usar o SqlSiteMapProvider sem dependências de cache, simplesmente omita o atributo sqlCacheDependency.

 

A seção <SQLCACHEDEPENDENCY>habilita as dependências de cache do SQL no ASP .NET, e fornece as informações de configuração necessárias, inclusive o intervalo de apuração que especifica com que freqüência o ASP .NET deve verificar a ocorrência de alterações no banco de dados (neste exemplo, cada cinco segundos). O nome do banco de dados nesta seção, é mapeado para o nome do banco de dados especificado no atributo sqlCacheDependency do provedor; o nome do string de conexão mapeia para o string de conexão na seção <CONNECTIONSTRINGS>.

Para que estas configurações funcionem, é necessário preparar antes o banco de dados do mapa do site e a tabela que contém os dados do site, para suportar as dependências de cache SQL, o que demanda apenas dois minutos com o utilitário aspnet_regsql.exe, que vem com o ASP .NET 2.0. Se o banco de dados for um banco de dados local chamado SiteMapDatabase, antes deve-se rodar o seguinte comando para preparar o mesmo:

 

aspnet_regsql –S localhost –E –d SiteMapDatabase -ed

 

A seguir, se os dados do mapa do site forem armazenados na tabela SiteMap do banco de dados, rodamos o seguinte comando para preparar a tabela:

 

aspnet_regsql –S localhost –E –d SiteMapDatabase –t SiteMap -et

 

O primeiro comando acrescenta uma tabela de notificação de mudança ao banco de dados, e ao mesmo tempo armazena os procedimentos para acessar a mesma. O segundo acrescenta um gatilho de insert/update/delete à tabela SiteMap. Quando disparado, o gatilho insere uma entrada na tabela de notificação de mudança, indicando que o conteúdo da tabela SiteMap foi alterado. O ASP .NET 2.0 examina a tabela de notificação de mudanças em intervalos pré-programados, para descobrir se aconteceram alterações na tabela SiteMap e para qualquer outra tabela que for monitorada com as dependências de cache SQL.

 

Se você usar o SqlSiteMapProvider com um banco de dados SQL Server 2005, não terá que preparar o mesmo para trabalhar com as dependências de cache SQL. Nem mesmo será necessário identificar o banco de dados e a tabela que contém os dados do mapa do site; apenas habilitaremos as dependências de cache SQL com um elemento <SQLCACHEDEPENDENCY> e configuraremos o atributo sqlCacheDependency de SqlSiteMapProvider para "CommandNotification", como mostrado na Listagem 3 (também será necessário rodar o working process ASP .NET, com privilégios dbo para que as dependências de cache do SQL Server 2005 funcionem automaticamente).

 

Listagem 3. Cache

<CONFIGURATION>

Imagem

 

Estando tudo devidamente configurado, o SqlSiteMapProvider tira proveito do suporte ao SQL Server 2005 embutido no ASP .NET, empacotando um objeto SqlCacheDependency no objeto SqlCommand usado para consultar o banco de dados do mapa do site. O SqlCacheDependency, por sua vez, usa as notificações de consulta do SQL Server 2005, para receber callbacks assíncronos que indicam que os dados retornados pela consulta mudaram.

Não acontecerá nenhuma consulta e nem serão requeridos nenhuma tabela especial ou procedimentos armazenados ou gatilhos. Se precisava achar um motivo para atualizar seu banco de dados atual para o SQL Server 2005, esta funcionalidade já é compensatória para aplicações ASP .NET orientadas a dados.

Criando o Banco de dados do Mapa de site

Uma tabela do mapa do site criada para o SqlSiteMapProvider, tem que estar em conformidade com um schema predefinido que se adapte à representação de dados hierárquicos em um banco de dados relacional. O script SQL na Listagem 4, cria uma tabela deste tipo chamada SiteMap e a povoa com exemplos de nodos de mapa de site.

 

Listagem 4. SQL

CREATE TABLE [dbo].[SiteMap] (

    [ID]          [int] NOT NULL,

    [Title]       [varchar] (32),

    [Description] [varchar] (512),

    [Url]         [varchar] (512),

    [Roles]       [varchar] (512),

    [Parent]      [int]

) ON [PRIMARY]

GO

 

ALTER TABLE [dbo].[SiteMap] ADD

    CONSTRAINT [PK_SiteMap] PRIMARY KEY CLUSTERED

    (

        [ID]

    )  ON [PRIMARY]

GO

 

-- Add site map nodes

 

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)

VALUES (1, 'Home', NULL, '~/Default.aspx', NULL, NULL)

 

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)

VALUES (10, 'News', NULL, NULL, '*', 1)

 

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)

VALUES (11, 'Local', 'News from greater Seattle', '~/Summary.aspx?CategoryID=0', NULL, 10)

 

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)

VALUES (12, 'World', 'News from around the world', '~/Summary.aspx?CategoryID=2', NULL, 10)

 

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)

VALUES (20, 'Sports', NULL, NULL, '*', 1)

 

INSERT INTO SiteMap (ID, Title, Description, Url, Roles, Parent)

VALUES (21, 'Baseball', 'What''s happening in baseball', '~/Summary.aspx?CategoryID=3', NULL, 20)

 

...

 

-- Create the stored proc used to query site map nodes

 

CREATE PROCEDURE proc_GetSiteMap AS

    SELECT [ID], [Title], [Description], [Url], [Roles], [Parent]

    FROM [SiteMap] ORDER BY [ID]

GO

 

Cada registro adicionado à tabela, representa um nodo do mapa do site, e cada um destes nodos tem campos que são mapeados para propriedades SiteMapNode do mesmo nome, assim como para campos que denotam relações entre nodos. Cada nodo tem que ter um ID unívoco, que será armazenado no campo ID. Para tornar um nodo pai de outro, configuramos o campo Parent do nodo filho com o ID do nodo pai. Todos os nodos com exceção do nodo raiz, tem que ter um nodo pai. Além disto, por causa do modo em que o BuildSiteMap foi implementado, um nodo só pode ter como pai, outro nodo com um ID menor (notar a cláusula ORDER BY da consulta do banco de dados). Por exemplo, um nodo com um ID 100 pode ser o filho de um nodo com um ID 99, mas não pode ser filho de um nodo com um ID 101.

 

Por padrão, o SqlSiteMapProvider assume que a tabela onde os dados do mapa do site estão armazenados é chamado SiteMap. O SqlSiteMapProvider usa o stored procedure chamado proc_GetSiteMap para consultar os nodos do mapa do site no banco de dados e seu alvo será a tabela SiteMap. Se quiser mudar o nome da tabela do mapa do site, simplesmente mude o nome da tabela no banco de dados e no stored procedure.

 

Jeff Prosise é um editor contribuinte da Revista MSDN e autor de vários livros, incluindo Programming Microsoft .NET (Microsoft Press, 2002). É também um co-fundador da Wintellect, uma empresa de treinamento e de consultoria de software.