Mrug
Mrug

Reputation: 501

Recursive List to nicely formatted table

I have hit a slightly complicated problem that I am finding it hard to wrap my head around.

I have a class:

 public class Location
 {
    public string Name { get; set; }

    public List<Location> ChildLocations { get; set; }
 }

It again has reference to Child Location objects. So this is a recursive List and I want to output this list in a nicely formatted HTML table. This is the code:

using System.Collections.Generic;
using System.Web.Mvc;

namespace WebApplication1.Controllers
{
    public class Location
    {
        public string Name { get; set; }

        public List<Location> ChildLocations { get; set; }
    }

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            List<Location> locations = new List<Location>() {
                 new Location() {
                    Name = "Item 1",
                    ChildLocations = new List<Location>() {
                        new Location()
                        {
                            Name="Female",
                            ChildLocations = new List<Location>()
                            {
                                new Location() { Name = "Female 1" },
                                new Location() { Name = "Female 2" }
                            }

                        },
                        new Location()
                        {
                            Name="Male",
                            ChildLocations = new List<Location>()
                            {
                                new Location() { Name = "Male 1" },
                                new Location() { Name = "Male 2" }
                            }

                        }
                    }
                },
                 new Location() {
                    Name = "Item 2",
                    ChildLocations = new List<Location>() {
                        new Location()
                        {
                            Name="Female",
                            ChildLocations = new List<Location>()
                            {
                                new Location() { Name = "M1" },
                                new Location() { Name = "M2" },
                                new Location() { Name = "M3" }
                            }

                        },
                        new Location()
                        {
                            Name="Male",
                            ChildLocations = new List<Location>()                            

                        },
                        new Location()
                        {
                            Name="Unknown",
                            ChildLocations = new List<Location>()

                        }
                    }
                }
            };

            return View(locations);
        }
    }
}

and I want to make the output look something like this:

enter image description here

<table class="table table-border">
  <tr>
    <td rowspan="4">
      Item 1
    </td>
    <td rowspan="2">
      Female
    </td>   
    <td>
      Female 1
    </td>
  </tr> 
  <tr>
    <td>Female 2</td>
  </tr>
  <tr>
    <td rowspan="2">Male</td>
    <td>Male 1</td>
  </tr>
  <tr>
      <td>Male 2</td>    
  </tr>
  <tr>
    <td rowspan="5">
      Item 2      
    </td>
    <td rowspan="3">Female</td>
    <td>M1</td>
  </tr>
  <tr>
    <td>M2</td>
  </tr>
  <tr>
    <td>
      M3
    </td>
  </tr>
  <tr>
    <td>Male</td>
  </tr>
  <tr>
    <td>Unknown</td>
  </tr>
</table>

and if somebody wants to view the HTML I have the jsfiddle for it:

https://jsfiddle.net/4pk0necp/

I want to render this type of table using the data that I have pro grammatically in my cshtml:

@using WebApplication1.Controllers;
@model List<Location>
@{
    ViewBag.Title = "Home Page";
}

I understand that a function to calculate the rowspan would be needed so I created this tiny function which correctly returns the depth:

public static int GetDepth(this Location location)
    {
        int noOfchildren = 0;
        bool counted = false;
        foreach (Location item in location.ChildLocations)
        {
            if (item.ChildLocations.Count <= 0)
            {
                if (!counted)
                {
                    noOfchildren += location.ChildLocations.Where(i => i.ChildLocations.Count <= 0).Count();
                    counted = true;
                }
            }
            else
                noOfchildren += GetDepth(item);
        }

        return noOfchildren;
    }

The give list of Tree about is just a sample and there can be many levels of depth in the tree. Any help is appreciated.

Edit: I tweaked my GetDepth function since we only need leaf level nodes count.

Upvotes: 1

Views: 81

Answers (1)

Evan Trimboli
Evan Trimboli

Reputation: 30082

Try using the following class. Only barely tested:

public class TreeDrawer {

    private readonly Dictionary<Location, int> _depthMap;

    public TreeDrawer() {
        _depthMap = new Dictionary<Location, int>();
    }

    public string Draw(IEnumerable<Location> locations) {
        var sb = new StringBuilder("<table>");

        bool first = true;
        foreach (var l in locations) {
            Draw(l, sb, true, first);
            first = false;
        }

        sb.Append("</table>");

        return sb.ToString();
    }

    private void Draw(Location l, StringBuilder sb, bool fromRoot, bool first) {
        int depth = GetDepth(l);
        bool openedRow = false;
        if (fromRoot || !first) {
            sb.Append("<tr>");
            openedRow = true;
        }
        sb.Append("<td");
        if (depth > 1) {
            sb.Append(" rowspan=\"");
            sb.Append(depth);
            sb.Append("\"");
        }
        sb.Append(">");
        sb.Append(l.Name);
        sb.Append("</td>");

        bool isFirstChild = true;
        if (l.ChildLocations != null) {
            foreach (var child in l.ChildLocations) {
                Draw(child, sb, false, isFirstChild);
                isFirstChild = false;
            }
        }
        if (openedRow) {
            sb.Append("</tr>");
        }

    }

    private int GetDepth(Location l) {
        if (!_depthMap.ContainsKey(l)) {
            _depthMap.Add(l, Math.Max(1, l.ChildLocations?.Sum(GetDepth) ?? 0));
        }
        return _depthMap[l];
    }
}

Upvotes: 2

Related Questions