3

I have a categories table which is set up to allow an infinite number of sub category levels. I would like to mimic the following: enter image description here

It should be clarified that sub categories can have sub categories. E.g. Parent cat -> level 1 -> level 2 -> level 3 etc.

My categories table has two columns, CategoryName and ParentID.

This list box will be used when assigning the correct category to a product.

How can I write this?

Edit

In response to thedugas I had to modify your answer to work with my situation. I found some errors that needed to be fixed, but below is a final, working solution.

protected void Page_Load(object sender, EventArgs e)
    {
        using (DataClasses1DataContext db = new DataClasses1DataContext())
        {

        var c = db.Categories.Select(x => x);

        List<Category> categories = new List<Category>();
        foreach (var n in c)
        {
            categories.Add(new Category()
            {
                categoryID = n.categoryID,
                title = n.title,
                parentID = n.parentID,
                isVisible = n.isVisible
            });
        }
        List<string> xx = new List<string>();

        foreach (Category cat in categories)
        {
            BuildCatString(string.Empty, cat, categories, xx);
        }

        ListBox1.DataSource = xx;
        ListBox1.DataBind();

    }
}

private void BuildCatString(string prefix, Category cat, IEnumerable<Category> categories, List<string> xx)
{
    if (cat.parentID == 0)
    {
        xx.Add(cat.title);
        prefix = cat.title;
    }

    var children = categories.Where(x => x.parentID == cat.categoryID);
    if (children.Count() == 0)
    {
        return;
    }

    foreach (Category child in children)
    {
        if(prefix.Any())
        {
        xx.Add(prefix + "/" + child.title);
        BuildCatString(prefix + "/" + child.title,
            child, categories, xx);
        }
    }

}

Here is the almost finished work: enter image description here

The Muffin Man
  • 19,585
  • 30
  • 119
  • 191
  • @Nick - Can a top level category's (CD-DVD-Video) subcategory (CD audio) also have another sub category? In other words, could you have - CD-DVD-Video/CD audio/Cases? – dugas Mar 18 '11 at 06:24
  • @thedugas, yes, it could have an infinite amount of levels. – The Muffin Man Mar 18 '11 at 06:27
  • This seems to be exactly the sort of situation for which tree controls were designed. Why use a list control? – Jerry Coffin Mar 18 '11 at 06:28
  • @Jerry Coffin, I need to assign products to categories, I believe a list control would be better considering the user can pick multiple categories. – The Muffin Man Mar 18 '11 at 06:30
  • you need to convert all results into the category list - you are only populating the ones with a parentid of 0 – dugas Mar 18 '11 at 18:37
  • @thedugas, I modified the code above so that it initially stores `List categories` with the entire table, but I'm still getting the same results. When the code goes to evaulate `var children = categories.Where(cats => cats.parentID == cats.categoryID);` it finds none, which is strange because there is rows that have a parentID which matches parent rows ID. – The Muffin Man Mar 18 '11 at 19:04
  • @Nick - in my example, I used the subset of parents only in main before looping, you are calling on all in your page load (which explains the difference in results (the need for the prefix check). You can reduce the computations by only looping in your page load through Categories that have a ParentId of 0, not all of them after you contruct your categories list. – dugas Mar 18 '11 at 20:04
  • I would note that you do not have an *infinite* number of subcategory levels. That is, you have no category which is actually 0/1/2/3/4/5/6/7/8/9/10/... on into infinity. What you actually have is a *finite but unbounded* number of category levels, right? Every category has a finite number of levels, but there is no bound on how high that finite number can be. – Eric Lippert Mar 24 '11 at 15:11
  • @Eric Lippert, You are correct. I can't think of any reason why the client would need to go down more than 4 or 5 levels, but if they do it is possible to go as deep as needed. In this particular situation the user can click on the parent category and see items right away and have the ability to drill down deeper if they want to filter the results.(Newegg is set up like this). – The Muffin Man Mar 24 '11 at 16:24

3 Answers3

7

Nick asked me in a comment to another question how this sort of problem might be solved using LINQ to Objects without using any recursion. Easily done.

Let's suppose that we have a Dictionary<Id, Category> that maps ids to categories. Each category has three fields: Id, ParentId and Name. Let's presume that ParentId can be null, to mark those categories that are "top level".

The desired output is a sequence of strings where each string is the "fully-qualified" name of the category.

The solution is straightforward. We begin by defining a helper method:

public static IEnumerable<Category> CategoryAndParents(this Dictionary<Id, Category> map, Id id)
{
    Id current = id;
    while(current != null)
    {
        Category category = map[current];
        yield return category;
        current = category.ParentId;
    }
}

And this helper method:

public static string FullName(this Dictionary<Id, Category> map, Id id)
{
    return map.CategoryAndParents(id)
              .Aggregate("", (string name, Category cat) =>
                cat.Name + (name == "" ? "" : @"/") + name);
}

Or, if you prefer avoiding the potentially inefficient naive string concatenation:

public static string FullName(this Dictionary<Id, Category> map, Id id)
{
    return string.Join(@"/", map.CategoryAndParents(id)
                                .Select(cat=>cat.Name)
                                .Reverse());
}

And now the query is straightforward:

fullNames = from id in map.Keys
            select map.FullName(id);
listBox.DataSource = fullNames.ToList();

No recursion necessary.

Community
  • 1
  • 1
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • This is very helpful. Thank you for taking the time to answer. – The Muffin Man Mar 22 '11 at 19:03
  • Why is `string.Join` inefficient here? Only reason I can think of is the `Reverse`... – configurator Mar 26 '11 at 16:59
  • @configurator: The naive string version is O(n^2) in time in the number of subcategories. The Join version is O(n) in extra memory due to the Reverse. Since the number of subcategories is likely to be small, it's probably a wash. – Eric Lippert Mar 26 '11 at 19:40
  • Oh, I misread your text - I read it as "Or, if you prefer the potentially inefficient naive string concatenation" which didn't make much sense... – configurator Mar 26 '11 at 19:46
3

I would say to use a recursive CTE in SQL if you can. Edit: here is a recursive CTE for MS SQL >= 2005:

; WITH cte AS (
select CategoryId, CategoryName, ParentId,
cast(CategoryName as varchar(max)) as xpath
from 
categories
where ParentId = 0
UNION ALL
select c.CategoryId, c.CategoryName, c.ParentId, 
cast(p.xpath + '/' + c.CategoryName as varchar(max)) as xpath
from categories c inner join cte p on p.CategoryId = c.ParentId
)
select xpath from cte 
order by xpath

If you can't then here is one way:

class Category
{
        public int ParentId { get; set; }
        public int CategoryId { get; set; }
        public string CategoryName { get; set; }

        public static void BuildCatStringList(string prefix, Category c, 
                           IEnumerable<Category> categories, List<string> catStrings)
        {
            if (c.ParentId == 0)
            {
                catStrings.Add(c.CategoryName);
                prefix = c.CategoryName;
            }

            var children = categories.Where(cats => cats.ParentId == c.CategoryId);
            if (children.Count() == 0)
            {
                return;
            }

            foreach (Category child in children)
            {
                catStrings.Add(prefix + "/" + child.CategoryName);
                BuildCatStringList(prefix + "/" + child.CategoryName, 
                                   child, categories, catStrings);
            }
}


static void Main(string[] args)
{
        List<Category> categories = new List<Category>();
        categories.Add(new Category() { ParentId = 0, 
            CategoryName = "CD-DVD-Video", CategoryId=1 });
        categories.Add(new Category() { ParentId = 1, 
            CategoryName = "DVD", CategoryId = 10 });
        categories.Add(new Category() { ParentId = 1, 
            CategoryName = "Video cassettes", CategoryId= 11 });

        categories.Add(new Category() { ParentId = 0, 
            CategoryName = "Computer Hardware", CategoryId= 2 });
        categories.Add(new Category() { ParentId = 2, 
            CategoryName = "CD & DVD", CategoryId = 12 });
        categories.Add(new Category() { ParentId = 2, 
            CategoryName = "CPU Coolers", CategoryId = 13 });
        categories.Add(new Category() { ParentId = 2, 
            CategoryName = "Cases", CategoryId = 14 });
        categories.Add(new Category() { ParentId = 2, 
            CategoryName = "Keyboards", CategoryId = 15 });

        List<String> x = new List<string>();
        foreach (Category cat in categories.Where(c => c.ParentId == 0))
        {                
            Category.BuildCatStringList(String.Empty, cat, categories, x);
        }

}
dugas
  • 12,025
  • 3
  • 45
  • 51
  • thanks for writing that code, it's very helpful, can you take a look at my edit above? – The Muffin Man Mar 18 '11 at 18:09
  • I finally got it working, I had to make some changes to work with my solution, and fix two errors, but if you would like to update your answer I will accept it, as you laid out the groundwork for me. Thank you. – The Muffin Man Mar 18 '11 at 19:30
  • it looks like you fixed the `children` query already, but in the children `foreach` loop you should have `if(prefix.Any())` to prevent the name of the sub category being listed by itself with no parent cat prefixing it. Regardless, thank you for your help, I couldn't of done it without your answer. – The Muffin Man Mar 18 '11 at 19:47
-1

Assuming the ParentID will be NULL for the Top Category. I would go for:

  • Hold the data in a dataset Order By ParentID , CategoryName. lets say dataset1

string MainCategory as string="";

For(int i=0;i<=dataset1.CategoryTable.Rows-1;i++)
{
   if (dataset1.CategoryTable[i]["ParentID"] == DBNull.value) 
   {
       MainCategory= Convert.Tostring(dataset1.CategoryTable[i]["CategoryName"]);
   }
   else
   {
       // Add to the list
       List1.Add(MainCategory + Convert.Tostring(dataset1.CategoryTable[i]["CategoryName"]));
   }
}
Nirmal
  • 1,223
  • 18
  • 32