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
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"
]
}
]
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";
}
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; }
}
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" : "")>« Previous</button>
<span>Page @currentPage</span>
<button type="submit" name="page" value="@(currentPage + 1)">Next »</button>
</form>
</div>
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) {}
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;
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();
Deserialize this JSON.
List<University>? universities = JsonSerializer.Deserialize<List<University>>(json);
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>();
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();
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();
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.
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();
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.
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();
We’ve applied custom styles to the table headers, enhancing their appearance and making the table more visually appealing.
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();
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();
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.
Now, let’s test and explore the pagination functionality to ensure it works as expected.
Let’s test the functionality again, this time using data from a different country to ensure everything works seamlessly.
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();
}
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!
Top comments (0)