drupal hit counter
Jerry Huang | All posts tagged 'ASPNET'

Jerry Huang apps and developing apps

Make StaticSiteMapProvider dynamically

18. July 2010 22:18 by Jerry in Old blog posts

The term "dynamic" has 2 meanings:
1) dynamically generate menu data when user click on the menu; and
2) generate menu in per user basis.
In this paper I only discuss the second one.
 
In ASP.NET 2.0,  together with Menu or TreeView control, you can use StaticSiteMapProvider to generate a "static" hierarchical navigation menu to the end user.  Most of the StaticSiteMapProvider sub-classes look like this:
 [code language=C#]
 public class SiteMapProvider : StaticSiteMapProvider {
        private SiteMapNode root = null;
        public override SiteMapNode BuildSiteMap()
        {
            if (root != null) return root;
            lock (this)
            {
                base.Clear();
                ...generate your menu data here, e.g. get data from database
                root = new SiteMapNode(.......
            }
            return root;
        }
..............................
}//end of class[/code]
The BuildSiteMap function will run once and once only when the first visitor was opening the website, the root object will then be saved persistently and being reused during the lifetime of the web application. You may consider the root object as an application variable if you feel difficult to understand. As a result, other visitors will get exactly the same menu as the first one. That's why it named itself "Static".
 
In practice however, we are more prefer to get different menu items according to the login user's role or access right settings, rather than the stupid static items. A simple solution to make the SiteMapProvider dynamically is to discard the "root" - comment the first line of code, and that's it.
[code language=C#]        public override SiteMapNode BuildSiteMap()
        {
            //if (root != null) return root;[/code]

It works as you expected, but this is extremely inefficient; "as multiple concurrent page requests can result indirectly in multiple calls to load site map information" (i.e. to run BuildSiteMap) - the MSDN explains. Set a breakpoint inside BuildSiteMap function, you will find what MSDN said is true. Loading one single page in the web project will call BuildSiteMap many times. Alright, if that is the case, the only thing we need is a session variable to make sure every users only run the BuildSiteMap once.  The final code:

[code language=C#]public class DynamicSiteMapProvider : StaticSiteMapProvider {
//a kindly reminder that all private fields inside this class will be persistently available during the lifetime
//please remember to do extra init with these fields in the BuildSiteMap method

        private SiteMapNode root = null;
        public override SiteMapNode BuildSiteMap()
        {
            if (root != null&&HttpContext.Current.Session["AlreadySet"]!=null) return root;
            lock (this)
            {
                //next line is better to put on the first statement after "if", for concurrent reason
                HttpContext.Current.Session["AlreadySet"] = true;
                base.Clear();
                root = null;//init
                ...get "per-user" menu data here
                root = new SiteMapNode(......
 
            }
            return root;
        }
.................other code omitted....
}//end of class[/code]
 The solution above is trivial, a bit informal yet feasible. Actually SiteMapProvider has a property "securityTrimmingEnabled" to specify if the Provider equip with role. In that case, you need to override the "IsAccessibleToUser" method. More information please visit:
http://fredrik.nsquared2.com/viewpost.aspx?PostID=272&showfeedback=true
http://blogs.msdn.com/dannychen/archive/2006/03/16/553005.aspx

In order to finally solved the problem with a formal solution, we need to work with the ASP.NET 2.0 Role Manager, and Membership Provider if you want to.

First of all, define a series of roles that what menu/urls in your project they can access to, then modify your data structure to place your role information. E.g. add a "roles" field in your sitemap database table if you are using database storage. Using the following format if you are using xml file to store the sitemap:

[code language=XML] <siteMapNode title="Home" description="Home" url="~/default.aspx" roles="*">
  <siteMapNode title="menu 1" description="" roles="Admin,User" >
   <siteMapNode  roles="Admin" title="Admin Item" description="" url="~/admin.aspx" />
   <siteMapNode roles="User" title="User Item" description="" url="~/user.aspx" />
  </siteMapNode>
 </siteMapNode>[/code]

the menu will look like:
Home  (can be accessed by all users)
  - menu 1 (can be accessed by Admin and User
    - Admin Item (Can be accessed by Admin only)
    - User Item (can be accessed by User only)

*note: if you are not going to write your own xml SiteMapProvider, the ASP.NET 2.0 already has a simple one for you - System.Web.XmlSiteMapProvider. BUT, please be aware that the XmlSiteMapProvider only supports security trimming on menu/node who has child-node. In this case, if using XmlSiteMapProvider, the security trimming setting only works for "menu 1". In other word, "Admin Item" and "User Item" will be accessed by both "Admin" and "User" as "menu 1" specify.

Then, modify the Login.aspx:

[code language=C#]....After verifying the login username and password
.... and get the roleName of login user
//init the roles, clean login user's role
string[] allRoles={"Admin","User"};
foreach (string role in allRoles)
{
    if (!Roles.RoleExists(role)) Roles.CreateRole(role);
    if (Roles.IsUserInRole(user, role)) Roles.RemoveUserFromRole(user, role)
}
//add the user to the role s/he belongs to
Roles.AddUserToRole(user, roleName);[/code]

Finally, modify the BuildSiteMap method of  SiteMapProvider, and override the IsAccessibleToUser. The main difference from previous version is that in the BuildSiteMap function, instead of loading "per-user" menu data, we just load a full set of menu, leave the IsAccessibleToUser to determine if a node should be shown or access to. 

[code language=C#]public override SiteMapNode BuildSiteMap()
        {
            if (root != null) return root;

            lock (this)
            {
                base.Clear();
                ...get a full set of menu data here, but don't forget attaching role information to a node
                ... for example
                string role = .....get the roles of this menu, from database or xml, e.g "Admin,User"
                IList roles = new ArrayList();
                if (!string.IsNullOrEmpty(role)) {
                   string[] r=role.Split(",");
                   foreach (string item in r)
                       roles.Add(item)
               }
               SiteMapNode node = New SiteMapNode(this, menukey, url, title, desc, roles, null, null, null)
               ......other code omitted....
            }
            return root;
        }
public override bool IsAccessibleToUser (System.Web.HttpContext context, System.Web.SiteMapNode node)
        {
            if (!this.SecurityTrimmingEnabled) return true;
            if (node ==null || context==null || context.User==null) return false;
            if (node.Roles==null || node.Roles.Count<=0) return false;
            foreach(string role in node.Roles)
                if (role.Equals("*") || context.User.IsInRole(role))
                    return true;
            return false;
        }
 [/code]
Last step, don't forget to set SecurityTrimmingEnabled to true in web.config file.

   [code language=XML] <siteMap defaultProvider="DynamicSiteMapProvider" enabled="true">
      <providers>
        <add name="DynamicSiteMapProvider" type="myProject.DynamicSiteMapProvider"  securityTrimmingEnabled="true" />
      </providers>
    </siteMap>[/code]