Continuing with our application development, we will now implement pagination for our product list page, as shown in the following image:
You can download the starting code from the previous article. Open the solution and add a ListResponse.cs
file with the following content:
namespace MyApp;
public class ListResponse<T>
{
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public IEnumerable<T> Items { get; set; }
public ListResponse(IEnumerable<T> items, int page, int pageSize, int totalCount)
{
Page = page;
PageSize = pageSize;
TotalCount = totalCount;
Items = items;
}
}
The class above wraps our query results with their pagination information. Add the Extension.cs
file as follows:
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Primitives;
namespace MyApp;
public static class Extensions
{
public static async Task<ListResponse<T>> ToPagedListAsync<T>(
this IQueryable<T> source,
int page,
int pageSize)
{
var totalCount = await source.CountAsync();
if (totalCount > 0)
{
var items = await source
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new ListResponse<T>(items, page, pageSize, totalCount);
}
return new(Enumerable.Empty<T>(), 0, 0, 0);
}
public static Uri AddQuery(this string path, IEnumerable<KeyValuePair<string, StringValues>> query)
{
var queryBuilder = new QueryBuilder(query);
var uriBuilder = new UriBuilder()
{
Path = path,
Query = queryBuilder.ToString()
};
return uriBuilder.Uri;
}
}
The ToPagedListAsync
extension method is used to retrieve and wrap the paged data. The AddQuery
method helps us build a URI based on a path and the query parameters. Update the Products/ListProducts.cs
file as follows:
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Primitives;
namespace MyApp.Products;
public static class ListProducts
{
public class Request
{
public string? Name { get; set; }
}
public static async Task<RazorComponentResult> HandlePage(
[FromServices] MyAppDbContext appDbContext,
[AsParameters] Request request,
HttpContext httpContext,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
var name = request.Name ?? string.Empty;
var results = await appDbContext.Set<Product>()
.AsNoTracking().Where(p => p.Name.Contains(name)).ToPagedListAsync(page, pageSize);
var uri = "/products/list".AddQuery(new KeyValuePair<string, StringValues>[]
{
new("Name", request.Name),
new("Page", page.ToString()),
new("PageSize", pageSize.ToString())
});
httpContext.Response.Headers.Append("HX-Push-Url", uri.PathAndQuery);
return new RazorComponentResult<ListProductsPage>(new { Results = results, Query = uri.Query });
}
}
We are adding two parameters to the HandlePage
method: one to specify the current page and another to select the page size. To retrieve the data from the database, we are using the ToPagedListAsync
extension method previously created. The last change involves building the corresponding URI for our endpoint. Add the RazorComponents/Pagination.razor
file with the following content:
@using Microsoft.AspNetCore.Http.Extensions
@using Microsoft.Extensions.Primitives
@using System.Web
<nav class="btn-toolbar">
<ul class="pagination">
<li class="page-item @(_hasPreviousPage?"":"disabled")">
<a class="page-link"
href="#"
hx-get=@(_hasPreviousPage?GetUri(Page-1, PageSize):string.Empty)
hx-swap=@HtmxAttributes.Swap
hx-target=@HtmxAttributes.Target
hx-select=@HtmxAttributes.Select>Previous</a>
</li>
@for (int i = 1; i <= _totalPages; i++)
{
<li class="page-item @(Page==i?"active":"")">
<a class="page-link"
href="#"
hx-get=@GetUri(i, PageSize)
hx-swap=@HtmxAttributes.Swap
hx-target=@HtmxAttributes.Target
hx-select=@HtmxAttributes.Select>@i</a>
</li>
}
<li class="page-item @(_hasNextPage?"":"disabled")">
<a class="page-link"
href="#"
hx-get=@(_hasNextPage?GetUri(Page+1, PageSize):string.Empty)
hx-swap=@HtmxAttributes.Swap
hx-target=@HtmxAttributes.Target
hx-select=@HtmxAttributes.Select>Next</a>
</li>
</ul>
<div class="dropdown ms-auto">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">
Rows per page (@PageSize)
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item"
hx-get="@GetUri(1, 10)"
hx-swap=@HtmxAttributes.Swap
hx-target=@HtmxAttributes.Target
hx-select=@HtmxAttributes.Select
href="#">10</a>
</li>
<li>
<a class="dropdown-item"
hx-get="@GetUri(1, 20)"
hx-swap=@HtmxAttributes.Swap
hx-target=@HtmxAttributes.Target
hx-select=@HtmxAttributes.Select
href="#">20</a>
</li>
<li>
<a class="dropdown-item"
hx-get="@GetUri(1, 40)"
hx-swap=@HtmxAttributes.Swap
hx-target=@HtmxAttributes.Target
hx-select=@HtmxAttributes.Select
href="#">40</a>
</li>
</ul>
</div>
</nav>
@code {
[Parameter, EditorRequired]
public int Page { get; set; } = 1;
[Parameter, EditorRequired]
public int PageSize { get; set; } = 10;
[Parameter, EditorRequired]
public int TotalCount { get; set; } = default!;
[Parameter, EditorRequired]
public HtmxAttributes HtmxAttributes { get; set; } = default!;
[Parameter, EditorRequired]
public string Query { get; set; } = default!;
private int _totalPages;
private bool _hasPreviousPage;
private bool _hasNextPage;
protected override void OnParametersSet()
{
_totalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
_hasPreviousPage = Page > 1;
_hasNextPage = Page < _totalPages;
}
public string? GetUri(int page, int pageSize)
{
var query = HttpUtility.ParseQueryString(Query);
query["Page"] = page.ToString();
query["PageSize"] = pageSize.ToString();
var uriBuilder = new UriBuilder()
{
Path = HtmxAttributes.Endpoint,
Query = query.ToString()
};
return uriBuilder.Uri.PathAndQuery;
}
}
We use two Bootstrap components to build this Razor component: pagination and dropdown. The component's inputs are the current page, the page size, the total number of records, the HTMX attributes, and the query parameters. The OnParametersSet
method calculates the total number of pages and whether to enable the previous and next page buttons. The method GetUri
is used to build the correct URLs. Run the application to see the new Razor component in action:
You can find the final code here. Thank you, and happy coding.