SiteMapProvider

128

Чтобы внести действительно существенные изменения в работу модель навигации ASP.NET, придется создать собственный поставщик карты сайта. Необходимость в создании специального поставщика карты сайта может возникать по нескольким причинам:

При реализации специального поставщика карты сайта можно использовать одну из двух возможностей. Все поставщики карт сайтов являются производными от абстрактного базового класса SiteMapProvider в пространстве имен System.Web. Для реализации нового поставщика с нуля можно наследоваться от этого класса. Однако если требуется сохранить логику, но использовать другое хранилище данных, достаточно реализовать наследование не от этого класса, а от класса StaticSiteMapProvider. Он предоставляет базовую реализацию многих методов, включая логику хранения и поиска узлов.

В последующих разделах мы рассмотрим специальный поставщик, который позволяет хранить информацию о карте сайта в базе данных.

Хранение информации о карте сайта в базе данных

В следующем примере все навигационные ссылки хранятся в одной таблице базы. Поскольку базы данных плохо приспособлены для работы с иерархическими данными, придется прибегнуть к некоторым ухищрениям. В данном примере каждая ссылка навигации связана с родительской ссылкой в той же таблице, за исключением корневого узла. Это значит, что хотя ссылки навигации "упакованы" в одной таблице, можно воссоздать правильную структуру, начав с домашней страницы, а затем перейдя к поиску подмножества строк на каждом уровне.

На рисунке ниже показана таблица SiteMap с некоторыми образцами данных, которая грубо повторяет карту сайта, представленную в предыдущей статье:

Таблица SiteMap

В этом решении поставщик карты сайта не обращается к таблице напрямую. Вместо этого он будет использовать хранимую процедуру. Это придает решению некоторую гибкость и потенциально позволяет хранить информацию о навигации в другой схеме при условии, что хранимая процедура будет возвращать таблицу с ожидаемыми именами столбцов. В этом примере использована следующая хранимая процедура и таблица:

-- Создать таблицу SiteMap
CREATE TABLE SiteMap
(
	ID INT Identity(1,1) NOT NULL PRIMARY KEY,
	Url varchar(255) NOT NULL,
	Title nvarchar(512) NOT NULL,
	Description nvarchar(1024) NOT NULL,
	ParentID INT
)

-- Добавить данные в таблицу
INSERT INTO SiteMap (Url, Title, Description, ParentID) VALUES 
	(N'~/default.aspx', N'Главная', N'Главная страница сайта', NULL),
	(N'~/Products.aspx', N'Товары', N'Наши товары', 1),
	(N'~/Hardware.aspx', N'Комплектующие ПК', N'Процессоры, видеокарты, жесткие диски', 2),
	(N'~/Software.aspx', N'Программы', N'Visual Studio, Expression Blend, Windows Server', 2),
	(N'~/Services.aspx', N'Службы', N'Наши службы тех. поддержки, обучения и др.', 1),
	(N'~/Training.aspx', N'Обучение', N'Мы научим использовать вас наши программные продукты', 5),
	(N'~/Consulting.aspx', N'Консультации', N'Задавайте ваши вопросы', 5),
	(N'~/Support.aspx', N'Тех. поддержка', N'Помогаем 24/7', 5);
-- Создать хранимую процедуру
CREATE PROCEDURE GetSiteMap AS
SELECT * FROM SiteMap ORDER BY ParentID, Title

Создание поставщика карты сайта

Поскольку этот поставщик карты сайта не изменяет базовую логику навигации по карте сайта, он может быть производным от класса StaticSiteMapProvider, а не от SiteMapProvider, с повторной реализацией всего поведения отслеживания и навигации (что представляет собой более трудоемкую задачу). Ниже показано объявление класса для поставщика (находится в папке App_Code приложения):

public class SqlSiteMapProvider : StaticSiteMapProvider
{
	// ...
}

Прежде всего, необходимо переопределить метод Initialize(), чтобы получить всю необходимую относящуюся к сайту информацию из файла web.config. Метод Initialize() предоставляет доступ к элементу конфигурации в файле web.config и определяет поставщика карты сайта.

В данном примере поставщик нуждается в трех фрагментах информации:

  1. Строка соединения с базой данных, в которой хранятся данные карты сайта (в моем случае таблица SiteMap находится в тестовой базе данных Northwind).

  2. Имя хранимой процедуры, которая возвращает карту сайта.

  3. Имя поставщика для базы данных. Это позволяет использовать независимое от поставщик кодирование. Другими словами, SQL Server, Oracle или любую другую базу данных можно поддерживать одинаково просто, при условии, что имеется установленная фабрика поставщиков .NET.

Веб-приложение можно сконфигурировать для использования специального поставщика (SqlSiteMapProvider) и предоставить три необходимые порции информации в разделе <siteMap> файла web.config:

<configuration>
  <system.web>
    <siteMap defaultProvider="SqlSiteMapProvider">
      <providers>
        <add name="SqlSiteMapProvider" 
             type="SqlSiteMapProvider"
             connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=True"
             providerName="System.Data.SqlClient"
             storedProcedure="GetSiteMap" cacheTime="600"/>
      </providers>
    </siteMap>
  </system.web>
</configuration>

Теперь в своем поставщике нужно просто получить эти три порции информации и сохранить их для дальнейшего использования. Код, который понадобится добавить в класс SqlSiteMapProvider, имеет следующий вид:

using System;
using System.Data;
using System.Web;
using System.Data.Common;
using System.Web.Configuration;

public class SqlSiteMapProvider : StaticSiteMapProvider
{
    private string connectionString;
    private string providerName;
    private string storedProcedure;
    private int cacheTime;

    private bool initialized = false;
    public virtual bool IsInitialized
    {
        get { return initialized; }
    }

    public override void Initialize(string name, System.Collections.Specialized.NameValueCollection attributes)
    {
        if (!IsInitialized)
        {
            base.Initialize(name, attributes);

            // Извлечь настройки из файла web.config
            providerName = attributes["providerName"];
            connectionString = attributes["connectionString"];
            storedProcedure = attributes["storedProcedure"];
            cacheTime = Int32.Parse(attributes["cacheTime"]);

            if (String.IsNullOrEmpty(providerName))
                throw new ArgumentException("He найден поставщик");
            else if (String.IsNullOrEmpty(connectionString))
                throw new ArgumentException("He найдена строка подключения");
            else if (String.IsNullOrEmpty(storedProcedure))
                throw new ArgumentException("He найдена хранимая процедура");

            initialized = true;
        }
    }
    
    // ...
}

Реальную работу поставщик выполняет в методе BuildSiteMap(), который создает объекты SiteMapNode, образующие дерево навигации. Обычно во время существования приложения можно будет создать объект SiteMapNode один раз, а затем повторно его использовать множество раз. Чтобы это было возможным, поставщик должен сохранить карту сайта в памяти, либо кэшировать ее. Я выбрал второй вариант.

Корневой узел SiteMapNode содержит первый уровень узлов, который, в свою очередь, будет содержать следующий уровень узлов, и т.д. Таким образом, корневой узел является отправной точкой для всего дерева навигации.

Чтобы действительно создать карту сайта, нужно переопределить метод BuildSiteMap(). Сначала необходимо проверить, была ли уже сгенерирована карта сайта, а затем создать ее. Поскольку несколько страниц могут совместно использовать один и тот же экземпляр поставщика карты сайта, перед обновлением любой информации общего пользования (такой как хранящееся в памяти дерево навигации) целесообразно заблокировать объект:

public class SqlSiteMapProvider : StaticSiteMapProvider
{
    // ...

    public override SiteMapNode BuildSiteMap()
    {
        SiteMapNode rootNode;

        // Так как экземпляр класса может изменяться несколькими страницами, 
        // будем использовать блокировку, чтобы убедиться, что карта не
        // обновляется двумя страницами одновременно
        lock (this)
        {
            // He перестраивайте карту, если в этом нет необходимости. 
            // Если карта сайта часто изменяется, подумайте 
            // о применении кэширования, как показано в этом примере
            rootNode = HttpContext.Current.Cache["rootNode"] as SiteMapNode;

            if (rootNode == null)
            {
                // Начать с "чистого листа"
                Clear();

                // Получить все данные (с помощью кода, независимого от поставщика)
                DbProviderFactory provider = DbProviderFactories.GetFactory(providerName);

                // Использовать эту фабрику для создания соединения
                DbConnection con = provider.CreateConnection();
                con.ConnectionString = connectionString;

                // Создать команду
                DbCommand cmd = provider.CreateCommand();
                cmd.CommandText = storedProcedure;
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.Connection = con;

                // Создать DataAdapter
                DbDataAdapter adapter = provider.CreateDataAdapter();
                adapter.SelectCommand = cmd;

                // Получить, результаты в DataSet
                DataSet ds = new DataSet();
                adapter.Fill(ds, "SiteMap");
                DataTable dtSiteMap = ds.Tables["SiteMap"];

                // Создаем узлы SiteMapNode из DataSet

                // Получить корневой узел
                DataRow rowRoot = dtSiteMap.Select("ParentID IS NULL")[0];

                rootNode = new SiteMapNode(this,
                    rowRoot["Url"].ToString(), rowRoot["Url"].ToString(),
                    rowRoot["Title"].ToString(), rowRoot["Description"].ToString());
                string rootID = rowRoot["ID"].ToString();
                AddNode(rootNode);

                // Заполнить вниз по иерархии
                AddChildren(rootNode, rootID, dtSiteMap);

                HttpContext.Current.Cache.Insert("rootNode", rootNode,
                  null, DateTime.Now.AddSeconds(cacheTime), TimeSpan.Zero);
            }
        }
        return rootNode;
    }
    
    // ...
    
}

Давайте рассмотрим этот код более подробно. После извлечения корневого узла из кэша он проверяется на наличие null-ссылки, которая говорит о том, что этот объект создается в первый раз. Если это так, потребуется создать поставщика базы данных и применить его для вызова хранимой процедуры, получающей хронологию навигации. Хронология навигации хранится в объекте DataSet (объект DataReader не будет работать, поскольку для перемещения по структуре карты сайта необходима навигация назад и вперед).

После этого нужно перейти к DataTable, чтобы создать объекты SiteMapNode, начиная с корневого узла. Корневой узел можно найти посредством поиска узла, не имеющего родителя (у такого узла свойство parentID имеет значение null в таблице SiteMap). В данном примере проверка на всевозможные состояния ошибок (например, дублирование корневых узлов) не выполняется.

Теперь, чтобы создать SiteMapNode, понадобится предоставить ключ, URL-адрес, заголовок и описание. В реализации поставщика карты сайта, применяемой по умолчанию, ключ и URL-адрес совпадают, поэтому поиск по URL-адресу упрощается. Специальный поставщик SqlSiteMapProvider также использует это соглашение.

После этого нужно заполнить оставшуюся часть иерархии. Этот шаг необходимо выполнять рекурсивно, что позволит проникнуть вглубь иерархии, имеющей неограниченное количество уровней. Для этой цели поставщик SqlSiteMapProvider использует приватный метод AddChildren, который за раз заполняет один уровень. По завершении этого процесса будет возвращен корневой узел, предоставляющий доступ ко всей карте сайта.

Метод AddChildren() просто ищет в DataTable записи, в которых свойство ParentID имеет то же значение, что и текущий ID - другими словами, он находит все дочерние узлы текущего узла. Каждый раз при обнаружении дочернего узла он будет добавлять его в коллекцию SiteMapNode.ChildNodes с помощью метода AddNode(), унаследованного от класса StaticSiteMapProvider. Ниже показан полный код:

public class SqlSiteMapProvider : StaticSiteMapProvider
{
    // ...

    private void AddChildren(SiteMapNode rootNode, string rootID, DataTable dtSiteMap)
    {
        DataRow[] childRows = dtSiteMap.Select("ParentID = " + rootID);
        foreach (DataRow row in childRows)
        {
            SiteMapNode childNode = new SiteMapNode(this,
              row["Url"].ToString(), row["Url"].ToString(),
              row["Title"].ToString(), row["Description"].ToString());
            string rowID = row["ID"].ToString();

            // Использовать метод AddNode объекта SiteMapNode для добавления 
            // SiteMapNode в коллекцию ChildNodes
            AddNode(childNode, rootNode);

            // Проверить наличие дочерних узлов в этом узле
            AddChildren(childNode, rowID, dtSiteMap);
        }
    }

    // ...
}

Осталось только заполнить еще несколько обязательных перегрузок, которые получают информацию о карте сайта:

public class SqlSiteMapProvider : StaticSiteMapProvider
{
    // ...

    protected override SiteMapNode GetRootNodeCore()
    {
        return BuildSiteMap();
    }

    public override SiteMapNode RootNode
    {
        get { return BuildSiteMap(); }
    }

    protected override void Clear()
    {
        lock (this)
        {
            HttpContext.Current.Cache.Remove("rootNode");
            base.Clear();
        }
    }
}

Этот код завершает пример. Теперь можно запросить те же страницы, что были созданы ранее, с использованием нового поставщика карты сайта (конфигурация которого задана в файле web.config). По сути, используется точно такая же разметка. Специальный поставщик подключается легко и просто. Новая информация будет проходить через специальный поставщик и поступать на страницы без какого-либо намека на изменение в базовой структуре. Можно просто удалить файл Web.sitemap созданный в примере из предыдущей статьи и запустить этот пример (с той же разметкой мастер-страницы), чтобы убедиться, что узлы загружаются из базы данных, а не из фала:

Загрузка карты сайта из базы данных

Добавление сортировки

В настоящий момент поставщик SqlSiteMapProvider возвращает результаты, упорядоченные по заголовку в алфавитном порядке. Это означает, что страница "Службы" всегда будет появляться перед страницей "Товары". Для быстрой проверки такой вариант вполне приемлем, но на практике, вероятно, потребуется управлять порядком появления страниц. К счастью, существует простое решение этой задачи. По сути, для этого даже не нужно изменять код SqlSiteMapProvider. Достаточно будет добавить в таблицу SiteMap новое поле (скажем, OrdinalPosition) и изменить процедуру GetSiteMap так, чтобы она использовала это поле:

-- Изменить хранимую процедуру GetSiteMap
ALTER PROCEDURE GetSiteMap AS
SELECT * FROM SiteMap ORDER BY ParentID, OrdinalPosition, Title

Во-первых, записи сортируются по группам родительских узлов (т.е. процедура определяет, под каким узлом должны располагаться эти узлы). Затем они упорядочиваются по значениям OrdinalPosition, если они были заданы. И, наконец, они сортируются по заголовкам.

Сортировка осуществляется применительно только к тем группам страниц, которые находятся на одном и том же уровне. Например, можно использовать одни и те же порядковые числа (скажем, 1, 2, 3), чтобы упорядочить страницы в ветви Products так же, как в ветви Services. Если две страницы из одной и той же группы будут иметь одинаковый порядковый номер, их упорядочение относительно друг друга будет выполняться в алфавитном порядке по заголовку. (Побочным эффектом в этом случае будет то, что если не задать какие-то порядковые номера, узлы будут отсортированы в алфавитном порядке по заголовкам, как это было сделано в предыдущем примере.)

Строго говоря, вовсе не обязательно выполнять сортировку по идентификатору ParentID. Код SqlSiteDataMapProvider обрабатывает узлы по одной партии за раз, и каждая партия состоит из узлов, имеющих один и тот же идентификатор ParentID. Однако сортировка по идентификатору ParentID упрощает тестирование сортировки. В этом случае можно будет запустить хранимую процедуру GetSiteMap, просмотреть результаты и понять, как организованы узлы.

Добавление кэширования

Вы могли заметить, что SqlSiteMapProvider постоянно хранит корневой узел для текущей карты сайта в кэше. Если бы он хранил этот узел в памяти (например, в поле класса), это означало, что SqlSiteMapProvider использует одну и ту же карту сайта до тех пор, пока не будет перезапущен домен приложения (например, при повторном создании веб-сайта или изменении его параметров конфигурации). Если предполагается регулярное изменение карты сайта, то проверить, были ли восприняты изменения приложением, и была ли обновлена карта сайта, можно несколькими способами. Лучше всего для этой цели подходит кэширование данных, благодаря которому корневой узел будет сохраняться в течение строго определенного периода времени.

Давайте акцентируем внимание на моментах из предыдущего примера, связанных с кэшированием. Сначала мы указали временной предел срока хранения карты сайта в кэше, используя конфигурационный файл web.config (в виде значения, выраженного в секундах):

<add name="SqlSiteMapProvider" ... cacheTime="600"/>

Конструктор SqlSiteMapProvider получает его с помощью следующего оператора:

cacheTime = Int32.Parse(attributes["cacheTime"]);

В методе BuildSiteMap() первом создании карты сайта корневой узел кэшируется, а при повторном запросе извлекается из кэша:

lock (this)
{
    rootNode = HttpContext.Current.Cache["rootNode"] as SiteMapNode;

    if (rootNode == null)
    {
        // ...
        HttpContext.Current.Cache.Insert("rootNode", rootNode,
        	null, DateTime.Now.AddSeconds(cacheTime), TimeSpan.Zero);
    }
}

Чтобы написать более изощренный код, можно применить проверку кэша на основе SQL Server. Это позволит автоматически удалять карту сайта, находящуюся в кэше, если в таблице SiteMap произойдут изменения. Единственный недостаток такого подхода в том, что эта особенность является специфической для SQL Server, поэтому в случае ее применения теряются широкие возможности совместимости с базой данных, которые предлагает SqlSiteMapProvider (в настоящее время он поддерживает любой источник данных, имеющий поставщика и фабрику данных ADO.NET).

Если вы решите реализовать проверку кэша на основе базы данных, нужно будет позаботиться о том, чтобы эта функциональность была необязательной. Например, продемонстрированный здесь подход, основанный на временном кэшировании, можно применить, если только не будет найден определенный атрибут в дескрипторе специального поставщика, который покажет, что поддерживается объявление недействительным кэша базы данных.

Настройка параметров безопасности

В одной из последующих статей будет рассказано о том, как защитить определенные страницы и папки с помощью правил авторизации. Эти правила позволяют отказать в доступе к секретному содержимому анонимным пользователям, независимо от того, какой тип аутентификации используется. Правила авторизации можно использовать также для блокирования конкретных пользователей, ролей или групп Windows.

Это создает определенную проблему при использовании единой карты сайта для всех пользователей. Дело в том, что карта сайта может включать некоторые страницы, доступ к которым может быть разрешен только для некоторых классов пользователей. Например, все пользователи будут видеть ссылку на страницу Admin.aspx, даже если вы применяете правила авторизации, которые явным образом запрещают доступ к этой странице большинству пользователей. Во избежание этой путаницы можно применять часто недооцениваемое средство карты сайта, называемое настройкой параметров безопасности.

При активизации этого средства все страницы, к которым доступ должен быть запрещен (в соответствии с правилами авторизации), в любом случае исключаются из карты сайта. Это означает, что пользователи, не обладающие полномочиями администратора, не смогут видеть ссылку на страницу Admin.aspx. А если правила авторизации применяются для того, чтобы создать отдельные группы страниц для отдельных ролей, то каждый пользователь будет видеть только соответствующие страницы.

Чтобы задействовать средство настройки параметров безопасности, нужно использовать атрибут securityTrimmingEnabled при регистрации поставщика карты сайта в файле web.config. Несмотря на то что для этой цели можно было бы отредактировать корневой файл web.config на веб-сервере, проще всего добавить стандартный поставщик карты сайта с новыми параметрами конфигурации, как показано ниже:

<configuration>
  <system.web>
    <siteMap defaultProvider="SecureSqlSiteMapProvider">
      <providers>
        <add name="SecureSqlSiteMapProvider" 
             type="System.Web.XmlSiteMapProvider"
             siteMapFile="Web.sitemap"
             securityTrimmingEnabled="true" />
      </providers>
    </siteMap>
  </system.web>
</configuration>

После включения средство настройки параметров безопасности автоматически применяется ко всем узлам файла карты сайта. Тем не менее, это поведение можно изменить. Если известно, что определенные разделы карты сайта должны быть показаны всем пользователям, или если вы не хотите применять средство настройки параметров безопасности для сокрытия некоторых страниц, его можно явно отключить для некоторой части карты сайта. Для этого атрибут roles данного узла должен быть установлен в *, как показано ниже:

<siteMapNode title="Службы" description="Наши службы тех. поддержки, обучения и др."
		url="~/Services.aspx" roles="*">

Теперь узел Services будет виден всем пользователям, независимо от их роли, даже если средство настройки параметров безопасности включено. Необходимость в этом возникает по нескольким причинам:

Значение атрибута roles не передается вложенным узлам. Это означает, что если узел Administration содержит другие узлы, и эти узлы указывают на защищенные страницы, которые требуется показывать в карте сайта, в каждый такой узел нужно будет добавить атрибут roles="*".

Настройка параметров безопасности подразумевает, что в каждом запросе будет выполнена дополнительная работа. Если карта сайта содержит большое количество узлов, то дополнительная нагрузка может негативно сказаться на производительности. В Microsoft рекомендуют применять средство настройки параметров безопасности для тех карт сайта, в которых насчитывается не более 150 узлов. Другой способ обеспечения высокой производительности предусматривает отключение средства настройки параметров безопасности для тех разделов карты сайта, в которых не нужно использовать атрибут roles.

Узел roles наводит на мысль о другой, реже используемой возможности. Его можно применять для того, чтобы явно указать разделенный запятыми список ролей (или, в случае аутентификации Windows, групп Windows), которым будет разрешено видеть страницу. Однако такое использование может привести к возникновению конфликтов. Его нельзя применять для ограничения доступа к узлу; такой список просто расширяет доступ. Другими словами, когда вы явным образом включаете средство настройки параметров безопасности, ASP.NET определяет, кто должен видеть узел карты сайта, исходя из настроек авторизации для данной страницы в файле web.config. Затем ASP.NET также отображает узел для ролей, которые были заданы явно.

Следует иметь в виду, что атрибут roles показывает, будет ли отображаться узел в карте сайта. Это никоим образом не влияет на то, может ли пользователь иметь доступ к странице, что определяется правилами авторизации в файле web.config.

Пройди тесты
Лучший чат для C# программистов