Latest Build - Category Control

Topics: Controls
Feb 8, 2012 at 1:27 PM

Happened to notice that in the latest build there is an update to the Category control providing visual representation for nested categories. Had a look at the code and it's beautifully simple, however, I can see a potential problem. 

Categories are indented according to the number of dashes in the complete title (neat).

In BE a parent category can have zero posts, if it has zero posts it will not be displayed but it may have children that do have posts and these will be displayed indented - which could then make these child categories appear to belong to an unrelated category above it in the list. (hovering will give complete title in the tooltip, but the visual clue to ordering might be a little misleading). 

For some reason I couldn't compile the latest version, so I've been unable to confirm this.

Coordinator
Feb 8, 2012 at 6:00 PM

Look at the references in the Core project, if Microsoft.Web.Optimization.dll reference is broken, delete and then add pointer to dll in the /lib/optimization folder, that should fix compilation issue.

Feb 8, 2012 at 11:54 PM

Thanks for that. 

Just did a quick test on the new Category control.

Categories having no posts are hidden (sensible), trouble being that when their child categories do have posts, they are visible and indented. So for example, a parent category of 'Pub' having no posts but having child categories 'Beer' and 'Wine' that do have posts, sitting below a category of 'Gym' then looks something like this -

  • Gym (3)
    • Beer (5)
    • Wine (1)

Trouble is that if you don't hide zero count categories, you end up with something like this (when showing counts) -

  • Gym (3)
  • Pub (0)
    • Beer (5)
    • Wine (1)

Which can also be a little misleading on the counts.

Anyway, thought it worth a mention.

 

Coordinator
Feb 9, 2012 at 3:02 AM

Yes agree, I haven't look into it too closely but if you have a solution by all means - code contributions are always welcome! :)

Feb 9, 2012 at 2:33 PM
Edited Feb 9, 2012 at 2:46 PM

The solution I've just started using is slightly more than visual, it aggregates the categories and feeds for each parent category to include descendants. For example- 

  • Pub (10) - having 4 of it's own
    • Beer (5)
    • Wine (1) 

Clicking on 'Pub' shows 10 posts, the posts marked with the general category 'Pub' (4off) and everything nested below it. Clicking on 'Beer' shows 5 posts, if there are no posts having the general category of 'Pub' then it looks like this- 

  • Pub (6)
    • Beer (5)
    • Wine (1) 

Where clicking on 'Pub' shows the posts or feeds for Beer and Wine. 

I have this running and it seems to be stable enough. 

I have included another feature whereby you can view 'All Categories', the advantage of this being that you can then include logic on that page to view all the posts by whatever criteria you choose i.e. by date, by number of comments, most rated etc. 

I like the simplicity of indenting by the number of dashes in the complete title (from the latest BE category control) and intend to use this for presentation of structure in both the archives and post admin (probably have archives done later today). 

If this seems like a reasonable way of doing things, then I'm quite happy to contribute. 

The next step is to investigate the possibility of allowing duplicate category names, i.e. 

  • Programming
    • C#
    • VB 
  • .Net
    • C#
    • VB

PS 

I was thinking of adding support to this category control to enable collapsing and expansion of tree branches, not sure if that's overkill though.

Feb 12, 2012 at 10:56 AM

Looking for some more feedback. 

Category widget now supports expanding and collapsing of child cats.

Archive formatting now reflects any child category structuring. 

Example

Here's the only issue I can think of when viewing by category, and it may not even be an issue.

Because parent categories can contain child cats, you need to show everything that's in the parent cat, this means that you can't show posts marked only with the parent cat separately. However, these are shown first when viewing by category, followed by posts in child cats. 

If you don't have any parent child cats then everything works and shows the same as before. 

My questions are simple. 

Does anybody want this functionality?

If so, is this a reasonable way to handle sub categories?

Is there another way?

Coordinator
Feb 12, 2012 at 4:22 PM

Shouldn't that be 9 instead of 8 in articles category? 3 + 3 + 2 + 1?

Feb 12, 2012 at 6:40 PM
Edited Feb 12, 2012 at 7:28 PM

A fair point, which I'm glad you noticed, there are 8 unique posts in the general category 'Articles' but since BE supports a single post having multiple categories, you have to count it in each category it appears, but only once in the overall post count. In this case the post 'iMad' appears in both sub categories 'Apple' and 'Tablet' of parent category 'Articles'- hope that makes sense.

Edit, just to clarify the reasoning.

If you click on the category link for 'Apple' you see 3 posts, one of which is 'iMad' and likewise for the feed, which is what you would expect for that category.

Clicking on category link 'Tablet' you see 2 posts, one of which is 'iMad', again what you expect for that category.

If you click to view the parent category 'Articles' or just want the general 'Article' feed, then the post 'iMad' should appear only once.

Jul 31, 2012 at 6:39 PM
Edited Jul 31, 2012 at 6:40 PM

I need the css code to nest the category list.

 

Trying to get it to work here:

 

http://blog.bloggersonline.com/

 

Thanks,

 

Brian Davis

Jul 31, 2012 at 9:00 PM

Hi Brian, 

The gallery version of this is now quite old, BE 2.6 has nesting built in (style attribute for indenting set according to the number of dashes in the complete title).

I'm now using a couple of updated versions of my own, one that integrates with the main page menu (labelled posts) and a collapsible tree version that sits in the side bar.

Both these versions include full category support as noted above, trouble is, they were written for BE 2.0. I will be moving to 2.6 or 2.7 when it comes out and will be looking to update and make available once fully tested for these versions. The widgets that I post then will probably not include full category support, since this requires the addition of a couple of methods in the core and default.aspx (although all changes could probably be accommodated in default.aspx, which could be made available for those who wish it).

So to cut a long story short, I suppose it depends on what version you want, if it's just the basic nested unordered lists, then I could certainly have a look at the CSS for that and pass it on.

You are of course welcome to whatever code you might think be of any use in the meantime.

Cheers

Andy

Jul 31, 2012 at 10:34 PM

I got the widget part working ok.

 

I just need the CSS :)

 

I got it some what working with CSS but my theme is sorta complex with CSS.

 

Just need an example for the css

 

Mainly css for the children elements to make them indent.

Also in the control code

 

Where does it insert the second <UL> for the children?

 

I would like to have the children css a class

 

Like <UL class="submenu">

 

Then have css for the classes. Easier to edit and etc.

 

End goal for the HTML produced by the widget:

 

<ul class="menu">  <--- This I know where it is :)
  <li>Menu 1</li>
  <li>
   <ul class="submenu">  <----  Where in the widget code behind is the <ul> tag inserted?
    <li>submenu 1</li>
    <li>submenu 2</li>
    <li>submenu 3</li>
   </ul>
  </li>
</ul>

 

I got the code and ideal from

 

http://thewebthought.blogspot.com/2011/06/css-html-nested-lists-with-style.html

Thanks,

 

Brian Davis

Aug 1, 2012 at 12:24 AM

The widget already produces properly formatted HTML for the nested lists, so that part is fine.

Personally, I prefer to style the items via the rules for CSS inheritance i.e.

ul li {some style for the first ul li level}, ul ul li {some overriding style for the next} etc

But if you want to add named classes for each level, then you can.

The items are added recursively, so a depth counter can be added to give class names appropriate to nesting depth.

Revised commented code is included below, I gave it a quick test and it appears to be fine (a little more testing would not go amiss though).

Hope this is of some use.

// --------------------------------------------------------------------------------------------------------------------
// <summary>
//   Builds a category list.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace App_Code.Controls
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.HtmlControls;

    using BlogEngine.Core;

    /// <summary>
    /// Builds a category list.
    /// </summary>
    public class CategoryList : Control
    {
        #region Constants and Fields

        /// <summary>
        /// The html string.
        /// </summary>
        private static Dictionary<Guid, string> blogsHtml = new Dictionary<Guid, string>();

        /// <summary>
        /// The show rss icon.
        /// </summary>
        private static Dictionary<Guid, bool> blogsShowRssIcon = new Dictionary<Guid, bool>();

        /// <summary>
        ///     The show post count.
        /// </summary>
        private bool showPostCount = true;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        ///     Initializes static members of the <see cref = "CategoryList" /> class.
        /// </summary>
        static CategoryList()
        {
            Post.Saved += (sender, args) => blogsHtml.Remove(Blog.CurrentInstance.Id);
            Category.Saved += (sender, args) => blogsHtml.Remove(Blog.CurrentInstance.Id);
        }

        #endregion

        #region Properties

        /// <summary>
        ///     Gets or sets a value indicating whether ShowPostCount.
        /// </summary>
        public bool ShowPostCount
        {
            get
            {
                return this.showPostCount;
            }

            set
            {
                if (this.showPostCount == value)
                {
                    return;
                }

                this.showPostCount = value;
                blogsHtml.Remove(Blog.CurrentInstance.Id);
            }
        }

        /// <summary>
        ///     Gets or sets a value indicating whether or not to show feed icons next to the category links.
        /// </summary>
        public bool ShowRssIcon
        {
            get
            {
                Guid blogId = Blog.CurrentInstance.Id;

                if (!blogsShowRssIcon.ContainsKey(blogId))
                    blogsShowRssIcon[blogId] = true;

                return blogsShowRssIcon[blogId];
            }

            set
            {
                if (ShowRssIcon == value)
                {
                    return;
                }

                blogsShowRssIcon.Remove(Blog.CurrentInstance.Id);
                blogsHtml.Remove(Blog.CurrentInstance.Id);
            }
        }

        /// <summary>
        ///     Gets Html.
        /// </summary>
        private string Html
        {
            get
            {
                Guid blogId = Blog.CurrentInstance.Id;

                if (!blogsHtml.ContainsKey(blogId))
                {
                    //Depth counter added to this method call
                    var ul = this.BindCategories(null, 0);
                    using (var sw = new StringWriter())
                    {
                        using (var hw = new HtmlTextWriter(sw))
                        {
                            ul.RenderControl(hw);
                            blogsHtml[blogId] = sw.ToString();
                        }
                    }
                }

                return blogsHtml[blogId];
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Outputs server control content to a provided <see cref="T:System.Web.UI.HtmlTextWriter"/> object and stores tracing information about the control if tracing is enabled.
        /// </summary>
        /// <param name="writer">
        /// The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the control content.
        /// </param>
        public override void RenderControl(HtmlTextWriter writer)
        {
            writer.Write(this.Html);
            writer.Write(Environment.NewLine);
        }

        #endregion

        #region Methods

        /// <summary>
        /// Get the post count for the specified category.
        /// </summary>
        /// <param name="cat">
        /// The category.
        /// </param>
        /// <returns>
        /// <c>int</c> for the category post count.
        /// </returns>
        private static int GetPostCount(Category cat)
        {
            var matchingPosts = Post.Posts.Where(post => post.IsVisible).SelectMany(post => post.Categories).Where(category => category == cat);
            return matchingPosts.Count();            
        }

        /// <summary>
        /// Determines whether the specified category has children.
        /// </summary>
        /// <param name="cat">
        /// The category.
        /// </param>
        /// <returns>
        /// <c>true</c> if the specified category has children; otherwise, <c>false</c>.
        /// </returns>
        private static bool HasChildren(Category cat)
        {
            return cat == null ? false : Category.Categories.Any(c => c.Parent.Equals(cat.Id));
        }

        /// <summary>
        /// Bind categories and construct category navigation.
        /// </summary>
        /// <returns>List control (ul list or nested ul lists).</returns>
        /// 
        //Last parameter is a depth counter, you need this to name classes according to depth
        private HtmlGenericControl BindCategories(Category parentCat, int depth)
        {
            var ul = new HtmlGenericControl("ul");
            depth++;

            if (parentCat == null)
            {
                ul.Attributes.Add("id", "categorylist");

                if (Category.Categories != null)
                {
                    var rootCategories = Category.Categories.Where(c => c.Parent == null);

                    foreach (var rootCat in rootCategories.OrderBy(c => c.Title))
                    {
                        bool hasChildren = HasChildren(rootCat);
                        var li = GetCatDetails(rootCat, hasChildren, depth);
                        if (hasChildren)
                        {
                            li.Controls.Add(BindCategories(rootCat, depth));                            
                        }
                        ul.Controls.Add(li);
                        //Add classes to unordered lists
                        if (ul.HasControls())
                        {
                            ul.Attributes.Add("class", "ulLevel" + depth);
                        }
                        //end
                    }
                }
            }
            else
            {
                var childCategories = Category.Categories.Where(c => c.Parent.Equals(parentCat.Id));

                foreach (var childCat in childCategories)
                {
                    var hasChildren = HasChildren(childCat);
                    var li = GetCatDetails(childCat, hasChildren, depth);
                    if (hasChildren)
                    {
                        li.Controls.Add(BindCategories(childCat, depth));                        
                    }
                    ul.Controls.Add(li);
                    //Add classes to unordered lists
                    if (ul.HasControls())
                    {
                        ul.Attributes.Add("class", "ulLevel" + depth);
                    }
                    //end
                }
            }

            return ul;
        }

        //Last two parameters included if you want to add classes to list items
        private HtmlGenericControl GetCatDetails(Category childCat, bool hasChildren, int depth)
        {
            var li = new HtmlGenericControl("li");
            if (this.ShowRssIcon)
            {
                string description = string.Format("{0} feed for {1}", BlogSettings.Instance.SyndicationFormat.ToUpperInvariant(), childCat.CompleteTitle());
                var img = new HtmlImage
                {
                    Src = string.Format("{0}pics/rssButton.png", Utils.RelativeWebRoot),
                    Alt = description
                };
                img.Attributes["class"] = "rssButton";
                var feedAnchor = new HtmlAnchor { HRef = childCat.FeedRelativeLink, Title = description };
                feedAnchor.Attributes["rel"] = "nofollow";
                feedAnchor.Controls.Add(img);
                li.Controls.Add(feedAnchor);
            }
            var anc = new HtmlAnchor { HRef = childCat.RelativeLink, InnerHtml = childCat.Title, Title = childCat.CompleteTitle() };

            //Add if you want class added to your list item
            if (hasChildren)
            {
                li.Attributes.Add("class", "liLevel" + depth);
            }
            //end

            li.Controls.Add(anc);
            if (this.ShowPostCount)
            {
                var spn = new HtmlGenericControl("span") { InnerText = string.Format(" ({0})", GetPostCount(Category.GetCategory(childCat.Id))) };
                li.Controls.Add(spn);
            }
            return li;
        }

        #endregion
    }
}

Dec 2, 2013 at 5:34 AM
Edited Dec 2, 2013 at 5:35 AM
Dec 2, 2013 at 10:43 AM
Edited Dec 2, 2013 at 10:45 AM
Both great posts.
For the second post, if you can live without a treeview control, you can present nested categories in admin with just a few lines of code.

This is how it looks in BE 2, for multiple blog versions it's a little different.

admin/Posts/Add_entry.aspx.cs
 /// <summary>
        /// The bind categories.
        /// </summary>
        private void BindCategories(Guid postId)
        {
            string catHtml = "";
            var post = postId == Guid.Empty ? null : Post.GetPost(postId);

            foreach (var cat in Category.Categories)
            {
                string chk = "";
                if (post != null && post.Categories.Contains(cat))
                    chk = "checked=\"checked\"";
                //Altered to give indentation to nested categories (following two lines)                
                int level = MyThemeTools.cUtils.GetCatDepth(cat) * 16;
                catHtml += string.Format("<input type=\"checkbox\" {0} id=\"{1}\" style=\"margin-left:{2}\">", chk, cat.Id, level.ToString() + "px");
                catHtml += string.Format("<label>{0}</label><br/>", Server.HtmlEncode(cat.Title));
            }
            cblCategories.InnerHtml = catHtml;
        }
GetCatDepth method
 /// <summary>
        /// Use this method insted of counting dashes in complete title to determine nesting depth,
        /// some cat names may have actual dashes in the name.
        /// </summary>
        public static int GetCatDepth(Category c)
        {
            int depth = 0;
            var parentId = c.Parent;
            while (parentId != null)
            {
                depth++;
                parentId = Category.GetCategory(parentId.Value).Parent;
            }
            return depth;
        }
Result
Dec 2, 2013 at 10:50 AM
Thank you very much
I will try it.
But is it possible to add posts in the tree?
I mean every category includes posts that are affiliated to it.
Dec 2, 2013 at 11:48 AM
Edited Dec 2, 2013 at 12:01 PM
That code will work for BE 2, for multi-blogs 2.5 upwards you would need to alter the GetCatDepth method.
Alternatively, if you are sure you won't be using dashes in any category names, just count the dashes in the complete title(as currently implemented in CategoryList.cs) giving:
int level = cat.CompleteTitle().Count(c => c == '-');
The complete title shows the hierarchy using dashes(grandparent - parent - child).

The next version of BE looks like it will have a completely different admin, mobile friendly, so if you plan to upgrade it might be worth waiting to see what that looks like.

Posts under categories
Personally, I prefer to keep the category tree as an outline and show the affiliated posts in the post list, but what you suggest is possible.

Having said that, I'm planning a site redesign and as part of that intend to have a page top category menu, on hover this will show a list of sub-categories and post extracts from the first few posts in each category, a bit like this page top menu, which is not dissimilar to what you are talking about.

Might be a couple of months before I get round to it though.