Andri Yadi

A geeky technopreneur, trying to do something big with his startup

SQL Server-based SiteMap Provider

Menanggapi pertanyaan di Milist dotnet (http://dotnet.netindonesia.net/?0::3593) tentang bagaimana membuat menu yang dynamically generated from database, rasanya gw perlu menulis posting ini. Mungkin ini topik lama, tapi demi mendokumentasikan knowledge, nggak ada salahnya gw posting di sini, ya gak?

Jika yang dimaksud pertanyaan pada milist tsb adalah pemenuan untuk ASP.NET, bisa bertumpu pada control <asp:Menu> dengan attribut DataSourceID di-set dengan ID dari control <asp:SiteMapDataSource>. Kemudian, kita harus men-set attribut SiteMapProvider dari control SiteMapDataSource tsb dengan sebuah provider yang memungkinkan untuk meng-query element-element menu dari sebuah table di database. As far as I know, provider seperti itu belum ada di ASP.NET, so we need to create it.

Ok, lets get started. In this case I use SQL Server database.

1. Create a table and name it with whatever, such as: Sitemap

image

Then, insert some records, like this:

image

Please note the menu hierarchy. For example: menu with title Live Video is the child of Video menu because its PARENT is set to Video menu SITEMAP_ID

 

2. Create provider class, name it SqlSiteMapProvider.cs, and place it inside App_Code folder. The code should be self-explained. I'll attach the code in this posting

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Runtime.CompilerServices;
using System.Security.Permissions;
using System.Web;
using System.Web.Caching;
using System.Web.Configuration;
 
/// <summary>
/// SqlSiteMapProvider
/// This class is a site map provider implementation for site map data stored in SQL Server Database.
/// This provider provides caching feature so that it will return root SiteMapNode if available in cache.
/// </summary>
 
namespace DyCode.Provider
{
    [SqlClientPermission(SecurityAction.Demand, Unrestricted = true)]
    public class SqlSiteMapProvider : StaticSiteMapProvider
    {
        private const string ErrMsg1 = "Missing node ID";
        private const string ErrMsg2 = "Duplicate node ID";
        private const string ErrMsg3 = "Missing parent ID";
        private const string ErrMsg4 = "Invalid parent ID";
        private const string ErrMsg5 = "Empty or missing connectionStringName";
        private const string ErrMsg6 = "Missing connection string";
        private const string ErrMsg7 = "Empty connection string";
 
        public const string CacheDependencyName = "__SiteMapCacheDependency";
 
        private string m_ConnectionString;   // Database connection string
        private string m_TableName = "sys_sitemap";     // TableName that store site map
        private string m_CacheKey = "SQLSiteMapProvider_Nodes";
        private bool m_CacheEnabled = false;
        private int m_IndexID, m_IndexTitle, m_IndexUrl, m_IndexDesc, m_IndexRoles, m_IndexParent;
        private Dictionary<int, SiteMapNode> m_NodesDictionary = new Dictionary<int, SiteMapNode>(16);
        private readonly object m_Lock = new object();
        private SiteMapNode m_RootNode;
 
        public override void Initialize(string name, NameValueCollection config)
        {
            // Verify that config isn't null
            if (config == null)
                throw new ArgumentNullException("config");
 
            // Assign the provider a default name if it doesn't have one
            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 Server site map provider");
            }
 
            // Call the base class's Initialize method
            base.Initialize(name, config);
 
            // Initialize m_ConnectionString
            string connect = config["connectionStringName"];
            if (String.IsNullOrEmpty(connect))
                throw new ProviderException(ErrMsg5);
            config.Remove("connectionStringName");
 
            if (WebConfigurationManager.ConnectionStrings[connect] == null)
                throw new ProviderException(ErrMsg6);
 
            m_ConnectionString = WebConfigurationManager.ConnectionStrings[connect].ConnectionString;
            if (String.IsNullOrEmpty(m_ConnectionString))
                throw new ProviderException(ErrMsg7);
 
            // Initialize Table Name
            string tableName = config["tableName"];
            if (!String.IsNullOrEmpty(tableName))
            {
                m_TableName = tableName;
            }
            config.Remove("tableName");
 
            // Initialize Cache Enabled/Disabled
            string cacheEnabled = config["cacheEnabled"];
            if (!String.IsNullOrEmpty(cacheEnabled))
            {
                m_CacheEnabled = Convert.ToBoolean(cacheEnabled);
            }
            config.Remove("cacheEnabled");
 
            // Initialize Cache Key
            string cacheKey = config["cacheKey"];
            if (!String.IsNullOrEmpty(cacheKey))
            {
                m_CacheKey = cacheKey;
            }
            config.Remove("cacheKey");
 
            // SiteMapProvider processes the securityTrimmingEnabled
            // attribute but fails to remove it. Remove it now so we can
            // check for unrecognized configuration attributes.
 
            if (config["securityTrimmingEnabled"] != null)
                config.Remove("securityTrimmingEnabled");
 
            // 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);
            }
        }
 
        [MethodImpl(MethodImplOptions.Synchronized)]
        public override SiteMapNode BuildSiteMap()
        {
            lock (m_Lock)
            {
                SqlConnection connection = new SqlConnection(m_ConnectionString);
                SqlCommand command = null;
 
                if (m_CacheEnabled)
                {
                    SiteMapNode rootNode = (SiteMapNode)HttpRuntime.Cache.Get(m_CacheKey);
                    if (rootNode != null)
                    {
                        //check dependency
                        int count = 0;
                        try
                        {
                            command = new SqlCommand("SELECT COUNT(*) FROM " + this.m_TableName, connection);
                            command.CommandType = CommandType.Text;
                            connection.Open();
                            count = Convert.ToInt32(command.ExecuteScalar());
                        }
                        finally
                        {
                            connection.Close();
                        }
 
                        int siteMapNodeCount = Convert.ToInt32(HttpRuntime.Cache.Get(CacheDependencyName));
 
                        if (count != siteMapNodeCount)
                        {
                            //HttpContext.Current.Response.Write("Remove cache dependency<br/>");
                            //If table records that store sitemap are changed,
                            //remove the cache item so that OnSiteMapChanged event is triggered
                            HttpRuntime.Cache.Remove(CacheDependencyName);
                        }
                        else
                        {
                            //HttpContext.Current.Response.Write("Return SiteMapNode from cache<br/>");
                            return (SiteMapNode)HttpRuntime.Cache.Get(m_CacheKey);
                        }
                    }
                }
 
                // Make sure site map is cleared if it exists before continue             
                if (m_RootNode != null)
                {
                    ClearSiteMap();
                }
 
                // Query the database for site map nodes            
                try
                {
                    command = new SqlCommand("SELECT SITEMAP_ID, TITLE, DESCRIPTION, URL, ROLES, PARENT FROM " + this.m_TableName + " ORDER BY SITEMAP_ID", connection);
                    command.CommandType = CommandType.Text;
                    connection.Open();
 
                    SqlDataReader reader = command.ExecuteReader();
                    m_IndexID = reader.GetOrdinal("SITEMAP_ID");
                    m_IndexUrl = reader.GetOrdinal("URL");
                    m_IndexTitle = reader.GetOrdinal("TITLE");
                    m_IndexDesc = reader.GetOrdinal("DESCRIPTION");
                    m_IndexRoles = reader.GetOrdinal("ROLES");
                    m_IndexParent = reader.GetOrdinal("PARENT");
 
                    if (reader.Read())
                    {
                        // Create the root SiteMapNode and add it to the site map
                        m_RootNode = CreateSiteMapNodeFromDataReader(reader);
                        AddNode(m_RootNode, null);
 
                        // Build a tree of SiteMapNodes underneath the root node
                        int counter = 1;
                        while (reader.Read())
                        {
                            // Create another site map node and add it to the site map
                            SiteMapNode node = CreateSiteMapNodeFromDataReader(reader);
                            AddNode(node, GetParentNodeFromDataReader(reader));
                            counter++;
                        }
 
                        if (m_CacheEnabled && (HttpRuntime.Cache.Get(m_CacheKey) == null))
                        {
                            //Set a value for the cache entry that will serve as the 
                            //key for the dependency to be created on
                            HttpRuntime.Cache[CacheDependencyName] = counter;
 
                            //Create the array of cache key item names
                            string[] keys = new String[1];
                            keys[0] = CacheDependencyName;
 
                            //Create a dependency object referencing the array of cachekeys (keys)
                            CacheDependency dependency = new CacheDependency(null, keys);
 
                            HttpRuntime.Cache.Insert(m_CacheKey, m_RootNode, dependency,
                                                     Cache.NoAbsoluteExpiration,
                                                     Cache.NoSlidingExpiration,
                                                     CacheItemPriority.NotRemovable,
                                                     new CacheItemRemovedCallback(OnSiteMapChanged));
                        }
 
                    }
                }
                finally
                {
                    connection.Close();
                }
 
                // Return the root SiteMapNode
                return m_RootNode;
            }
        }
 
        private void ClearSiteMap()
        {
            Clear();
            m_NodesDictionary.Clear();
            m_RootNode = null;
        }
 
        protected override SiteMapNode GetRootNodeCore()
        {
            lock (m_Lock)
            {
                BuildSiteMap();
                return m_RootNode;
            }
        }
 
        // Helper methods
        private SiteMapNode CreateSiteMapNodeFromDataReader(DbDataReader reader)
        {
            // Make sure the node ID is present
            if (reader.IsDBNull(m_IndexID))
                throw new ProviderException(ErrMsg1);
 
            // Get the node ID from the DataReader
            int id = reader.GetInt32(m_IndexID);
 
            // Make sure the node ID is unique
            if (m_NodesDictionary.ContainsKey(id))
                throw new ProviderException(ErrMsg2 + "; Title: " + reader.GetString(m_IndexTitle));
 
            // Get title, URL, description, and roles from the DataReader
            string title = reader.IsDBNull(m_IndexTitle) ? null : reader.GetString(m_IndexTitle).Trim();
            string url = reader.IsDBNull(m_IndexUrl) ? null : reader.GetString(m_IndexUrl).Trim();
            string description = reader.IsDBNull(m_IndexDesc) ? null : reader.GetString(m_IndexDesc).Trim();
            string roles = reader.IsDBNull(m_IndexRoles) ? null : reader.GetString(m_IndexRoles).Trim();
 
            // If roles were specified, turn the list into a string array
            string[] rolelist = null;
            if (!String.IsNullOrEmpty(roles))
                rolelist = roles.Split(new char[] { ',', ';' }, 4000);
 
            // Create a SiteMapNode
            SiteMapNode node = new SiteMapNode(this, id.ToString(), url, title, description, rolelist, null, null, null);
 
            // Record the node in the m_NodesDictionary dictionary
            m_NodesDictionary.Add(id, node);
 
            // Return the node        
            return node;
        }
 
        private SiteMapNode GetParentNodeFromDataReader(DbDataReader reader)
        {
            // Make sure the parent ID is present
            if (reader.IsDBNull(m_IndexParent))
                throw new ProviderException(ErrMsg3 + "; Title: " + reader.GetString(m_IndexTitle));
 
            // Get the parent ID from the DataReader
            int pid = reader.GetInt32(m_IndexParent);
 
            // Make sure the parent ID is valid
            if (!m_NodesDictionary.ContainsKey(pid))
                throw new ProviderException(ErrMsg4 + "; Title: " + reader.GetString(m_IndexTitle));
 
            // Return the parent SiteMapNode
            return m_NodesDictionary[pid];
        }
 
        public void OnSiteMapChanged(string key, object item, CacheItemRemovedReason reason)
        {
            lock (m_Lock)
            {
                if (key == CacheDependencyName && reason == CacheItemRemovedReason.DependencyChanged)
                {
                    // Clear the site map
                    ClearSiteMap();
                }
            }
        }
    }
}

 

3. Create some configurations in web.config, like this:

3.1. Make sure you have a connection string to be used to connect to database that contains the Sitemap table.

<connectionStrings>
    <add name="SqlConnectionString" 
connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=YOURDB;Integrated Security=True"/>
</connectionStrings>

 

3.2. Add SiteMap tag inside <system.web> tag

<siteMap enabled="true" defaultProvider="AspNetSqlSiteMapProvider">
      <providers>
        <add    name="AspNetSqlSiteMapProvider" 
                type="DyCode.Provider.SqlSiteMapProvider" 
                securityTrimmingEnabled="true" 
                connectionStringName="SqlConnectionString" 
                tableName="Sitemap" 
                cacheEnabled="true" 
                cacheKey="MemberSiteMapProvider_Cache"/>
      </providers>
    </siteMap>

If you notice, tableName attribute above is set with table name that we have created previously.

 

4. In the page that will display menu, write this code:

<asp:Menu id="SiteMenu" runat="server" DataSourceID="SiteMapDataSource1" 
Orientation="Horizontal"/>
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" 
SiteMapProvider="AspNetSqlSiteMapProvider" ShowStartingNode="false" />

Please note that, SiteMapProvider attribute of SiteMapDataSource control is set with the name of provider that we have registered in web.config at step 3.

 

5. Test it

image

 

This provider is what we DyCoders always use in many projects and no known problem so far.

Some advantages of this provider are:

  1. Most of settings are configurable, like table name that stores menu data
  2. Cached. So it will not query menu data to db every time the menu displayed.

In order to use other database, you should change all SQL Server-related stuffs (used ADO.NET classes, SQL statements, and created table) to desired database. You can adapt this code to use OR Mapping technology so database independency can be achieved.

That's it. Enjoy. Please refer to attachment to get the complete code.

Share this post: | | | |
Published Jun 13 2008, 01:49 PM by andriyadi
Filed under:

Comments

 

agung said:

How if there is more than NULL value in parent field ? For example, if Home, Member, and Video in the same level ?

Try to change parent field to NULL for Member and Video. An error happened.

July 8, 2008 12:19 PM
 

andriyadi said:

yes, not yet validated...thx for correction.

July 8, 2008 12:56 PM
 

Deepak said:

when i implement the same the following error prompting.Please help me out.

Could not load type 'DyCode.Provider.SqlSiteMapProvider'.

Regards

Deepak.

July 23, 2008 2:33 PM
 

Deepak said:

Is it will work in .Net 3.5 or later version also work.

Regards

Deepak.

July 23, 2008 2:47 PM
 

nagesh said:

thank you very much nice article it helped me

March 18, 2009 7:13 PM