Search code examples
c#linqrazor2sxc

Create a tree structure in linq with a single list source with parent - child as strings of an object


I start with a list Cats with this data:

Name______________ Parent__________Description_______________FilesCoded________CodingReferences

"Blue"____________ "1.Colors"______"whatever"________________10________________11

"Red"_____________ "1.Colors"______"this"____________________2_________________3

"LightBlue"_______ "Blue"__________"that"____________________3_________________4

"Square"__________ "3.Forms"_______""________________________3_________________6

"ClearBlue"_______ "LightBlue"_____""________________________0_________________0

I need to create this output:

1.Colors

____Blue

________LightBlue

____________ClearBlue

3.Forms

____Square

I use this code, which does the trick, but I have around 15 to 20 levels on the tree, which makes the code ugly and highly inefficient. Is there a way to make this:

  1. Simpler, with a function looping through the levels instead of copying the code each time;
  2. Faster. The table is quite large, so this is getting slower each level added.

Current Code:

@{
    var Cats = AsList(App.Data["Categories"]).OrderBy(o => o.Parent).ThenBy(oo => oo.Name);
    // List of objects with: Name, Parent, Description, and other fields.
    var FirstLevelTree = Cats.Where(a => Char.IsNumber(a.Name[0])).Select(s => s.Name).Distinct();
}

@functions{
    public CategoryInfo setCatInfo (string catName)
    {
        var Cats = AsList(App.Data["Categories"]);
        
        var returnInfo = new CategoryInfo();
        returnInfo.desc = "Error: empty string";
        returnInfo.info = "Error: empty string";
        
        var checkCat = Cats.Where(n => n.Name == catName);
        if (checkCat.Count() != 1)
        {
            returnInfo.info = "Error: no such cat in list";
            returnInfo.desc = "Error: no such cat in list";
        } else {
            var thisCat = checkCat.First();
            returnInfo.info = thisCat.Name + "somethingelse";
            returnInfo.desc = thisCat.Description + "alsosomethingelse";
        }
        return returnInfo;
    }
    
    public class CategoryInfo {
        public string info {get;set;}
        public string desc {get;set;}
    }
}

<div id="myCats">

    @foreach(var top in FirstLevelTree)
    {
        <p>@top</p>
        var setlvlOne = Cats.Where(a => a.Parent == top);
        foreach(var lvlOne in setlvlOne)
        {
            var getLvlOneInfo = setCatInfo(lvlOne.Name);
            <p style="margin-left: 20px;">@Html.Raw(getLvlOneInfo.info)</p>
               if (!String.IsNullOrEmpty(getLvlOneInfo.desc))
               {
                    <p style="margin-left: 30px;">@Html.Raw(getLvlOneInfo.desc)</p>
               }
            var setlvlTwo = Cats.Where(b => b.Parent == lvlOne.Name);
            foreach(var lvlTwo in setlvlTwo)
            {
                var getLvlTwoInfo = setCatInfo(lvlTwo.Name);
                <p style="margin-left: 40px;">@Html.Raw(getLvlTwoInfo.info)</p>
                   if (!String.IsNullOrEmpty(getLvlTwoInfo.desc))
                   {
                        <p style="margin-left: 50px;">@Html.Raw(getLvlTwoInfo.desc)</p>
                   }
                var setlvlThree = Cats.Where(c => c.Parent == lvlTwo.Name);
                foreach(var lvlThree in setlvlThree)
                {
                    var getLvlThreeInfo = setCatInfo(lvlThree.Name);
                    <p style="margin-left: 60px;">@Html.Raw(getLvlThreeInfo.info)</p>
                       if (!String.IsNullOrEmpty(getLvlThreeInfo.desc))
                       {
                            <p style="margin-left: 70px;">@Html.Raw(getLvlThreeInfo.desc)</p>
                       }
                    var setlvlFour = Cats.Where(d => d.Parent == lvlThree.Name);
                    foreach(var lvlFour in setlvlFour)
                    {
                        var getLvlFourInfo = setCatInfo(lvlFour.Name);
                         <p style="margin-left: 80px;">@Html.Raw(getLvlFourInfo.info)</p>
                           if (!String.IsNullOrEmpty(getLvlFourInfo.desc))
                           {
                                <p style="margin-left: 90px;">@Html.Raw(getLvlFourInfo.desc)</p>
                           }
                        var setlvlFive = Cats.Where(e => e.Parent == lvlFour.Name);
                        foreach(var lvlFive in setlvlFive)
                        {
                            var getLvlFiveInfo = setCatInfo(lvlFive.Name);
                            <p style="margin-left: 100px;">@Html.Raw(getLvlFiveInfo.info)</p>
                               if (!String.IsNullOrEmpty(getLvlFiveInfo.desc))
                               {
                                    <p style="margin-left: 110px;">@Html.Raw(getLvlFiveInfo.desc)</p>
                               }
                        }
                    }
                }
            }
        }  
    }
</div>

Solution

  • Simpler

    Yes. Create a Razor Helper that calls itself recursively. That should allow you to avoid all the duplicative code in your example, and it'll support a basically infinite maximum depth without requiring any more code changes.

    Faster

    It depends on what's taking all the time.

    As you get more levels, presumably you're just generating a lot more HTML. That'll take more time server-side, and it'll also bog down the browser. To avoid that kind of problem, you may need to look at only loading in the parts that the user is actually interested in. For example, you can create a collapsed structure that only loads in data from deeper nodes as users drill down into it.

    I'd also pay close attention to what may be happening in the code you haven't provided.

    • Is setCatInfo an expensive operation?
    • What is backing the Cats collection? If it's using deferred execution, like an Entity Framework DbSet, you may be doing a separate round-trip each time you iterate over the results of a Cats.Where(...) call.

    Either way, executing Cats.Where() all over the place is giving your page generation an O(n²) algorithmic complexity. I'd suggest collecting all the categories into a lookup, grouped by their Parent:

    var catsByParent = Cats.ToLookup(c => c.Parent);
    

    This is a one-time O(log n) operation, and ever after that point you should be able to get the categories with a given parent much more quickly.

    var thisLevel = catsByParent[parentLevel.Name];
    

    Side notes

    • Using @Html.Raw() is a big code smell. Most likely you just want @thisLevel.desc. If @thisLevel.desc is guaranteed to be safe for injecting directly into your HTML, it should be an IHtmlString.
    • Rather than putting all the HTML elements at the same level and using styling to nest them, consider using a nested HTML structure and a common CSS rule that makes each layer of that nested structure get indented a little bit from the previous one.