DEV Community

Cover image for The Fastest Way to Generate a Table from a Collection.
Serhii Korol
Serhii Korol

Posted on

The Fastest Way to Generate a Table from a Collection.

Developers frequently encounter the task of displaying data in a table. Typically, this is handled on the frontend by iterating through data using loops. However, there’s an alternative approach: generating the table on the backend, eliminating the need for complex looping logic.

To simplify this process, I’ve developed a new NuGet package called ListToTable. This package is a fork of an existing one, but I’ve enhanced it with additional features and improvements. In this post, I’ll walk you through how it works and demonstrate its practical applications.

Preparations

To get started, create a simple ASP.NET MVC project using the default templates. Once your project is set up, install the ListToTable NuGet package to begin leveraging its functionality.

dotnet add package ListToTable --version 1.0.1
Enter fullscreen mode Exit fullscreen mode

Implementation

I’ll be retrieving data from a public API, which you can explore by visiting this link: http://universities.hipolabs.com/. The API returns a response in the form of an array containing simple models, making it easy to work with and integrate into your application.

[
  {
    "web_pages": [
      "https://www.fscj.edu/"
    ],
    "state-province": null,
    "alpha_two_code": "US",
    "name": "Florida State College at Jacksonville",
    "country": "United States",
    "domains": [
      "fscj.edu"
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode

However, some fields in the API response contain multiple values. To display this data properly in the table, we’ll need to join these values and handle any potential NULL entries. I’ll address this by implementing the necessary logic directly in the model.

public sealed class University
{
    [JsonPropertyName("name")]
    public required string Name { get; set; }
    [JsonPropertyName("alpha_two_code")]
    public required string AlphaTwoCode { get; set; }

    [JsonPropertyName("web_pages")]
    public required IEnumerable<string> WebPagesList { get; set; }

    public string WebPages => WebPagesList.Any() ? string.Join(", ", WebPagesList) : "No web pages available";

    [JsonPropertyName("state-province")]
    public string? StateProvince { get; set; }

    public string? Province => string.IsNullOrEmpty(StateProvince) ? "N/A" : StateProvince;

    [JsonPropertyName("country")]
    public required string Country { get; set; }

    [JsonPropertyName("domains")]
    public required IEnumerable<string> DomainsList { get; set; }

    public string Domains => DomainsList.Any() ? string.Join(", ", DomainsList) : "No domains available";
}
Enter fullscreen mode Exit fullscreen mode

You have the option to create a separate small model for different countries. This approach allows you to filter and select specific countries, avoiding the need to deserialize irrelevant data. However, this isn’t ideal because the API request can be quite heavy. As a more efficient alternative, you can use a static collection of countries to achieve the same result without the overhead of unnecessary data processing.

public sealed record Country
{
    [JsonPropertyName("country")]
    public required string CountryName { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now go to the Index view file and add this markup:

@{
    var currentPage = ViewBag.CurrentPage as int? ?? 1;
    var pageSize = ViewBag.PageSize as int? ?? 10;
    var currentCountry = ViewBag.CurrentCountry as string ?? "United States";
    var countries = ViewBag.Countries as List<string> ?? new List<string>();
}

<h1>Universities</h1>

<form method="get" action="@Url.Action("Index")">
    <label for="country">Select Country:</label>
    <select id="country" name="country" onchange="this.form.submit()">
        @foreach (var country in countries)
        {
            <!option value="@country" @(country == currentCountry ? "selected" : "")>@country</!option>
        }
    </select>
    <input type="hidden" name="page" value="1" />
    <input type="hidden" name="pageSize" value="@pageSize" />
</form>
<hr/>
<div>
    @Html.Raw(ViewBag.TableHtml)
</div>

<hr/>
<div>
    <form method="get" action="@Url.Action("Index")">
        <input type="hidden" name="country" value="@currentCountry" />
        <input type="hidden" name="pageSize" value="@pageSize" />

        <button type="submit" name="page" value="@(currentPage - 1)" @(currentPage == 1 ? "disabled" : "")>&laquo; Previous</button>
        <span>Page @currentPage</span>
        <button type="submit" name="page" value="@(currentPage + 1)">Next &raquo;</button>
    </form>
</div>
Enter fullscreen mode Exit fullscreen mode

The markup is designed to render the list of countries, display the table, and handle pagination.

Next, I’ll update the Index method in the HomeController. Locate this method and streamline its logic. Since we’re implementing pagination, the method will need to accept parameters for the current page, page size, and the selected country to filter the data accordingly.

public async Task<IActionResult> Index(string country = "United States", int page = 1, int pageSize = 10) {}
Enter fullscreen mode Exit fullscreen mode

Next, compute the offset parameter, which is essential for handling pagination requests. The offset determines the starting point for the data to be fetched, ensuring the correct subset of results is returned based on the current page and page size.

int offset = (page - 1) * pageSize;
Enter fullscreen mode Exit fullscreen mode

Send a request to the API and retrieve the response in JSON format. This data will be used to populate the table and support further processing.

using var client = new HttpClient();
        var result =
            await client.GetAsync(
                $"http://universities.hipolabs.com/search?country={Uri.EscapeDataString(country)}&limit={pageSize}&offset={offset}");
        var json = await result.Content.ReadAsStringAsync();
Enter fullscreen mode Exit fullscreen mode

Deserialize this JSON.

List<University>? universities = JsonSerializer.Deserialize<List<University>>(json);
Enter fullscreen mode Exit fullscreen mode

You have the flexibility to either fetch all available countries from the API or use a predefined static list of countries. This allows you to tailor the data to your specific needs while avoiding unnecessary overhead.

var countryResult = await client.GetAsync("http://universities.hipolabs.com/search");
        var countryJson = await countryResult.Content.ReadAsStringAsync();
        List<Country>? allUniversities = JsonSerializer.Deserialize<List<Country>>(countryJson);
        var countries = allUniversities?.Select(u => u.CountryName).Distinct().OrderBy(c => c).ToList() ??
                        new List<string>();

Enter fullscreen mode Exit fullscreen mode

Generate the table using the retrieved data. This step will transform the structured information into a readable and organized format for display.

var html = Table<University>.Add(universities).ToHtml();
Enter fullscreen mode Exit fullscreen mode

Assign the necessary variables with the processed data and return the View. This ensures the data is passed to the frontend for rendering.

ViewBag.TableHtml = html;
ViewBag.CurrentCountry = country;
ViewBag.Countries = countries;
ViewBag.CurrentPage = page;
ViewBag.PageSize = pageSize;

return View();
Enter fullscreen mode Exit fullscreen mode

If you run the application, you’ll notice that the table is functional but lacks visual appeal. Don’t worry—this is just the starting point, and we’ll enhance its appearance in the next steps.

ugly table

Let’s address this issue. To improve the table’s readability, we’ll exclude columns that contain complex or multiple data points, as they don’t fit well in a tabular format. We’ll also modify the table generator to include filters, ensuring the displayed data is clean and relevant.

var html = Table<University>.Add(universities)
            .FilterColumns([
                nameof(University.DomainsList), nameof(University.WebPagesList), nameof(University.StateProvince)
            ])
            .ToHtml();
Enter fullscreen mode Exit fullscreen mode

Now, the page looks much cleaner and more organized. The adjustments we made have significantly improved both the functionality and visual appeal of the table.

filtered table

The table currently uses default styles. If you’d like to customize its appearance, you can easily do so by adding the following code to apply your own styles:

var html = Table<University>.Add(universities)
            .Style(new StyleSettings
            {
                StyleType = StyleType.Header,
                Properties =
                {
                    { "background-color", "green" },
                    { "text-align", "center" },
                    { "color", "white" },
                    { "padding", "6px" },
                    { "border", "3px solid #dddddd" },
                    { "font-family", "sans-serif" },
                    { "font-size", "16px" }
                }
            })
            .FilterColumns([
                nameof(University.DomainsList), nameof(University.WebPagesList), nameof(University.StateProvince)
            ])
            .ToHtml();
Enter fullscreen mode Exit fullscreen mode

We’ve applied custom styles to the table headers, enhancing their appearance and making the table more visually appealing.

styled header

If you’d like to customize the entire table’s appearance, you can achieve this by using the following code:

var html = Table<University>.Add(universities)
            .Style(new StyleSettings
            {
                StyleType = StyleType.Header,
                Properties =
                {
                    { "background-color", "green" },
                    { "text-align", "center" },
                    { "color", "white" },
                    { "padding", "6px" },
                    { "border", "3px solid #dddddd" },
                    { "font-family", "sans-serif" },
                    { "font-size", "16px" }
                }
            })
            .Style(new StyleSettings { StyleType = StyleType.Table, Properties =
            {
                { "border-collapse", "collapse" },
                { "border", "5px solid #dddddd" },
                { "margin-left", "auto" },
                { "margin-right", "auto" },
                {"width", "90%"}
            }})
            .FilterColumns([
                nameof(University.DomainsList), nameof(University.WebPagesList), nameof(University.StateProvince)
            ])
            .ToHtml();
Enter fullscreen mode Exit fullscreen mode

styled table

There’s a small detail to address: by default, the content is aligned to the right, which may look misaligned. Fortunately, we can easily fix this by adjusting the alignment.

var html = Table<University>.Add(universities)
            .Style(new StyleSettings
            {
                StyleType = StyleType.Header,
                Properties =
                {
                    { "background-color", "green" },
                    { "text-align", "center" },
                    { "color", "white" },
                    { "padding", "6px" },
                    { "border", "3px solid #dddddd" },
                    { "font-family", "sans-serif" },
                    { "font-size", "16px" }
                }
            })
            .Style(new StyleSettings { StyleType = StyleType.Table, Properties =
            {
                { "border-collapse", "collapse" },
                { "border", "5px solid #dddddd" },
                { "margin-left", "auto" },
                { "margin-right", "auto" },
                {"width", "90%"}
            }})
            .FilterColumns([
                nameof(University.DomainsList), nameof(University.WebPagesList), nameof(University.StateProvince)
            ])
            .ColumnContentTextJustification(new Dictionary<string, TextJustification>
            {
                { nameof(University.Name), TextJustification.Centered },
                { nameof(University.AlphaTwoCode), TextJustification.Centered },
                { nameof(University.WebPages), TextJustification.Centered },
                { nameof(University.Province), TextJustification.Centered },
                { nameof(University.Country), TextJustification.Centered },
                { nameof(University.Domains), TextJustification.Centered },
            })
            .ToHtml();
Enter fullscreen mode Exit fullscreen mode

This code enables you to justify the content of each column individually. In future versions, I plan to add support for justifying all columns at once, along with more options for applying custom styles.

final

Now, let’s test and explore the pagination functionality to ensure it works as expected.

page 2

Let’s test the functionality again, this time using data from a different country to ensure everything works seamlessly.

select country

The final version of the code should look like the example below. This includes all the enhancements and adjustments we’ve discussed.

public async Task<IActionResult> Index(string country = "United States", int page = 1, int pageSize = 10)
    {
        int offset = (page - 1) * pageSize;

        using var client = new HttpClient();
        var result =
            await client.GetAsync(
                $"http://universities.hipolabs.com/search?country={Uri.EscapeDataString(country)}&limit={pageSize}&offset={offset}");
        var json = await result.Content.ReadAsStringAsync();

        List<University>? universities = JsonSerializer.Deserialize<List<University>>(json);

        var countryResult = await client.GetAsync("http://universities.hipolabs.com/search");
        var countryJson = await countryResult.Content.ReadAsStringAsync();
        List<Country>? allUniversities = JsonSerializer.Deserialize<List<Country>>(countryJson);
        var countries = allUniversities?.Select(u => u.CountryName).Distinct().OrderBy(c => c).ToList() ??
                        new List<string>();

        var html = Table<University>.Add(universities)
            .Style(new StyleSettings
            {
                StyleType = StyleType.Header,
                Properties =
                {
                    { "background-color", "green" },
                    { "text-align", "center" },
                    { "color", "white" },
                    { "padding", "6px" },
                    { "border", "3px solid #dddddd" },
                    { "font-family", "sans-serif" },
                    { "font-size", "16px" }
                }
            })
            .Style(new StyleSettings { StyleType = StyleType.Table, Properties =
            {
                { "border-collapse", "collapse" },
                { "border", "5px solid #dddddd" },
                { "margin-left", "auto" },
                { "margin-right", "auto" },
                {"width", "90%"}
            }})
            .FilterColumns([
                nameof(University.DomainsList), nameof(University.WebPagesList), nameof(University.StateProvince)
            ])
            .ColumnContentTextJustification(new Dictionary<string, TextJustification>
            {
                { nameof(University.Name), TextJustification.Centered },
                { nameof(University.AlphaTwoCode), TextJustification.Centered },
                { nameof(University.WebPages), TextJustification.Centered },
                { nameof(University.Province), TextJustification.Centered },
                { nameof(University.Country), TextJustification.Centered },
                { nameof(University.Domains), TextJustification.Centered },
            })
            .ToHtml();

        ViewBag.TableHtml = html;
        ViewBag.CurrentCountry = country;
        ViewBag.Countries = countries;
        ViewBag.CurrentPage = page;
        ViewBag.PageSize = pageSize;

        return View();
    }
Enter fullscreen mode Exit fullscreen mode

I hope you found this article engaging and that my NuGet package proves useful for your projects. If you’d like to explore further, you can access the full source code at this link.

Thank you for reading! I look forward to sharing more with you next time. Until then, happy coding!

Buy Me A Beer

Top comments (0)