In Focus

Navigation using Treeview Control and SQL Reporting Services 2005

This tutorial will show you how to use the treeview control with SQL reporting services. This treeview is the standard treeview control introduced in .NET 2.0, not the IE Web Package which is a .NET 1.0/1.1 package.


I spent several hours sifting through article after article about how to build tree navigations dynamically. They assumed that the order of the data coming in was 'orderly' and that you were usually using the IE Web Control toolkit which is a .Net 1.0 package. In our environment 2.0+, that would not work for us and we needed to ensure browser compatibility.
 
Thanks to a colleague's suggestion of recursion, I came up with the below code. It's length is due to all the detailed commenting. Feel free to comment on it...any positives or negatives? It seems to work well, and we have yet to find a way to break it. 

The code assumes you have a page called 'default.aspx' and that you have added one treeview to it labeled TreeView1. You can substitute your sql server info as necessary in the code behind:

using System;

using System.Data;

using System.Configuration;

using System.Security.Cryptography;

using System.Text;

using System.Web;

using System.Web.Security;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using System.Web.UI.HtmlControls;

using DW_SQL_ReportingService_TST;  //Substitute your web reference to your SQL Server Reporting Services

public partial class _Default : System.Web.UI.Page

{

    ReportingService objRepSrvc;

    CatalogItem[] objCatalogItems;

    protected static char pathSeparator = '/';

    protected static char[] pathSeparatorArray = { pathSeparator };

    protected static string pathSeparatorString = new string(pathSeparator, 1);
 

    protected void Page_Load(object sender, EventArgs e)

    { 

        GetReportStructure();

    }

 

    /// <summary>

    /// Instantiate a new ReportingService object with specific credentials.  Then provide the

    /// report path to the ListChildren method.  This will return a collection of CatalogItems containing

    /// your report links. 

    /// </summary>

    private void GetReportStructure()

    { 

        objRepSrvc = new ReportingService();

        objRepSrvc.Credentials = new System.Net.NetworkCredential("userid", "password", "domainname"); 

        /* We subtract one from the ReportPath variable here since it ends with a forward slash.  Our path

         * statement must be /dirA/DirB - without the ending slash

         * In addition, we also set the recursive flag to True indicating we want all subdirectories

         */

        objCatalogItems = objRepSrvc.ListChildren(ReportPath.Remove(ReportPath.Length - 1), true);

        BuildTree(objCatalogItems); 

    }

 

    private void GetReportStructure(string path)

    {

        objRepSrvc = new ReportingService(); 

    }

 

    /// <summary>

    /// This method begins the process of putting our report paths into a usable form.

    /// </summary>

    /// <param name="objCatalogItems"></param>

    private void BuildTree(CatalogItem[] objCatalogItems)

    {

 

        // Iterate through each CatalogItem in your collection

        foreach (CatalogItem item in objCatalogItems)

        {

            // Since we want to use only Report Items we specify that as the Type.

            // In addition we also only want to use reports that are not hidden.  A

            // hidden report is often a drilldown or child report that needs parameters

            // from a prior report; therefore; only show reports not hidden.

            if (item.Type == ItemTypeEnum.Report && (!item.Hidden))

            {

                // Pull the current items complete path and strip off the beginning

                // portion which includes the ReportPath.  Then assign it to a string variable

                string path = item.Path.Remove(0, ReportPath.Length);

                // Split the path string by our previously assigned PathSeparatorArray and put

                // the values into a string array

                string[] tokens = path.Split(PathSeparatorArray);

                // Pass in our new string array and the collection of nodes from our Treeview on

                // our aspx page.

                BuildNodesFromTokens(tokens, TreeView1.Nodes); 

            }

        }

    }

 

    /// <summary>

    /// This method is the first step in actually building our tree node collection.

    /// </summary>

    /// <param name="tokens">A string array containing our report and pathing parts</param>

    /// <param name="nodes">A collection of nodes from our treeview</param>

    private void BuildNodesFromTokens(string[] tokens, TreeNodeCollection nodes)

    {

        // Create a new string array that can hold our original string array values - 1.  The

        // reason for this is to begin shrinking our string array one by one.

        string[] newtoks = new string[tokens.Length - 1];

        // Check for any current nodes on the page

        if (nodes.Count > 0)

        {

            // Since nodes currently exists, we need to check to see if any of the current

            // nodes match the next value of tokens which we'll use to create a node.

 

            // Instantiate an integer variable to act as a counter

            int matchFound = 0;

            // Loop through each node already on the page

            foreach (System.Web.UI.WebControls.TreeNode tr in nodes)

            {

                // Compare the current node text with the current value of index 0 in our

                // string array.  If there is a match then assign all but the first value of

                // our original string array to our new string array - newtoks.

                if (tokens[0].Equals(tr.Text))

                {

                    for (int i = 0; i < newtoks.Length; i++)

                    {

                        newtoks[i] = tokens[i + 1];

                    }

                    // Increment counter

                    matchFound++;

                    // Pass the new string array, which is one less than original, and the current

                    // node to AddNode method.

                    AddNode(newtoks, tr);

                } 

            }

            // If no match was found in the foreach loop

            if (matchFound == 0)

            {

                // Add a new node to the page and use the index 0 value of the tokens (original)

                // string array. 

                nodes.Add(new System.Web.UI.WebControls.TreeNode(tokens[0]));

 

                // Check to see if we have more than one value (indicating additional levels or depth)

                if (tokens.Length - 1 > 0)

                { 

                    // Set the SelectAction of our last node, which is the new one we just created, to

                    // be None.  This prevents the folders from being links with a URL action.  We

                    // simply want it to be a + or - symbol indicating if we can expand or collapse the

                    // folder.

                    nodes[nodes.Count - 1].SelectAction = TreeNodeSelectAction.None;

                    // Assign all but the first value of tokens, our original string array to the newly

                    // created newtoks array.

                    for (int i = 0; i < newtoks.Length; i++)

                    {

                        newtoks[i] = tokens[i + 1];

                    }

                    // Since we just created a new 'parent' node, we'll pass any

                    // existing string array values to the AddChild method to build.  We can

                    // do this since we know that the parent node had not previously existed.  Therefore

                    // no new checks for existence are needed.

                    AddChild(newtoks, nodes[nodes.Count - 1]);

                }

                else

                {

                    // Since this was the last link, get the last node added (use Count - 1) & set it's value, url, and target window when clicked.

                    nodes[nodes.Count - 1].Value = Server.UrlEncode(String.Join(PathSeparatorString.ToString(), tokens));

                    nodes[nodes.Count - 1].Target = "_blank";

                    nodes[nodes.Count - 1].NavigateUrl = "./ReportViewer/ReportViewer.aspx?Report=" + ReportPath + node[nodes.Count -1].ValuePath;

                } 

            }

        }

        else

        {

            // Since no nodes currently exists we can safely add a new node using the index 0 value

            // of tokens.

            nodes.Add(new System.Web.UI.WebControls.TreeNode(tokens[0]));

 

            // Check to see if we have more than one value (indicating additional levels or depth)

            if (tokens.Length - 1 > 0)

            {

                // Set the SelectAction of our last node, which is the new one we just created, to

                // be None.  This prevents the folders from being links with a URL action.  We

                // simply want it to be a + or - symbol indicating if we can expand or collapse the

                // folder.

                nodes[nodes.Count - 1].SelectAction = TreeNodeSelectAction.None;

                // Assign all but the first value of tokens, our original string array to the newly

                // created newtoks array.

                for (int i = 0; i < newtoks.Length; i++)

                {

                    newtoks[i] = tokens[i + 1];

                }

                // Since we just created a new 'parent' node, we'll pass any

                // existing string array values to the AddChild method to build.  We can

                // do this since we know that the parent node had not previously existed.  Therefore

                // no new checks for existence are needed.

                AddChild(newtoks, nodes[nodes.Count - 1]);

            }

            else

            {

                // Since this was the last link, get the last node added (use Count - 1) & set it's value, url, and target window when clicked.

                nodes[nodes.Count - 1].Value = Server.UrlEncode(String.Join(PathSeparatorString.ToString(), tokens));

                nodes[nodes.Count - 1].Target = "_blank";

                nodes[nodes.Count - 1].NavigateUrl = "./ReportViewer/ReportViewer.aspx?Report=" + ReportPath + node[nodes.Count - 1].ValuePath; 

            }

        }

    }

 

    /// <summary>

    /// This method is used to determine if we need to create a new node or go another level deeper.

    /// </summary>

    /// <param name="tokens">A string array containing our pathing (folder structure)</param>

    /// <param name="node">A specific tree node from our TreeView on the aspx page</param>

    private void AddNode(string[] tokens, System.Web.UI.WebControls.TreeNode node)

    {

        // Instantiate a newtoks string array to null;

        string[] newtoks = null; 

        // Check the token length and if not equal to zero, go ahead and assign the newtoks array length

        // to the tokens length - 1.  We perform this check because, while unlikely, if we ever got duplicate

        // reports in the same exact position we would cycle through and end up with an index out of range

        // exception.  You'll see this as we go on.

        if (tokens.Length != 0)

        {

            newtoks = new string[tokens.Length - 1];

        }

 

        // Does the current node already have children (either folders or reports)

        if (node.ChildNodes.Count > 0)

        {

            // Since there were children, now we check each child node to see if it's

            // text property matches index 0 of the string array - tokens.

 

            // Instantiate a integer variable as a counter

            int matchFound = 0;

            // Loop through each node in the current node's child collection.

            foreach (System.Web.UI.WebControls.TreeNode tr in node.ChildNodes)

            {

                // Does the current child node match the string array index 0 value

                if (tokens[0].Equals(tr.Text))

                {

                    // Since there was a match, load the newtoks string array with

                    // all but the first value of the 'tokens array'

                    for (int i = 0; i < newtoks.Length; i++)

                    {

                        newtoks[i] = tokens[i + 1];

                    }

                    // Increment our counter since match was found.

                    matchFound++;

                    // Use recursive call to the AddNode method again passing in the next value

                    // of the string array and the current node.

                    AddNode(newtoks, tr);

                }

            }

            // Check if a match was found

            if (matchFound == 0)

            {

                // Since no match was found pass the current values of the tokens array and the current

                // node to the AddChild method.

                AddChild(tokens, node);

            } 

        }

        else if (newtoks != null)

        { 

            // We check to see if newtoks is null or not.  This is due to the AddNode method potentially allowing

            // a token initialized with 0 size to be sent.  If that is the case, and we try to pass that on to the

            // AddChild method, we'll get an index out of bounds exception.  To avoid that, we have the newtoks

            // array instantiated to an actual array of size only if tokens.length-1 is not 0.  I was able to

            // reproduce this by manually creating duplicate string arrays.  You, in theory, should not be able to

            // have duplicate reports or folders at the same level, however, in case you do we can accomodate it

            // by including this conditional. 

            // Since there were no child nodes of the current node, we will place a call to the

            // AddChild method.

            AddChild(tokens, node);

        } 

    }

 

    /// <summary>

    /// This method will actually create the node and link properties of the last level of each node.

    /// </summary>

    /// <param name="tokens">A string array containing our pathing (folder structure)</param>

    /// <param name="node">A specific tree node from our TreeView on the aspx page</param>

    private void AddChild(string[] tokens, System.Web.UI.WebControls.TreeNode node)

    {

        // Since we know this is a new child, we add a new child to the current node and assign it's

        // text property the value of index 0 in the string array

        node.ChildNodes.Add(new System.Web.UI.WebControls.TreeNode(tokens[0]));

 

        // Check to see if there is more than one value in the string array

        if (tokens.Length - 1 > 0)

        {

            // Since there is more than one vale in the array, create a new array called

            // newtoks and assign it the same size of tokens - 1.  We do this because

            // we no longer the first value in tokens since we have just used it to create

            // a new child node.

            string[] newtoks = new string[tokens.Length - 1];

            for (int i = 0; i < newtoks.Length; i++)

            {

                newtoks[i] = tokens[i + 1];

            }

            // Since there are additional children under the newly created child, we know it's not a

            // report but is a folder.  As a result, we set it's SelectAction to none so that it's displayed

            // as a regular folder level label with nothing more than the + or - to expand/collapse.

            node.ChildNodes[node.ChildNodes.Count - 1].SelectAction = TreeNodeSelectAction.None;

            // Pass the newtoks array and the newly created childnode through a recursive call to AddChild.

            AddChild(newtoks, node.ChildNodes[node.ChildNodes.Count - 1]); 

        }

        else

        {

            // The array only has one value indicating that this is the final level/report.

 

            // Since this was the last link, get the last node added (use Count - 1) & set it's value, url, and target window when clicked.

            node.ChildNodes[node.ChildNodes.Count - 1].Value = Server.UrlEncode(String.Join(PathSeparatorString.ToString(), tokens));

            node.ChildNodes[node.ChildNodes.Count - 1].Target = "_blank";

            node.ChildNodes[node.ChildNodes.Count - 1].NavigateUrl = "./ReportViewer/ReportViewer.aspx?Report=" + ReportPath+ node.ChildNodes[node.ChildNodes.Count - 1].ValuePath;
            //Server.UrlEncode(objCrypt.Encrypt(ReportPath + node.ChildNodes[node.ChildNodes.Count - 1].Value));

        } 

    } 

 

    public static string ReportServer

    {

        // Substitue with whatever your SqlReportingServerName is

        get { return "http://SqlReportingServerName/Reports"; }

    }

 

    public static string ReportPath

    {

        get

        {   // Substitue with whatever your folder structure starts with

            return "/Sales/Field/";

        }

    }

 

    public static char[] PathSeparatorArray

    {

        get { return pathSeparatorArray; }

    }

 

    public static string PathSeparatorString

    {

        get { return pathSeparatorString; }

    } 
}