Binding the category list to a Treeview control-Part Two

This article follows on from Part One (which is about creating a nested list of categories from BE.NET) and is mainly focussed on the admin section of the site (mainly the add_entry.aspx page). The goal of this exercise is to allow the user to graphically classify a post into one or more categories using an old-fashioned asp.net  treeview control. 

The Datasource

In Part one, the datasource was being populated from the GetDataSource() method in the class EnhancedCategoryList. The method returned a templated SortedList categories which are assigned to at least 1 post.

In the backend, however, the situation is different. Above all, we need:

  • 1. All categories form BlogEngine.NET, not just those who have posts...
  • 2. A single Root Category item (if you want to bind the list to a treeview) and all the existing categories need to belong to the Root item.

 

Following these requirements, we have a different implementation of the GetDataSource() method for the backend.

private Category GetDataSource() 
    { 
      Category root = new Category(); 
      foreach (Category cat in Category.Categories) 
        { 
            if (cat.Parent != null) 
            { 
                Category c = Category.GetCategory(new Guid(cat.Parent.Value.ToString())); 
                if (c != null) 
                { 
                    if ((!c.ChildCategories.Contains(cat))) 

                        c.ChildCategories.Add(cat); 
                } 
            }           
            if ((!cat.Parent.HasValue)) 
                root.ChildCategories.Add(cat); 
        } 
        return root; 
    } 

Run(1)

Let's put the datasource to the test. If we bind a treeview to the GetDatasource() method,  an Invalid Operation is thrown at runtime ["HierarchicalDataboundControl only accepts data sources that implement IHierarchicalDatasource or IHierarchicalEnumerable"]. Pretty self-explanatory you say....

The IHierarchicalEnumerable Interface

To get rid of the error message, we need to implement the IHierarchicalEnumerable Interface for our collection [Source: The IHierarchicalEnumerable interface on MSDN].

In our case, the actual collection is exposed through the ChildCategories property of the Category Class. Recall that in Part One, we had two options to implement this property.  The beauty of having chosen Option 2 over Option 1 is that it allows us to safely extend our Categories Class to implement the required interface. If it had been otherwise, you would have spent some time in refactoring (and testing) the whole code.

Side question: Why did they hide the IHierarchicalEnumerable interface under System.Web.UI? hmmm...

The only method to implement in this interface is the GetHierarchyData.

/// 
/// Container to host a list of Category objects
/// 
public class Categories : List, IHierarchicalEnumerable
{
    List cat = null;
    public Categories(): base()
    {
        cat = new List();
    }
    #region IHierarchicalEnumerable Members
    public IHierarchyData GetHierarchyData(object enumeratedItem)
    {
        return enumeratedItem as Category;
    }
    #endregion
}

 

I felt a bit disappointed when I realised that the above code does not compile. Seriously, I thought that it was going to be an easy ride...

To get rid of the error, we need to implement IHierarchyData interface on the Category Class.

The IHierarchyData Interface

The IHierarchyData interface exposes a node of a hierarchical data structure, including the node object and some properties that describe characteristics of the node. [Source: The IHierarchyData interface on MSDN].

In brief, we cannot represent a category object in a treeview unless we implement this interface.

The GetChildren()method returns the children of a category.

public IHierarchicalEnumerable GetChildren()
{
    Categories children = new Categories();
    if (childCategories.Count > 0)
    {
        foreach (Category cat in childCategories)
        {
            children.Add(cat);
        }
    }
    return children;      
}

The GetParent() method returns the parent of the Category.

public IHierarchyData GetParent()
{
    if (parent != null)
    {
        return parent;
    }
    return null;
}

NB: parent here refers to the Parent Category Object (not the usual BE.Net Guid of the parent category).See Part one for my explanation on this.

The HasChildren property determines whether or not a category has children.

public bool HasChildren
{
    get {
        return (childCategories.Count > 0);          
    }
}

The Item property returns the actual object.

public object Item
{      
    get { return this; }        
} 


The Type property returns a string representation of the type of the object.

public string Type
{
    get { return this.GetType().ToString(); }        
} 

The Path property is what I think crucial to the implementation of the IHierarchyData. Very often, this property is misunderstood (or taken too lightly). This property is returned when you access the ValuePath  of a TreeNode object.  In our case, we will represent the Path as a cumulative ID ToString() representation of the IDs of current category, its parent and ancestors up to the Root Category (separated by the /). For  example, if the ID of the current category is d3e132252-e01d-4b95-be9c-347e20d37cbc, then the value path of the node holding the current category is

d1e3be6f-8026-4d8d-8205-21863c4f6066/845d740f-9228-4a87-9fc3-dc66fa349678/3e132252-e01d-4b95-be9c-347e20d37cbc

where :

d1e3be6f-8026-4d8d-8205-21863c4f6066 is the ID of the root category

845d740f-9228-4a87-9fc3-dc66fa349678 is the ID of the parent category

3e132252-e01d-4b95-be9c-347e20d37cbc is the ID of the current category.

Also, the FindNode method of a treeview relies on the ValuePath property of a node to locate a specific node in the treeview. Consequently, it is important not to mess this up. Otherwise, we would be able to know which nodes have been checked in the treeview.

public string Path 
{ 
   get
    { 

        Category cat = this; 
        string s= String.Empty; 
        while ((cat = cat.ParentCategory) != null) 
        {   
            s = String.Concat (cat.Id.ToString(),"/", s); 
        }            
       return String.Concat(s,this.Id.ToString()); 
   } 
} 

Run(2)

Even after having implemented those two interfaces, an identical InvalidOperationException is thrown at runtime ..Back to basics...

After endlessly jumping from one MSDN page to another, I landed on the HierarchicalDataSourceControl Class

I really really really found the following paragraph interesting!!

"The following code example demonstrates how to extend the abstract HierarchicalDataSourceControl class and the HierarchicalDataSourceView class, and implement the IHierarchicalEnumerable and IHierarchyData interfaces to create a hierarchical data source control that retrieves file system information. The FileSystemDataSource control enables Web server controls to bind to FileSystemInfo objects and display basic file system information. The FileSystemDataSource class in the example provides the implementation of the GetHierarchicalView method, which retrieves a FileSystemDataSourceView object. The FileSystemDataSourceView object retrieves the data from the underlying data storage, in this case the file system information on the Web server. For security purposes, file system information is displayed only if the data source control is being used in a localhost, authenticated scenario, and only starts with the virtual directory that the Web Forms page using the data source control resides in. Finally, two classes that implement IHierarchicalEnumerable and IHierarchyData are provided to wrap the FileSystemInfo objects that FileSystemDataSource uses. "

Just what I needed... It looks as if that the only classes that I did not implement were HierarchicalDataSourceControl and HierarchicalDatasourceView classes.

The CategoryDatasourceView class (Extending the  HierarchicalDatasourceView class)

To incorporate these addition UI related class, I created a folder called DatasourceControls under the App_Code folder.

The important method here is the Select Method.  In here, we build the category tree and attach it to the root Item.

namespace DatasourceControls 
{ 
    public class CategoryDatasourceView : HierarchicalDataSourceView 
    { 
        string viewPath; 
        Category rootCategory; 

        public CategoryDatasourceView(string ViewPath) 
        { 
            viewPath = ViewPath; 
        } 
        public CategoryDatasourceView(Category root) 
        { 
            rootCategory = root; 
        } 
       public override IHierarchicalEnumerable Select() 
        { 
            Categories childrenOfRoot = new Categories(); 
            foreach (Category cat in rootCategory.ChildCategories) 
            { 
                childrenOfRoot.Add(cat); 
            } 
            return childrenOfRoot;           
        }        
    } 
} 

The CategoryDatasource class (Extending the HierarchicalDataSourceControl class)

There's not much to say about this class:P MSDN classifies this class as " the base class for data source controls that represent hierarchical data."

namespace DatasourceControls 
{ 
    public class CategoryDatasource : HierarchicalDataSourceControl, IHierarchicalDataSource 
    { 
        CategoryDatasourceView view = null; 
        Category rootPage; 
        public CategoryDatasource(Category RootPage) : base() 
        { 
            rootPage = RootPage; 
        } 
        // Return a strongly typed view for the current data source control. .
        protected override HierarchicalDataSourceView GetHierarchicalView(string viewPath) 
        { 
            if (view == null) 
            { 
                view = new CategoryDatasourceView(rootPage); 
            } 
            return view;           
        } 
        #region IHierarchicalDataSource Members 
        // The CategoryDatasource can be used declaratively. To enable 
        // declarative use, override the default implementation of 
        // CreateControlCollection to return a ControlCollection that 
        // you can add to. 
        protected override ControlCollection CreateControlCollection() 
        { 
            return new ControlCollection(this); 
        } 
        #endregion 
    } 
} 

Run (3)

After having read the example on MSDN, I realised that the way i bound the treeview was wrong. We now need to create a new instance of the CategoryDatasource prior to binding it to the treeview. Otherwise, you will be stuck in a dead end [The infamous invalid operation exception will be thrown at runtime.]

CategoryDatasource ds = new CategoryDatasource(GetDataSource()); 
tree.DataSource = ds; 
tree.DataBind(); 

Partly successful: This time, IT WORKS!!

Each time a node is constructed, the ToString() implementation of the underlying Category Object is called, hence resulting in the above. To resolve this, we need to use an instance of the TreeNodeBinding class.

TreeNodeBinding binding = new TreeNodeBinding();
binding.TextField = "Title";
binding.ValueField = "Id";
tree.DataBindings.Add(binding);

CategoryDatasource ds = new CategoryDatasource(GetDataSource());
tree.DataSource = ds;
tree.DataBind();


The TexField property of the TreeNodeBinding class maps to the Title property of the Category Class. Likewise, the ValueField maps to the Id property.

Result

NB:
In this situation, it seems logical to make use of the DataMember property of the TreeNodeBinding class and assign it to "Category". However, in doing so, you will end up with same Category.ToString() node problem.

Testing

I quickly mocked up a test harnest page to evaluate the functionality of the treeview.

protected void btnTest_Click(object sender, EventArgs e) 
{ 
    Response.Write("You clicked the following Nodes : 
"); foreach (TreeNode node in trvCategories.CheckedNodes) { Response.Write("Node Text: " + node.Text + "
"); Response.Write("Node Value: " + node.Value + "
"); Response.Write("Node Value Path: " + node.ValuePath + "
"); Response.Write("
"); } }

Finding a node

In this one, the node i wanted to find was the subcategory node.

protected void btnFind_Click(object sender, EventArgs e) 
{ 
    TreeNode t = trvCategories.FindNode(TextBox1.Text); 
    if (t == null) 
    { 
        lblResult.Text = "Node NOT FOUND with a value path of " + TextBox1.Text + "
"; } else { lblResult.Text = "Node FOUND with a value path of " + TextBox1.Text + "

"; lblResult.Text += "Node Text: " + t.Text + "
"; lblResult.Text += "Node Value: " + t.Value + "
"; lblResult.Text += "Node Value Path: " + t.ValuePath + "
"; } }

Satisfactory :)

Changes in BlogEngine.Net (add_entry.aspx)

Once the testing of tree-bound collection is complete, we now need to cascade our changes to the add_entry page.

Datasources

Firstly, we need to copy across the method that will allow us to generate our collection.

private Category GetDataSource() 
{ 

    Category root = new Category(); 
    foreach (Category cat in Category.Categories) 
    { 
        if (cat.Parent != null) 
        { 
            Category c = Category.GetCategory(new Guid(cat.Parent.Value.ToString())); 
            if (c != null) 
            { 
               if ((!c.ChildCategories.Contains(cat))) 
                    c.ChildCategories.Add(cat); 
            } 
        } 
         if ((!cat.Parent.HasValue)) 
            root.ChildCategories.Add(cat); 
    } 
    return root; 
} 

On the aspx page, i commented out the checkboxlist  cblCategories.

<div style="width:400px"> 
    <%--<asp:CheckBoxList runat="server" Width="400" ID="cblCategories" CssClass="cblCategories"
    RepeatLayout="flow" RepeatDirection="Horizontal" TabIndex="12" />--%> 

    <asp:TreeView ID="treeCategories" runat="server" ShowLines=true  
    ShowCheckBoxes="All"   ShowExpandCollapse="true"  /> 
</div> 

In the code behind file,

Changes to  btnSave_Click() - Lines (218-223)]

/* foreach (ListItem item in cblCategories.Items) 
{ 
    if (item.Selected) 
       post.Categories.Add(Category.GetCategory(new Guid(item.Value))); 
}*/ 
foreach (TreeNode node in treeCategories.CheckedNodes) 
{ 
    if (node.Checked) 
        post.Categories.Add(Category.GetCategory(new Guid(node.Value))); 
} 

Changes to BindCategories() - Lines (273-279)

private void BindCategories() 
{ 
/* 
    foreach (Category cat in Category.Categories) 
    { 
          cblCategories.Items.Add(new ListItem(Server.HtmlEncode(cat.Title), cat.Id.ToString())); 
    } 
 * */ 

TreeNodeBinding binding = new TreeNodeBinding();
binding.TextField = "Title"; 
binding.ValueField = "Id"; 
treeCategories.DataBindings.Add(binding); 

CategoryDatasource ds = new CategoryDatasource(GetDataSource()); 
treeCategories.DataSource = ds; 
treeCategories.DataBind(); 

} 

Changes to BindPost() - Lines (357 -362)

/*foreach (Category cat in post.Categories) 
    { 
          ListItem item = cblCategories.Items.FindByValue(cat.Id.ToString()); 
          if (item != null) 
                item.Selected = true; 
    }*/ 
foreach (Category cat in post.Categories) 
{ 
    TreeNode node = treeCategories.FindNode(cat.Path); 
    if (node != null) 
    { 
        node.Checked = true; 
    } 
} 

Changes to btnCategory_Click() -  (lines 174 - 184)

///  
/// Creates and saves a new category 
///  
private void btnCategory_Click(object sender, EventArgs e) 
{ 
    if (Page.IsValid) 
    { 
          Category cat = new Category(txtCategory.Text, string.Empty); 
          cat.Save(); 
          /*ListItem item = new ListItem(Server.HtmlEncode(txtCategory.Text), cat.Id.ToString()); 
          item.Selected = true; 
          cblCategories.Items.Add(item);*/ 
    BindCategories(); 
    } 
} 

 

Final Result

Conclusion

I thought that this  development was gonna be easy-but i must say that I under-estimated the task at hand :)

I hope this will help someone, somewhere...someday :)

Tag cloud

Flash Player 9 required.

About Me

I wish I could write something here..
//TODO: ElaborateMe