ASP.NET MVC 4 generating a treeview with recursive partial view

user2082422 picture user2082422 · Jul 9, 2013 · Viewed 25.4k times · Source

I have a partial view in an MVC 4 project, which is strongly typed. It takes an IEnumerable collection of a table of a database. In that table there are IDs, Names, and ParentIDs for storing hierarchical connection between records. The view that calls the partial view is also strongly typed, it takes the whole database as the model, and passes the Categories table to the partial view, as an enumerable collection:

@Html.Partial("_TreeCategories", @Model.Categories.ToList())

And in the partial view, I want to take the root nodes first, so I can extend the whole tree in a recursive way. In the database table, all records are considered as root nodes with a ParentID == null.
So generally, my way to do this would look like:

@model IEnumerable<TreeCollections.OpenAccess.Category>

@if (Model.ToList().Count >= 0)
    {
    @if (Model.ToList()[0].Parent_id == null)
    {
        <text><ul id="navigation"></text>
    }

    @foreach (var node in @Model)
    {
        <li><a href="[email protected]">@node.Name</a>
            @foreach (var subNode in @Model.Where(s => s.Parent_id == node.Id))
            {
                @Html.Partial("_TreeCategories", subNode)
            }
        </li>
    }
    @if (Model.ToList()[0].Parent_id == null)
    {
        </ul>
    }
}

So I check if the first element's ParentID of the Model is null, and if it is, then it should create a < ul> tag with the id "navigation", so the jquery plugin can recognise that it is meant to be a treeview control. Then it creates a list tag with a recursive call within. The recursively called partial view takes the children of the node as the model. And lastly if we arrived to the end of the partial view's rendering, and we are at the "root level", it should write a closing < ul> tag
There are some problems, however. First, at the end, that closing unordered list tag is wrong, VS can't find the matching start tag for that. Second, I don't know why, but at the top, I can put the starter < ul> tag in between tags, and I can't do it at the closing tag below. But I'm not sure about these < ul > tags either, I feel those are wrong too.

Please, help me, I'm stuck with this for days now.

Answer

hubson bropa picture hubson bropa · Jul 9, 2013

man, you got some wonk going on here. i feel your pain on getting stuck.

see if this floats your boat.

you need a seed value to keep track of what you are looking for in the listing when you do recursion on the same list. it's better to do a parent children mapping in the class, but meh this was fun to do given your structure and should do the trick.

Models

namespace trash.Models
{
    public class Category
    {
        public int ID { get; set; }
        public int? Parent_ID { get; set; }
        public string Name {get; set;}
    }

    public class SeededCategories
    {
        public int? Seed { get; set; }
        public IList<Category> Categories { get; set; }
    }
}

Controller (NOTE: you start the recursion chain by setting the Seed property to null which will pick up all the null parents)

namespace trash.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            IList<trash.Models.Category> categories = new List<trash.Models.Category>();
            categories.Add(new trash.Models.Category { ID = 1, Parent_ID = null, Name = "Top1" });
            categories.Add(new trash.Models.Category { ID = 2, Parent_ID = null, Name = "Top2" });
            categories.Add(new trash.Models.Category { ID = 3, Parent_ID = 1, Name = "Top1Ring1" });
            categories.Add(new trash.Models.Category { ID = 4, Parent_ID = 1, Name = "Top1Ring2" });

            trash.Models.SeededCategories model = new Models.SeededCategories { Seed = null, Categories = categories };
            return View(model);
        }
    }
}

Views Index

@model trash.Models.SeededCategories

Here's a list
@Html.Partial("_TreeCategories", Model)

Partial (your _TreeCategories. NOTE: set the Seed to the current node ID and volia recursion)

@model trash.Models.SeededCategories

@if (Model.Categories.Where(s => s.Parent_ID == Model.Seed).Any())
{
    <ul>
        @foreach (var node in Model.Categories)
        {
            if (node.Parent_ID == Model.Seed)
            {
                trash.Models.SeededCategories inner = new trash.Models.SeededCategories { Seed = node.ID, Categories = Model.Categories };
            <li><a href="[email protected]">@node.Name</a>
                @Html.Partial("_TreeCategories", inner)
            </li>
            }
        }
    </ul>
}