Group list items into pages, rows and columns
You're closing tags only when a row or table is full. Is data always going to fit the table size?


if (i % labelsPerRow == (labelsPerRow - 1)) {
    @Html.Raw("&lt;/tr&gt;")
}

if (i % labelsPerPage == (labelsPerPage - 1)) {
    @Html.Raw("&lt;/table&gt;")
}



You could make a functional helper and use that instead. Notice that after the loop, the row and table are ensured to be closed. The purpose is to allow the consumer to pick how to build a TAggregate from TSource items and observer methods. We're observing every item, table start/end and row start/end.

public static class GridRenderer
{
    public static TAggregate Render&lt;TSource, TAggregate&gt;(IEnumerable&lt;TSource&gt; source, TAggregate seed, int pageSize, int rowSize,
        Func&lt;TSource, TAggregate, TAggregate&gt; itemObserver,
        Func&lt;TAggregate, TAggregate&gt; beginPageObserver,
        Func&lt;TAggregate, TAggregate&gt; endPageObserver,
        Func&lt;TAggregate, TAggregate&gt; beginRowObserver,
        Func&lt;TAggregate, TAggregate&gt; endRowObserver)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (pageSize &lt;= 0) throw new ArgumentOutOfRangeException(nameof(pageSize));
        if (rowSize &gt; pageSize) throw new ArgumentOutOfRangeException(nameof(rowSize));
        // .. check observers for null

        var items = source.ToList();
        var result = seed;

        if (!items.Any()) return result;

        for (var i = 0; i &lt; items.Count; i++)
        {
            if (i % pageSize == 0)
            {
                result = beginPageObserver(result);
            }

            if (i % rowSize == 0)
            {
                result = beginRowObserver(result);
            }

            result = itemObserver(items[i], result);

            if ((i + 1) % rowSize == 0)
            {
                result = endRowObserver(result);
            }

            if ((i + 1) % pageSize == 0)
            {
                result = endPageObserver(result);
            }
        }

        if (items.Count % rowSize != 0)
        {
            result = endRowObserver(result);
        }

        if (items.Count % pageSize != 0)
        {
            result = endPageObserver(result);
        }

        return result;
    }
}


Let's say your data does not fit the table size and row size.

var renderer = new StringBuilder();
GridRenderer.Render(new[] {1, 2, 3, 4, 5}, renderer, 4, 2,
    (item, cur) =&gt; cur.AppendLine($"    &lt;td class=\"label\"&gt;{item}&lt;/td&gt;"),
    cur =&gt; cur.AppendLine("&lt;table class=\"page\"&gt;"),
    cur =&gt; cur.AppendLine("&lt;/table&gt;"),
    cur =&gt; cur.AppendLine("  &lt;tr&gt;"),
    cur =&gt; cur.AppendLine("  &lt;/tr&gt;"));
var layout = renderer.ToString();


The grid still gets created correctly.

&lt;table class="page"&gt;
  &lt;tr&gt;
    &lt;td class="label"&gt;1&lt;/td&gt;
    &lt;td class="label"&gt;2&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td class="label"&gt;3&lt;/td&gt;
    &lt;td class="label"&gt;4&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;table class="page"&gt;
  &lt;tr&gt;
    &lt;td class="label"&gt;5&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;


Note I've used a StringBuilder, but you could also use any other class to render the output. You could also change the flow a bit if you want an empty table when no data is available.