Creating a nested html list for the Category List in BlogEngineNET - Part One

Background

On a typical BlogEngine.Net install, a category is represented by its title and any subsequent child of the category has the parent category title prefixed. As shown in the screen below, Controls and Subcategory are subcategories of Blogengine.Net and Main Category respectively.

If you have a fairly complex layout for your categories (I bet you do! Especially if you are an avid and pretty organised blogger), the classic Category list will display the categories in a flat structure and it'll be up to the user to determine the category structure, based on the category title (without any visual cues).

The solution a.k.a the EnhancedCategoryList control.

The aim is to display the category list in a tree-like structure, thus making the layout more intuitive and easier to navigate across on the Front End. For the purpose of this exercise, the presentation of the Category list will consist of nested unordered lists. Of course, I'll try to reuse as much code as possible whilst keeping it simple.

The steps are quite simple to follow (and would make sense to anyone in the know: P). I will use a clean BlogEngine.Net 1.4.5 install as the codebase. To be fair, I don't see any reason why the EnhancedCategoryList control should not work with any previous version of BE.Net.  The changes that I will make are pretty much self-contained and have no side effects on other parts of the application.

Firstly, we need to keep track of the children categories of a parent. We have 2 options here:

OPTION 1: Use a List<Category> member inside the Category Class.

[Serializable] 
public class Category : BusinessBase, IComparable
  {
	//.... .
    List _ChildCategories;

    public Category() 
	{ 
	  Id = Guid.NewGuid(); 
	  this._ChildCategories = new List ();
	} 

	public Category(string title, string description)
	{ 
	  this.Id = Guid.NewGuid();
	  this._Title = title;
	  this._Description = description;
	  this.Parent = null;
	  this._ChildCategories = new List ();
	} 
      //.... 
  } 

OPTION 2: Roll out your own container to host the child categories. I personally prefer this method because it will give us scope for improvement and/or extensions. I have a strong feeling that we will be able improve on this throughout the development of the EnhancedCategoryList control. So, that's why I'm sticking to Option 2.

  
namespace BlogEngine.Core 
{
    public class Categories : List 
    { 
        List cat = null; 
        public Categories() : base() 
        { 
            cat = new List(); 
        } 
	} 


[Serializable] 
public class Category : BusinessBase, IComparable 
  {
	   //.....
      Categories _ChildCategories; 
      public Category() 
		{ 
		  Id = Guid.NewGuid(); 
		  this._ChildCategories = new Categories (); 
		} 

    public Category(string title, string description) 
		{ 
		  this.Id = Guid.NewGuid(); 
		  this._Title = title; 
		  this._Description = description; 
		  this.Parent = null; 
		  this._ChildCategories = new Categories(); 
		} 
      //.... .
  } 

 

We also need to expose childCategories as a public property of the Category Class.

public Categories ChildCategories 
{
   get { return _ChildCategories; }
   set { _ChildCategories = value; }
} 

I also thought that the Parent property of the Category Class should reflect the actual Parent Category object, not just the Guid of the Parent object. So, I created the ParentCategory property. Please note that this property is not really necessary for this solution.This has been added for the sake of completeness (and future-proofing).

Category _parent;
public Category ParentCategory 
    { 
        get 
		{ 
            if (this.Parent.HasValue) 
            { 
                _parent = Category.GetCategory(this.Parent.Value); 
                return _parent; 
            } 
            return null; 
        } 
        set { _parent = value; } 
    } 

We're now done with the Category.cs class. It now supports a collection of child categories and has the ability to return its immediate parent (if any).

Back in the website project, I created a new class called EnhancedCategoryList.cs in the /App_Code/Controls folder. The implementation of control will actually be split in 2 classes. The class CategoryListItem represents a category on the frontend (i.e it still has properties ShowPostCount and ShowRssIcon. However, it will be responsible to render itself (in the form of an <LI>...</LI>) and its children if any (<LI>...<UL><LI>...</LI></UL> ...</LI>) using recursion.

On the other hand, the EnhancedCategoryList class is responsible for organizing the existing BE.NET categories into a proper hierarchy of Category objects and handing off the presentation of these objects to the CategoryListItem class.

For matters of convenience, the CategoryListItem and EnhancedCategoryList classes are bundled in the same class file[EnhancedCategoryList.cs]

CategoryListItem Class

class CategoryListItem : Control 
{ 

        SortedList _Children; 
        string _url, _title, _description,_id = String.Empty; 
        bool _showPostCount, _showRssIcon = false; 
        int _postCount = -1; 
        int _tempPostCount = 0; 

        ///  
        /// A CategoryListItem is mapped out as the LI that can optionally contain the postcount,category link+text, and the rss feed. 
        ///  
        /// Category 
        /// Whether or not to show the post count next to the category 
        /// Whether or not to show the RSS icon for category 
        public CategoryListItem(Category currentCategory,bool ShowPostCount,bool ShowRSSIcon) 
        { 

            _url = Utils.RelativeWebRoot + "category/" + Utils.RemoveIllegalCharacters(currentCategory.Title) + BlogSettings.Instance.FileExtension; 
            _title = currentCategory.Title; 
            _description = currentCategory.Description; 
            _showPostCount = ShowPostCount; 
            _showRssIcon = ShowRSSIcon; 
            _id = currentCategory.Id.ToString(); 

            if (ShowPostCount) 
            { 
				_postCount = Post.GetPostsByCategory(currentCategory.Id).FindAll(delegate(Post p) 
					{ 
						return p.IsVisible; 
					}).Count; 
             } 

			if (currentCategory.ChildCategories.Count > 0) 
			{ 
                _Children = new SortedList();
                foreach (Category childCategory in currentCategory.ChildCategories) 
                {                    
                    if (EnhancedCategoryList.HasPosts(childCategory)) 
                       _Children.Add(childCategory.Title, new CategoryListItem(childCategory, _showPostCount, _showRssIcon)); 
                }
            } 
        } 

        /// 
        /// Renders a CategoryListItem. If the item has children, this method gets triggered recursively
        /// 
        /// HtmlTextWriter 
        public override void RenderControl(HtmlTextWriter writer) 
        { 
           writer.RenderBeginTag(HtmlTextWriterTag.Li); 
           if (_showRssIcon) 
            { 
                writer.AddAttribute(HtmlTextWriterAttribute.Class, "feed"); 
                writer.AddAttribute(HtmlTextWriterAttribute.Href, String.Concat(Utils.RelativeWebRoot, "syndication.axd?category=", _id.ToString())); 
                writer.AddAttribute(HtmlTextWriterAttribute.Rel, "nofollow"); 
                writer.AddAttribute(HtmlTextWriterAttribute.Title , String.Concat("RSS feed for Category : ", _title)); 
                writer.RenderBeginTag(HtmlTextWriterTag.A); 
                writer.AddAttribute(HtmlTextWriterAttribute.Src, String.Concat(Utils.RelativeWebRoot, "pics/rssButton.gif")); 
                writer.AddAttribute(HtmlTextWriterAttribute.Alt, String.Concat("RSS feed for Category : ", _title)); 
                writer.AddAttribute(HtmlTextWriterAttribute.Class, "rssButton"); 
                writer.RenderBeginTag(HtmlTextWriterTag.Img); 
                writer.RenderEndTag();//anchor tag 
                writer.RenderEndTag(); //image tag 
            } 

            writer.AddAttribute(HtmlTextWriterAttribute.Class , "linkContainer"); 
            writer.RenderBeginTag(HtmlTextWriterTag.Div ); 
            writer.AddAttribute(HtmlTextWriterAttribute.Href, _url); 
            writer.AddAttribute(HtmlTextWriterAttribute.Title, String.IsNullOrEmpty(_description) ? _title : _description); 

            writer.RenderBeginTag(HtmlTextWriterTag.A); 

            if (_showPostCount && _postCount!=0) 
            { 
                writer.Write(String.Concat(_title, " (", _postCount.ToString(), ")"));  
            }
			else
            { 
				writer.Write(_title); 
            } .
            writer.RenderEndTag(); // anchor tag
            writer.RenderEndTag(); // div tag 

            if (_Children != null)
            { 
                if (_Children.Count > 0) 
                {
                    writer.RenderBeginTag(HtmlTextWriterTag.Ul);
                    writer.Write(Environment.NewLine); //cleaner src code 
                    foreach (CategoryListItem m in _Children.Values) 
						{ 
							m.RenderControl(writer);  //recursively call the RenderControl method of the CategoryListItem class
							writer.Write(Environment.NewLine); 
						}
                    writer.RenderEndTag(); // inner UL Tag
                    writer.Write(Environment.NewLine); 
                } 
            } 
            writer.RenderEndTag(); // Outer LI Tag
            writer.Write(Environment.NewLine); 
        }
}		
	
	

EnhancedCategoryList Class

	
/// 
/// This class represents a container for CategoryListItems.
/// Mainly responsible for the organisation of categories from their previously flat structure into a hierarchy, and rendering of course.
/// Displays a nested list (UL LI) for the Category List.
/// DECLARATIVE USAGE : 
///  

public class EnhancedCategoryList : WebControl 
{ 
	SortedList _categories; 
	string _id = "categorylist"; 
	bool _ShowRssIcon = true; 
	bool _ShowPostCount = true; 
	/// 
	/// Gets or sets whether or not to show feed icons next to the category links.
	///  .
	public bool ShowRssIcon 
	{ 
		get { return _ShowRssIcon; } 
		set {
			if (_ShowRssIcon != value)
			   { 
				   _ShowRssIcon = value; 
			   }
			}	 
	} 
	/// 
	/// Gets or sets whether or not to show the number of posts in the category.
	///  
	public bool ShowPostCount 
	{
		get { return _ShowPostCount; } 
		set {
			 if (_ShowPostCount != value) 
				 { 
					  _ShowPostCount = value; 
				  } 
			} 
	} 
	/// 
	/// Use this property to override the id attribute in the UL tag. By default, it is set to "categorylist"
	///  
	public string ControlID 
	{
	   get { return _id; } 
	   set { _id = value; } 
	} 
	/// 
	/// Organizes items in Category.Categories to produce a "family tree", whereby mapping each parent-child relationship
	/// Children are exposed through ChildCategories property of the Category Class
	/// 
	/// A sorted list of Categories that have posts assigned to. 
	private SortedList GetDataSource() 
	{ 
		SortedList allCats = new SortedList(); 
		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) && (HasPosts(cat))) 
							c.ChildCategories.Add(cat); 
					} 
				} 
			if ((!cat.Parent.HasValue) && (HasPosts(cat))) 
				allCats.Add(cat.Title, cat); 
	} 
	return allCats; 
	} 

	/// 
	/// Determines whether or not a category has at least 1 post assigned.
	/// 
	/// A category
	/// True if the category has posts assigned to it.False otherwise 
	internal static bool HasPosts(Category cat) 
	{ 
		foreach (Post post in Post.Posts) 
		{ 
			if (post.IsVisible) 
			{ 
				foreach (Category category in post.Categories) 
					{ 
						if (category == cat) 
							return true; 
					} 
			} 
		} 
	return false; 
	} 

	public EnhancedCategoryList() 
	{ 
	} 

	/// 
	/// Get a reference to the Categories hierarchy
	/// Create categoryListItems out of the list and add them to the control tree
	///  
	protected override void CreateChildControls() 
	{ 
		_categories = GetDataSource(); 
		foreach (Category child in _categories.Values) 
		{ 
			CategoryListItem m = new CategoryListItem(child, ShowPostCount, ShowRssIcon); 
			this.Controls.Add(m); 
		} 
		base.CreateChildControls(); 
	} 

	public override void RenderBeginTag(HtmlTextWriter writer) .
	{ 
		writer.AddAttribute(HtmlTextWriterAttribute.Id, _id); 
		writer.RenderBeginTag(HtmlTextWriterTag.Ul); 
	} 

	public override void RenderEndTag(System.Web.UI.HtmlTextWriter writer) 
	{ 
		writer.RenderEndTag(); 
	} 
	/// 
	/// Renders all catListItems by calling the RenderControl method of the CategoryListItem class
	/// 
	/// HtmlTextWriter
	///  
	protected override void RenderContents(HtmlTextWriter writer) 
	{ 
		CategoryListItem catListItem; 
		this.EnsureChildControls(); 
		foreach (Control c in this.Controls) 
		{ 
			catListItem = c as CategoryListItem; 
			if (catListItem != null) 
				catListItem.RenderControl(writer); 
		} 
	} 

} 

Now then, you have 2 options to display the EnhancedCategoryList on the front end. If you're running a non-widget based BlogEngine.NET, you just need to place this code in the site.Master file that resides in your theme folder

 <blog:EnhancedCategoryList ShowPostCount="true" ShowRssIcon="true"  runat="Server" />

If you currently use BlogEngine.NET 1.4.5, you will need to create a widget folder called Enhanced Category List and basically create widget.ascx and Edit.ascx files. I've haven't looked into creating the editable widget (where you can toggle ShowRss and PostCount on and off). I guess that this is not a tough nut to crack anyway Cool You can use the existing Category List control as a reference and replicate the functionality.

With a little bit of rookie css kung fu, here's the result.

  

Downloads

EnhancedCategoryList.cs  : EnhancedCategoryList Class File.zip (2.97 kb)

Non-Editable BE.NET Widget : Enhanced Category List.zip (931.00 bytes)

 

Issues and Resolutions

So far, it has been great to see such a control on the front end. I guess the bulk of the work remains with the back end page, namely the add entry page.

The issue here is that there's no notion of a hierarchy in the control. So it would be difficult for you place your post into multiple categories(i.e. you may want to place the post into a parent and a child category as well. In this case, it will be hard for you to distinguish between the parent and the child category in the checkboxlist).

I was thinking to place a treeview control (to which we bind the Categories collection) on this page so that we can visually see the hierarchy. This is definitely a must have feature. Check this space for PART TWO of this article.

Until then.....Enjoy

PART TWO has now been published and can be accessed here.

Tag cloud

Flash Player 9 required.

About Me

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