Extending the RadTreeView pt2

1 posts, 0 answers
  1. Stuart Hemming
    Stuart Hemming avatar
    1622 posts
    Member since:
    Jul 2004

    Posted 24 Oct 2009 Link to this post


    RadControls version

    Any AJAX version
    .NET version

    Visual Studio version VS2008
    programming language C# (v3.0)
    browser support

    all browsers supported by RadControls

    Extending the RadTreeView pt2
    Last time I said that I had an extension I used when using the treeview for navigation. What this article will do is explain why I created it and how to use it.

    I guess that many of us see the treeview as a nice easy way to display a hierarchy of objects. Similarly, the TreeView is a nice intuitive way to let the user navigate around a site or set of options.

    in an app I was working on we had a menu with an entry in it that took the user to a module for, say, managing documents. Another took them to a discussion board and a third took them to an event management system. Each of these modules used a treeview for navigating through the structure of the objects being managed, files & folder, discussions & topics and events.

    I thought that it would be nice to be able to provice a "My Stuff"-style page where all the documents, events and discussions that the user was involved with or interested in could be accessed from.

    The immediate problem, I though, was that whilst each of these sets of objects are heirarchical, they have no relationship to one another and so to get them on a single TreeView would mean working through the data, discovering the objects relationship to all of the other similar objects and the adding it in the appropriate place.

    Too much like hard work.

    Then Albert @ telerik towers posted an answer to a question I posted in the TreeView forum about using multiple hierarchies. The upshot of that post was you can move all the nodes from a TreeView, however created, to another TreeView. That idea formed the basis of the extension I'm discussing here.

    You know that when you create a TreeView using hierarchical data you need to identify certain fields as the ID and ParentID, the Text value and, optionally, the Value, er, value. That's all well and good, but any hierarchy created is going to, potentially, represent different objects than any other one on the target TreeView and we're going to want to set things like the Category and Attributes properties, we're going to want to use different icons, etc.

    So, I wrote an extension that took a DataSource and a set of values and created a TreeView, then moved all of the nodes from that TreeView into the Nodes collection of my target TreeNode.

    Before we look at the code for the extension itself, let's look at the object I created to pass the values in....

        public class RadTreeAddHierarchyOptions 
            public object DataSource { getset; } 
            public string DataTextField { getset; } 
            public string DataValueField { getset; } 
            public string DataFieldID { getset; } 
            public string DataFieldParentID { getset; } 
            public string[] AttributeNames { getset; } 
            public string DataCategoryField { getset; } 
            public string DataToolTipField { getset; } 
            public string DataIsCheckableField { getset; } 
            public string DataIsCheckedField { getset; } 
            public string DataImageUrlField { getset; } 
            public string DataExpandedImageUrlField { getset; } 
            public string DataHoveredImageUrlField { getset; } 
            public string DataSelectedImageUrlField { getset; } 
            public string DataForeColourField { getset; } 
            public string DataBackColourField { getset; } 
            public string NoDataMessage { getset; } 

    As you can see, there are properties on this object covering a lot of the options you might want to set on the TreeView. One that is worth noting is the AttributeNames property. This array should be populated with the named of fields from your data source that you want to use as attibutes on the node.

    Clearly, you may not want to use all of these fields.

    You are equally likely to say, "I want to use X but I don't have a field in my dataset that I can use set that value." That's OK, 'cos it doesn't matter what the type or shape of your data you manipulate it as a simple list before passing it to the TreeView.

    I'll explain. We're using WCF so much of our data comes back as lists of objects. But what I do is create a simple DataTable adding fields for all of the bits of TreeView functionality I want to set then populate it from my original data source.

    Here's an example where I'm adding a document folder structure to my TreeView...

      RadTreeAddHierarchyOptions options = new RadTreeAddHierarchyOptions{ 
          AttributeNames = new string[] { "SecurityLevel""Classification" }, 
          DataTextField = "Name"
          DataValueField = "DocumentFolderId"
          DataFieldID = "DocumentFolderId"
          DataFieldParentID = "ParentDocumentFolderId"
          DataToolTipField = "Description"
          DataImageUrlField = "Icon"
          DataExpandedImageUrlField = "ExpandedIcon"
          NoDataMessage = "No Folders"
          DataCategoryField = "Category"
          DataSource = ds 
    Remember, with the exception of "DataSource" and "NoDataMessage" all the string values represent field names in our data source.

    The data source itself is based on a List<T> from my WCF service and is defined like this...

      DataTable ds = new DataTable(); 
      ds.Columns.Add(new DataColumn("Name")); 
      ds.Columns.Add(new DataColumn("DocumentFolderId")); 
      ds.Columns.Add(new DataColumn("ParentDocumentFolderId")); 
      ds.Columns.Add(new DataColumn("Description")); 
      ds.Columns.Add(new DataColumn("SecurityLevel")); 
      ds.Columns.Add(new DataColumn("Classification")); 
      ds.Columns.Add(new DataColumn("Icon")); 
      ds.Columns.Add(new DataColumn("ExpandedIcon")); 
      ds.Columns.Add(new DataColumn("Category")); 
    Having created a DataTable, I populate it from the original DataSource ...

      folders.ForEach(f => 
          DataRow row = ds.NewRow(); 
          row["DocumentFolderId"] = f.DocumentFolderId.ToString(); 
          row["Name"] = f.Name; 
          row["ParentDocumentFolderId"] = f.ParentDocumentFolderId; 
          row["Description"] = f.Description; 
          row["SecurityLevel"] = f.WorkgroupSecurityLevel.ToString(); 
          row["Classification"] = f.Classification.ToString(); 
          row["Icon"] = "~/Images/Office/folder/png/16/folder_closed_16x16.png"
          row["ExpandedIcon"] = "~/Images/Office/folder/png/16/folder_opened_16x16.png"
          row["Category"] = "Folder"
    All I need do now is call the extension method to create the TreeView and populate my node with the result ...

    Remember, the hierarchy added to the TreeView to which ParentNode belongs could be very different to any number of other hierarchies in the TreeView.

    OK. So what about the extension itself? I'll cut straight to the chase and list the code ...

        public static void AddHierarchy(this RadTreeNode node, RadTreeAddHierarchyOptions Options) 
              string error = String.Empty; 
              if (Options == null
                  error = "There are no Options defined"
              else if (Options.DataSource == null
                  error = "No Options DataSource has been specified"
              else if (!Options.DataFieldID.HasValue()) 
                  error = "No DataFieldID value has been specified"
              else if (!Options.DataFieldParentID.HasValue()) 
                  error = "No DataFieldParentID value has been specified"
              else if (!Options.DataTextField.HasValue()) 
                  error = "No DataTextField value has been specified"
              if (error.HasValue()) 
                  throw new Exception(error); 
              RadTreeView t = new RadTreeView(); 
              t.NodeDataBound += new RadTreeViewEventHandler(t_NodeDataBound); 
              if (Options.DataCategoryField.HasValue()) 
                  t.Attributes.Add("DataCategoryField", Options.DataCategoryField); 
              if (Options.DataToolTipField.HasValue()) 
                  t.Attributes.Add("DataToolTipField", Options.DataToolTipField); 
              if (Options.DataIsCheckableField.HasValue()) 
                  t.Attributes.Add("DataIsCheckableField", Options.DataIsCheckableField); 
              if (Options.DataIsCheckedField.HasValue()) 
                  t.Attributes.Add("DataIsCheckedField", Options.DataIsCheckedField); 
              if (Options.DataImageUrlField.HasValue()) 
                  t.Attributes.Add("DataImageUrlField", Options.DataImageUrlField); 
              if (Options.DataExpandedImageUrlField.HasValue()) 
                  t.Attributes.Add("DataExpandedImageUrlField", Options.DataExpandedImageUrlField); 
              if (Options.DataHoveredImageUrlField.HasValue()) 
                  t.Attributes.Add("DataHoveredImageUrlField", Options.DataHoveredImageUrlField); 
              if (Options.DataSelectedImageUrlField.HasValue()) 
                  t.Attributes.Add("DataSelectedImageUrlField", Options.DataSelectedImageUrlField); 
              if (Options.DataBackColourField.HasValue()) 
                  t.Attributes.Add("DataBackColourField", Options.DataBackColourField); 
              if (Options.DataForeColourField.HasValue()) 
                  t.Attributes.Add("DataForeColourField", Options.DataForeColourField); 
              if (String.IsNullOrEmpty(Options.NoDataMessage)) 
                  Options.NoDataMessage = "No Data"
              foreach (string s in Options.AttributeNames) 
                  t.Attributes.Add(s, s); 
              t.DataTextField = Options.DataTextField; 
              if (Options.DataValueField.HasValue()) 
                  t.DataValueField = Options.DataValueField; 
              t.DataFieldID = Options.DataFieldID; 
              t.DataFieldParentID = Options.DataFieldParentID; 
              t.DataSource = Options.DataSource; 
              if (t.Nodes.Count == 0) 
              while (t.Nodes.Count > 0) 
          static void t_NodeDataBound(object sender, RadTreeNodeEventArgs e) 
              RadTreeNode node = e.Node; 
              RadTreeView tree = node.TreeView; 
              IEnumerator keys = tree.Attributes.Keys.GetEnumerator(); 
              String key; 
              while (keys.MoveNext()) 
                  key = (String)keys.Current; 
                  if (node.DataItem is DataRowView) 
                      DataRow row = (node.DataItem as DataRowView).Row; 
                      if (row.Table.Columns.Contains(tree.Attributes[key])) 
                          string colName = tree.Attributes[key]; 
                          if (key == "DataForeColourField"
                              node.ForeColor = System.Drawing.ColorTranslator.FromHtml(row[colName].ToString()); 
                          if (key == "DataBackColourField"
                              node.BackColor = System.Drawing.ColorTranslator.FromHtml(row[colName].ToString()); 
                          if (key == "DataCategoryField"
                              node.Category = row[colName].ToString(); 
                          if (key == "DataToolTipField"
                              node.ToolTip = row[colName].ToString(); 
                          else if (key == "DataHoveredImageUrlField"
                              node.HoveredImageUrl = row[colName].ToString(); 
                          else if (key == "DataSelectedImageUrlField"
                              node.SelectedImageUrl = row[colName].ToString(); 
                          else if (key == "DataImageUrlField"
                              node.ImageUrl = row[colName].ToString(); 
                          else if (key == "DataExpandedImageUrlField"
                              node.ExpandedImageUrl = row[colName].ToString(); 
                          else if (key == "DataIsCheckableField"
                              node.Checkable = Convert.ToBoolean(row[colName]); 
                          else if (key == "DataIsCheckedField"
                              node.Checked = Convert.ToBoolean(row[colName]); 
                              node.Attributes.Add(tree.Attributes[key], row[colName].ToString()); 
                      List<PropertyInfo> infoList = node.DataItem.GetType().GetProperties().ToList(); 
                      PropertyInfo info = infoList.First(i => i.Name == tree.Attributes[key]); 
                      if (info != null
                          if (key == "DataToolTipField"
                              node.ToolTip = info.GetValue(node.DataItem, null).ToString(); 
                          else if (key == "DataCategoryField"
                              node.Category = info.GetValue(node.DataItem, null).ToString(); 
                          else if (key == "DataForeColourField"
                              node.ForeColor = System.Drawing.ColorTranslator.FromHtml(info.GetValue(node.DataItem, null).ToString()); 
                          else if (key == "DataBackColourField"
                              node.BackColor = System.Drawing.ColorTranslator.FromHtml(info.GetValue(node.DataItem, null).ToString()); 
                          else if (key == "DataImageUrlField"
                              node.ImageUrl = info.GetValue(node.DataItem, null).ToString(); 
                          else if (key == "DataHoveredImageUrlField"
                              node.HoveredImageUrl = info.GetValue(node.DataItem, null).ToString(); 
                          else if (key == "DataSelectedImageUrlField"
                              node.SelectedImageUrl = info.GetValue(node.DataItem, null).ToString(); 
                          else if (key == "DataExpandedImageUrlField"
                              node.ExpandedImageUrl = info.GetValue(node.DataItem, null).ToString(); 
                          else if (key == "DataIsCheckableField"
                              node.Checkable = Convert.ToBoolean(info.GetValue(node.DataItem, null)); 
                          else if (key == "DataIsCheckedField"
                              node.Checked = Convert.ToBoolean(info.GetValue(node.DataItem, null)); 
                            node.Attributes.Add(key, info.GetValue(node.DataItem, null).ToString()); 
    The first thing we do is check that we have the values defined in the Options parameter that we need. Then we create a new TreeView and wireup a NodeDataBound event handler.

    Next we create an attribute on the newly created TreeView for each of the options passed in. I did this 'cos I knew that I needed a 'global' (to this call to the extension) variable from which I could extract the data field names. I hit on the idea of using the TreeView's attribute collection 'cos I couldn't think of anything else. If anyone out there knows of a better way of doing this, please let me know.

    Having recorded all of the necessary details, we set up the new TreeView to bind to our passed in data source using the mechanisms described in the telerik documentation. The we call the DataBind method of the new TreeView.

    After binding the TreeView (and we'll look at what's happening in that NodeDataBound handler in just a minute) we check to see if there are any nodes and add a NoDataNode is there aren't. Finally, we copy each of the top-level nodes in the newly created TreeView to the Nodes collection of the RadTreeNode the extension is operating on.

    OK, now for the NodeDataBound event handler. First let me say that this code is far from generic; I've coded for DataTables and objects that come from WCF, there maybe other things you need to do to cope with other data source, I don't know.

    I'll talk about the DataTable case first.

    First we set up a loop to go through each attribute saved to the TreeView and see if out data table contains a field matching the attribute value. If it does, we test to see if the name of the attribute matches one of our extension properties ("DataCategoryField", "DataImageUrlField", "DataIsCheckableField", etc). If it does we set the relevant node property to the value stored in the relevant column of the data row being bound to the tree. If the attribute name doesn't match one of our extension properties, then we assume that it is the name of an attribute to be saved on the node, and do so, using the value in the relevant column of the row being bound to the TreeView.

    Something very similar happens where the data source is a list of objects, but this time we use Reflection to get at the values we need to set the properties and attributes of the newly created node.

    I hope this makes sense. The nice thing about it is you don't /really/ need to understand how it works, just the steps you need to /make/ it work, and they're straightforward:

    1. Generate your original data set.
    2. Create a new DataTable holding columns for the properties in the TreeView you want to set.
    3. Loop through your original data, adding a row to the table for each node you're going to add. Remember, you be as clever as you want about adding value to your raw data at this point.
    4. Set to the options object.
    5. Call the extension method on the node you want to add the hierarchy to.

    That's it.

    If you have any questions or feedback, either post here or mail me at sejhemming@gmail.com.
Back to Top