Wednesday, August 25, 2010

Dynamic Treeview using XML

I've been dealing quite a lot with WPF, but here is an interesting solution for ASP.Net.

I wanted a dynamic TreeView control to act as my navigation for a Web Application. The whole idea was to expose the links/items to a user only if she had permissions to view the page. Permissions are set via an other custom Web Application. Basically, each page has a PermissionId and each User is bound to a Role that has (or not) permission to a page.


To start things up, here is the XML:


  
    
    
  
  
    
    
    
    
  
  
    
    
    
    
  


Now, to be able to manipulate the XML through an object, I created an XSD from the XML (though Visual Studio), and generated a class based on the XSD (using xsd.exe /c).

Now, I will be manipulating the XML in my DataLayer, so I added the XML as an embedded resource. Then, using the following code, I get the user's permissions from the database and compare them to the full navigation. For those permissions that the user has, I generate a new XML object with these nodes.
(Note: Please ignore line 70. The formatter apparently does not like the code :) )

public string getNavigation(string userName)
{
    // Get user permissions from the DB
    List userPermissions = getPermissionsOfUser(userName);

    Navigation myNav = new Navigation();
    List myNavGroup = 
        new List();
    Navigation fullNav = new Navigation();


    //Load the full nav from the XML resource file
    Assembly _assembly = Assembly.GetExecutingAssembly();
    XmlTextReader xml = 
        new XmlTextReader(_assembly.
            GetManifestResourceStream("SecurityDL.Resources.Navigation.xml"));

    // Deserialize the XML to my Navigation object
    XmlSerializer _ser = new XmlSerializer(typeof(Navigation));
    fullNav = (Navigation)_ser.Deserialize(xml);

    // For each Navigation group
    foreach (NavigationNavigationGroup navGroup in fullNav.NavigationGroup)
    {
        // Create a new NavigationGroup object
        NavigationNavigationGroup myNavGroupT = new NavigationNavigationGroup();
        List myNavItems = 
            new List();
        myNavGroupT.Name = navGroup.Name;

        // For each NavigationItem in the NavigationGroup
        foreach (NavigationNavigationGroupNavigationItem navItem 
            in navGroup.NavigationItem)
        {
            // If the permission exists, add it to the list of NavigationItems
            if (userPermissions.Contains(navItem.PermissionValue))
            {
                myNavItems.Add(navItem);
            }
        }

        // If items where added, set the NavigationItem array 
        // of the temp NavigationGroup to these
        if (myNavItems.Count > 0)
        {
            myNavGroupT.NavigationItem = myNavItems.ToArray();
            myNavGroup.Add(myNavGroupT);
        }
    }

    // Set the NavigationGroup
    myNav.NavigationGroup = myNavGroup.ToArray();

    // Serialize the XML object (in memory) to a string
    String XmlizedString = null;
    MemoryStream memoryStream = new MemoryStream();
    XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8);
    _ser.Serialize(xmlTextWriter, myNav);
    memoryStream = (MemoryStream)xmlTextWriter.BaseStream;
    UTF8Encoding encoding = new UTF8Encoding();
    XmlizedString = encoding.GetString(memoryStream.ToArray());
    // Remove xml definitions etc.
    // If we don't do this, the TreeView cannot use it
    XmlizedString = XmlizedString.Replace(
        XmlizedString.Substring(0, 
        XmlizedString.IndexOf("")),"");

    return XmlizedString;
}

So, now for the TreeView definition. In our aspx page, we need to declare the TreeView and an XMLDataSource:

    
        
        
        
        
    


Now, the XPath attribute of the XMLDataSource is set to /*/*. This is used to not include the root element of the XML in our Treeview (in this example, "Navigation").

The ordering of the TreeNodeBinding elements servers as our hierarchy. So, NavigationItem will be nested in NavigationGroup.

As a last step, we simply need to bind the XMLDataSource with the generated XML:

SecurityDataLayer _db = new SecurityDataLayer();

xmlDS.Data = _db.getNavigation(Page.User.Identity.Name);
xmlDS.DataBind();
I cache the value so I don't need to re-evaluate for the user each time.

Here is the result (after some custom formatting):













That's it! Now, by simply changing the contents of the XML file, the site's navigation changes also!

 Note: 
Some parts of the code are not case-sensitive-correct. The syntax parser I use did not perform well...

1 comment: