+1-888-365-2779
Try Now
More in this section

Forums / General Discussions / Filtering CategoriesTree for specific blogs?

Filtering CategoriesTree for specific blogs?

16 posts, 1 answered
  1. Marko
    Marko avatar
    148 posts
    Registered:
    30 Jul 2008
    07 Dec 2009
    Link to this post
    Hi. 

    I have a page which has a BlogPosts control which is showing only one particular blog.  If I drop the CategoriesTree control on that page, too, and set it up so that the provider is "Blogs," it will pick up ALL categories, even the ones that are used in other blogs.  How can I have it show only the categories that are used in the same blog that is shown in the BlogPosts control?

    As far as I know, Categories and Tags are shared among all the blogs in Sitefinity, but it doesn't make sense to show them all on a page that is showing only one particular sub-blog.  I only see a way to filter by "RootCategory" in the CategoriesTree control properties.

    Am i missing something?

    p.s. I'm on ver. 3.6 sp2
  2. Ivan Dimitrov
    Ivan Dimitrov avatar
    16072 posts
    Registered:
    25 Nov 2016
    07 Dec 2009
    Link to this post
    Hi Marko,

    There is a way but it will result in the performance. Here are the steps.

    1. Create a custom control that derives from CategoriesTree.

    2.Override LayoutTemplatePath and set the path to the external template.

    3. Override BindCategories method.

    • Get all content items from your particular blog - you can expose a public property with a selector from where you pass the blog ID.
    • After you have all content items from this particular blog ( you will have all items from a given parent ) you need to get their metadata using GetMetaData("Category") method. Here you have to use foreach loop.
    • Get all categories

    IList categories;
    categories = Manager.GetCategories(0, 0, "CategoryName ASC");

    • you have Ilist so you can call categories.Contains(value) to check whether an content item is categorized. If not remove this item from the list.
    • You have to create a new list - say filteredCategories where you add all matches.
    • Rebind the whole Custom CategoriesThree control with the new datasource.
    •  Finally override InitializeControls and call your custom BindCategories method.
        Best wishes,
    Ivan Dimitrov
    the Telerik team

    Instantly find answers to your questions on the new Telerik Support Portal.
    Watch a video on how to optimize your support resource searches and check out more tips on the blogs.
  3. Marko
    Marko avatar
    148 posts
    Registered:
    30 Jul 2008
    07 Dec 2009
    Link to this post
    Hmm, I'll give it a try when I get a chance, but how big of a performance hit do you think it would be?  Significant?  I assume the bigger the collection of blog posts, the bigger the performance hit, because if having to loop through more and more posts over time, yes?

    Is this something that will possibly be in the future SF releases/updates?  It a little seems incomplete to me that you can have a BlogPosts control filtered to show only particular Blogs, but you can't do that with CategoriesTree to show the related categories to that blog.  Mind you, BlogPosts and CategoriesTree are likely to go together on any blog page, so this functionality, to me, doesn't seem that far-fetched.
  4. Ivan Dimitrov
    Ivan Dimitrov avatar
    16072 posts
    Registered:
    25 Nov 2016
    07 Dec 2009
    Link to this post
    Hi Marko,

    We will be able to isolate the controls for Sitefinity 4.0, with the new implementation. Your feedback is reasonable and we will consider it for the next SP of 3.x versions.

    As for the performance - there are two loops in this implementation

    • one for all content items.
    • one for all categories.
    If there are not so many posts and categories you will not have performance issues. This really depends on the number of posts you will have.
    All the best,
    Ivan Dimitrov
    the Telerik team

    Instantly find answers to your questions on the new Telerik Support Portal.
    Watch a video on how to optimize your support resource searches and check out more tips on the blogs.
  5. Marko
    Marko avatar
    148 posts
    Registered:
    30 Jul 2008
    08 Dec 2009
    Link to this post
    Thanks.  I appreciate you guys considering this for future updates.

    Few more questions about your recommendation above:

    "Get all content items from your particular blog"

    - How do I do that?  If I do Manager.GetContent(), I will get all blog posts, from all blogs. This is returned as an IList.  I suppose when I'm looping through all content items, I can check whether they belong to the specific blog, BEFORE getting their categories.  But if I do Manager.GetContent(guid), I will get an IContent back, and I'm not sure that this is what I need.  Any additional pointers for this piece would be helpful.
    - Also, how would I get the list of possible Blog names and IDs (so that i can expose them in a public property or something)?

    "Rebind the whole Custom CategoriesThree control with the new datasource."
     - How do I do this?  There is no this.DataSource property available that I can see.  I'm probably looking at the wrong place.

    Thanks!
  6. Ivan Dimitrov
    Ivan Dimitrov avatar
    16072 posts
    Registered:
    25 Nov 2016
    08 Dec 2009
    Link to this post
    Hi Marko,

    Get all content items from your particular blog - you can expose a public property with a selector from where you pass the blog ID.

    BlogManager bManager = new BlogManager("Blogs");
    IBlog blog = bManager.GetBlog(new Guid("5B8A4C99-15A6-4FCC-9BC6-24672B66A688"));

    - Also, how would I get the list of possible Blog names and IDs (so that i can expose them in a public property or something)?

    Please take a look at this post - Working with selectors(WebEditors) part 3. Blog selector and Newsletters group selector.

    You need to override BindCategories() method and implement custom logic to get the categories shown only for certain IBlog.

    Kind regards,
    Ivan Dimitrov
    the Telerik team

    Instantly find answers to your questions on the new Telerik Support Portal.
    Watch a video on how to optimize your support resource searches and check out more tips on the blogs.
  7. Marko
    Marko avatar
    148 posts
    Registered:
    30 Jul 2008
    08 Dec 2009
    Link to this post
    Thanks.  OK, so this is what I got so far:

    using System; 
    using System.Collections; 
    using System.Collections.Generic; 
    using System.ComponentModel; 
    using Telerik.Blogs; 
    using Telerik.Blogs.Data; 
    using Telerik.Cms.Engine.Data; 
    using Telerik.Cms.Engine.WebControls.Categories; 
    using Telerik.Cms.Web.UI; 
    using Telerik.Web.UI; 
     
     
    [DefaultProperty("SelectedBlogs")] 
    public class CategoriesTreeExtended : CategoriesTree  
        internal class CategoryItem 
        { 
            private string _text; 
            private Guid _id; 
            private Guid _parentId; 
     
            public string Text 
            { 
                get { return _text; } 
                set { _text = value; } 
            } 
            public Guid ID 
            { 
                get { return _id; } 
                set { _id = value; } 
            } 
            public Guid ParentID 
            { 
                get { return _parentId; } 
                set { _parentId = value; } 
            } 
            public CategoryItem(Guid id, Guid parentId, string text) 
            { 
                _id = id; 
                _parentId = parentId; 
                _text = text; 
            } 
        } 
     
        private Guid[] selectedBlogIDs = new Guid[]{}; 
     
        [TypeConverter("Telerik.Blogs.WebControls.SelectedBlogsConverter, Telerik.Blogs")] 
        [WebEditor("Telerik.Blogs.WebControls.BlogsSelector, Telerik.Blogs")] 
        public Guid[] SelectedBlogs 
        { 
            get { return this.selectedBlogIDs; } 
            set { this.selectedBlogIDs = value; } 
        } 
     
        protected override void BindCategories() 
        { 
            IList selectedBlogs; 
            IList allBlogPosts; 
            IList allCategories; 
     
            BlogManager bManager = new BlogManager("Blogs"); 
            if (this.selectedBlogIDs.Length == 0) 
                selectedBlogs = bManager.GetBlogs(); 
            else 
                selectedBlogs = bManager.GetBlogs(selectedBlogIDs); 
     
            allCategories = Manager.GetCategories(0, 0, "CategoryName ASC"); 
            List<CategoryItem> filteredCategories = new List<CategoryItem>(); 
            List<string> usedCategories = new List<string>(); 
     
            string blogPostCategory = string.Empty; 
            foreach (Blog blog in selectedBlogs) 
            { 
                allBlogPosts = blog.Posts; 
                foreach (CmsContentBase blogPost in allBlogPosts) 
                { 
                    blogPostCategory = blogPost.GetMetaData("Category"as string
                    if (!usedCategories.Contains(blogPostCategory)) 
                        usedCategories.Add(blogPostCategory); 
                } 
            } 
     
            foreach (Category category in allCategories) 
            { 
                if (usedCategories.Contains(category.CategoryName)) 
                { 
                    filteredCategories.Add(new CategoryItem(category.ID, category.ParentCategoryID, category.CategoryName)); 
                } 
            } 
     
            RadTreeView categoriesTree = this.CategoriesTreeView; 
            categoriesTree.DataTextField = "Text"
            categoriesTree.DataFieldID = "ID"
            categoriesTree.DataFieldParentID = "ParentID"
            categoriesTree.DataValueField = "ID"
     
            categoriesTree.DataSource = filteredCategories; 
            categoriesTree.DataBind(); 
        } 
     

    However, for some reason, this works properly if I leave the SelectedBlogs public property to its default (which results in all blogs being crawled).  However, if I select specific blogs, it works for CERTAIN blogs, but not for others.  I can't figure out why.  For example, if I have 3 blogs (Blog A, Blog B, and Blog C).  If, using my public property SelectedBlogs I set it to Blog A, the rendered TreeView is empty (I know Blog A has posts in it and they use certain categories).  If I select Blog B, it's filled with only the categories from Blog B, which is correct behavior.  But this is the tricky one: if I select Blog A + Blog B, the TreeView is filled with categories from BOTH blogs. So why would the categories from blog A not show up when Blog A is the only one selected?

    I was debugging the code, and if I put a break point on the line "categoriesTree.DataSource = filteredCategories;" I can see that filteredCategories object is filled with the right categories, but for some reason, they never render on screen.

    Any thoughts?

    Also, does my approach to filling the DataSource with a custom internal object CategoryItem seem like a good way to go?
  8. Ivan Dimitrov
    Ivan Dimitrov avatar
    16072 posts
    Registered:
    25 Nov 2016
    09 Dec 2009
    Link to this post
    Hello Marko,

    I created a blog posts with the correct implementation. Please take a look at Filter CategoriesTree control by Blog. Let me know if there are any problems, so I could fix them. I hope this helps.


    Best wishes,
    Ivan Dimitrov
    the Telerik team

    Instantly find answers to your questions on the new Telerik Support Portal.
    Watch a video on how to optimize your support resource searches and check out more tips on the blogs.
    Answered
  9. Marko
    Marko avatar
    148 posts
    Registered:
    30 Jul 2008
    09 Dec 2009
    Link to this post
    Thanks for writing that!  I didn't realize that i could be using IList<ICategory> instead of creating that custom internal class.  So much simpler!

    A couple of remaining things...

    1. It seems that this piece of code....
    this.CategoriesTreeView.NodeDataBound += new Telerik.Web.UI.RadTreeViewEventHandler(CategoriesTreeView_NodeDataBound); 
    +
    void CategoriesTreeView_NodeDataBound(object sender, Telerik.Web.UI.RadTreeNodeEventArgs e) 
        ICategory nodeCategory = (ICategory)e.Node.DataItem; 
        e.Node.NavigateUrl = base.GetNodeUrl(nodeCategory.CategoryName, nodeCategory.ID.ToString()); 

    ...can be replaced with:
    this.CategoriesTreeView.DataValueField = "ID"

    Unless I'm missing something else that's important in the approach you took with adding the event handler (and please let me know if I am), I would prefer the simpler approach with less code.  They both seem to generate the category link properly and pass the category ID as the BlogCatID url parameter.

    2. Hierarchy wasn't working properly, and categories that are subcategories of other ones were all shown at the same level.  Looking around, I think that this is what needs to change:
    //this.CategoriesTreeView.DataFieldParentID = "CategoryID"; 
    //change to: 
    this.CategoriesTreeView.DataFieldParentID = "ParentCategoryID"
    After I did that, the categories and subcategories were shown correctly, with expandable/collapsible +/- thing.  Did you mean to use "ParentCategoryID" instead of "ParentID" or did you have a reason for using "ParentID"?

    3. HOWEVER, after changing the above to "ParentCategoryID", this is where I get the same issue as with my original code: for certain blogs, the TreeView still renders EMPTY, while for others, it works fine.  Then I realized that the problem is caused when a blog uses categories that are SUBcategories of another category, but they don't use any parent (1st level) categories.  For example, if I have these categories:

    -- Category A
    ------- SubCat A1
    ------- SubCat A2
    ------- SubCat A3
    -- Category B
    -- Category C

    If I have a Blog that uses only SubCat A1, SubCat A2, and SubCat A3, the TreeView does NOT render anything (blank, only <UL> tags show up in rendered HTML).  But if at least one of the blog posts uses the parent category Category A, then the TreeView renders properly.

    Any thoughts as to why this is happening?
  10. Ivan Dimitrov
    Ivan Dimitrov avatar
    16072 posts
    Registered:
    25 Nov 2016
    09 Dec 2009
    Link to this post
    Hi Marko,

    Ok I figured it out.

    1. You can use this.CategoriesTreeView.DataFieldParentID = "ParentCategoryID"; if you do not want to work with events.

    2. To fix the problem with the RadTreeView modify the code as below:

    if(!string.IsNullOrEmpty(postCategory))
                        {
                            ICategory cat = Manager.GetCategory(postCategory);
                            if(!filteredList.Contains(cat))
                            {
                                 filteredList.Add(cat);
                            }
                            var parentId = cat.ParentCategoryID;
                            while (parentId != Guid.Empty)
                            {
                                ICategory cat1 = Manager.GetCategory(parentId);
                                if (!filteredList.Contains(cat1))
                                    filteredList.Add(cat1);
                                parentId = cat1.ParentCategoryID;
                            }
                        }


    All the best,
    Ivan Dimitrov
    the Telerik team

    Instantly find answers to your questions on the new Telerik Support Portal.
    Watch a video on how to optimize your support resource searches and check out more tips on the blogs.
  11. Marko
    Marko avatar
    148 posts
    Registered:
    30 Jul 2008
    09 Dec 2009
    Link to this post
    I'm primarily interested in Blogs, so I guess I'm fine if this doesn't quite work as expected for Events (I'll just use the regular CategoriesTree control for that).

    The fix worked for getting the proper hierarchy.  However, i was wondering if we can take it one step further.  if you observe the regular behavior of the regular CategoriesTree control, when ShowRootCategory is set to FALSE, the rendered TreeView will only show root category when THAT category itself was used in a blog post.  If you set ShowRootCategory to TRUE, then it will show the root category NO MATTER WHAT.  i think your last modification above basically does what the regular CategoriesTree does when ShowRootCategory = TRUE.

    So, now I'm trying to figure out how to get it to work properly when ShowRootCategory = FALSE.  I thought I would loop through all of the filtered categories AFTER they have been all collected, and check each one to make sure it EITHER:
        a.) has a parent category in the list (like you were doing already), but only if ShowRootCategory = True,
    OR
        b.) has its ParentCategoryID set to empty Guid, only if ShowRootCategory = False AND the parent is NOT found in the list.  This would make it a first-level node in the TreeView, I'm guessing, making it appear like it doesn't have a parent.

    So I started to do this, but I ran into a problem (I commented out the last thing you added, and moved it further down, after the loops):
       ...
     ...
    1       ICategory cat = Manager.GetCategory(postCategory); 
    2       if (!filteredList.Contains(cat)) 
    3       {
    4           filteredList.Add(cat); 
    5       } 
    6       //var parentId = cat.ParentCategoryID; 
    8       //while (parentId != Guid.Empty) 
    9       //{ 
    10       //    ICategory cat1 = Manager.GetCategory(parentId); 
    11       //    if (!filteredList.Contains(cat1)) 
    12       //        filteredList.Add(cat1); 
    13       //    parentId = cat1.ParentCategoryID; 
    14       //} 
    15       ... 
    16       ... 
    17    } //end foreach blogpost 
    18 //end foreach blog 
    19  
    20 //loop through all filtered categories and adjust things as needed: 
    21 foreach (ICategory cat in filteredList) 
    22
    23     var parentId = cat.ParentCategoryID; 
    24     if (parentId != Guid.Empty) 
    25     { 
    26         ICategory cat1 = Manager.GetCategory(parentId); 
    27         if (!filteredList.Contains(cat1)) 
    28         { 
    29             if (this.ShowRootCategory) 
    30                 filteredList.Add(cat1); 
    31             else 
    32                 cat.ParentCategoryID = Guid.Empty;  //the causes an error "You cannot change a value of an object that is not in transaction." 
    33         } 
    34     } 
    35

    It seems that it won't let me change the ParentCategoryID because it's probably trying to do this on the actual category that's stored in Sitefinity, and not a copy of it that would be used for the TreeView.  Is that right?

    I suppose I can go back to using the custom class object (or some other data structure, like DataTable) to capture and store the CategoryName, ID, and CategoryParentID, so that I can manipulate the values BEFORE they get handed off to TreeView.DataSource, but I'm afraid that this is a lot more expensive operation than using native IList<ICategory>.  I can give it a try and compare the speed of both approaches, I suppose.

    Any thoughts?  Am I making sense with what I'm trying to do?

  12. Ivan Dimitrov
    Ivan Dimitrov avatar
    16072 posts
    Registered:
    25 Nov 2016
    09 Dec 2009
    Link to this post
    Hi Marko,

     I do not think that this is a good way to do it, because you are adding one loop more, then you are trying to set the parent ID and alter the structure, which actually causes the error that you cannot change the value of an object that is not in a transaction. The last error could be avoided if you get the item, then modify it and finally save it to commit the Nolics transaction. Also the RootCategory is passed directly to the manager and then the child elements are loaded from this level.


    Kind regards,
    Ivan Dimitrov
    the Telerik team

    Instantly find answers to your questions on the new Telerik Support Portal.
    Watch a video on how to optimize your support resource searches and check out more tips on the blogs.
  13. Marko
    Marko avatar
    148 posts
    Registered:
    30 Jul 2008
    09 Dec 2009
    Link to this post
    "I do not think that this is a good way to do it, because you are adding one loop more, then you are trying to set the parent ID and alter the structure..."

    I tried to do it earlier in code, within the other loops where you had it, but it was impossible to know at that point whether to forcibly add a parent category or change ParentCategoryID, because the main loop hasn't finished looping through all blogs and blog posts yet at that point.  If ShowRootCategory = False, then I do not want to always add the Parent category, UNLESS one of the blog posts actually uses it in its metadata.  So you see my dilemma? That's why I did it in another loop at the end, because I had to know about all of the "filtered" categories first... But I don't like that i have to loop through it and alter it after it's already been put together--I agree with you on that.  I'll have to think some more about that...

    "Also the RootCategory is passed directly to the manager and then the child elements are loaded from this level."
    So, does the ShowRootCategory property only apply when you use some value in the RootCategory filter?

    EDIT:  By the way, the code you used in your blog doesn't sort the resulting filtered categories, so I changed a couple of things:
    //Instead of 
    //IList<ICategory> filteredList = new List<ICategory>(); 
     
    //I used 
    SortedList<string, ICategory> filteredList = new SortedList<string, ICategory>(); 

    Then...
    //instead of doing 
    //if (!filteredList.Contains(cat)) 
     
    //I had to do 
    if (!filteredList.ContainsValue(cat)) 
     
    //and 
    filteredList.Add(cat.CategoryName, cat); 

    And finally...
    //Instead of 
    this.CategoriesTreeView.DataSource = filteredList; 
     
    //I had to do 
    this.CategoriesTreeView.DataSource = filteredList.Values; 

    This worked pretty well and the resulting list is sorted based on category name.
  14. Ivan Dimitrov
    Ivan Dimitrov avatar
    16072 posts
    Registered:
    25 Nov 2016
    11 Dec 2009
    Link to this post
    Hi Marko,

    Modify BindCategories method as shown below

    protected override void BindCategories()
    {
        IList categories;
     
        blogManager = new BlogManager("Blogs");
        if (this.selectedBlogs.Length == 0)
            // call the base class we will not change anything.
            base.BindCategories();
        else
        {
            IList allCategories = new List<ICategory>();
            if (string.IsNullOrEmpty(RootCategory))
            {
                allCategories = blogManager.Content.GetCategories(0, 0, "CategoryName ASC");
            }
            else
            {
                ICategory rootCategory = blogManager.Content.GetCategory(RootCategory);
                allCategories = blogManager.Content.GetCategoriesTree(rootCategory);
            }
            IList allselectedBlogs = blogManager.GetBlogs(selectedBlogs);
            IList<ICategory> filteredList = new List<ICategory>();
            foreach (IBlog blog in allselectedBlogs)
            {
                if (blog.PostsCount > 0)
                {
                    IList blogposts = blog.Posts;
                    foreach (IContent cnt in blogposts)
                    {
                        string postCategory = (string)cnt.GetMetaData("Category");
                        if (!string.IsNullOrEmpty(postCategory))
                        {
                            ICategory cat = Manager.GetCategory(postCategory);
                            if (!filteredList.Contains(cat) && allCategories.Contains(cat))
                            {
                                filteredList.Add(cat);
                            }
                            var parentId = cat.ParentCategoryID;
                            while (parentId != Guid.Empty)
                            {
                                ICategory cat1 = Manager.GetCategory(parentId);
                                if (!filteredList.Contains(cat1))
                                    filteredList.Add(cat1);
                                parentId = cat1.ParentCategoryID;
                            }
                        }
                    }
                }
            }
             
          
        this.CategoriesTreeView.DataTextField = "CategoryName";
        this.CategoriesTreeView.DataFieldID = "ID";
        this.CategoriesTreeView.DataValueField = "ID";
        this.CategoriesTreeView.DataFieldParentID = "ParentCategoryID";
        this.CategoriesTreeView.DataSource = filteredList;
        this.CategoriesTreeView.NodeDataBound +=new Telerik.Web.UI.RadTreeViewEventHandler(CategoriesTreeView_NodeDataBound);
        this.CategoriesTreeView.DataBind();
     
        }
     
    }


    Best wishes,
    Ivan Dimitrov
    the Telerik team

    Instantly find answers to your questions on the new Telerik Support Portal.
    Watch a video on how to optimize your support resource searches and check out more tips on the blogs.
  15. Marko
    Marko avatar
    148 posts
    Registered:
    30 Jul 2008
    11 Dec 2009
    Link to this post
    That doesn't really address what I was talking about.  Also, the resulting categories tree is not sorted properly.  I addressed that with the code example in my previous post by using a SortedList<string, ICategory> (see above).  But I decided that what I have so far (and thanks to all your help, Ivan), will be sufficient enough for this job.

    As far as performance goes, at this point I'm not seeing a huge problem with the extended CategoriesTree, so I think I'll keep it.  The blog is not likely to have a great number of posts and categories, so it will probably be manageable for a while.

    Thank you for all your help!
  16. Marko
    Marko avatar
    148 posts
    Registered:
    30 Jul 2008
    11 Dec 2009
    Link to this post
    Btw, there is one more thing that I added which I think is needed: when a category is clicked (in categories tree), the resulting filtered page doesn't highlight the previously-clicked category.  So I added this:

    //first, add a global variable 
    private string categoryUrlParam = string.Empty; 

    //then, override OnLoad in order to grab the URL parameter value  when the page is loaded
    protected override void OnLoad(EventArgs e) 
        base.OnLoad(e); 
        this.categoryUrlParam = this.Page.Request.QueryString[this.CategoryKey]; 

    //and finally, set the selected node when appropriate 
    protected void CategoriesTreeView_NodeDataBound(object sender, Telerik.Web.UI.RadTreeNodeEventArgs e) 
        if (this.CategoriesTreeView.SelectedNode == null && !string.IsNullOrEmpty(this.categoryUrlParam)) 
        { 
             ICategory nodeCategory = (ICategory)e.Node.DataItem;  
                 
             if ( (this.CategoryKeyType == CategoryKeyTypes.ID &&  
                   nodeCategory.ID == new Guid(this.categoryUrlParam)) || 
                  (this.CategoryKeyType == CategoryKeyTypes.Name &&  
                   nodeCategory.CategoryName == this.categoryUrlParam) ) 
             { 
                  e.Node.Selected = true
             } 
        } 
    }  

    I didn't know if there was a more direct way of getting the URL parameter other than grabbing it straight from Request.QueryString["name"], but this worked.
Register for webinar
16 posts, 1 answered