Developing Your First App with HTMX and .NET: Part IV

Somebody who likes to code
Search for a command to run...

Somebody who likes to code
No comments yet. Be the first to comment.
In many applications, confirmation dialogs are essential for specific actions to ensure users are fully aware of the changes they are making. In our scenario, we will implement two actions that require user confirmation: one to enable a product and a...
Google processes tens of thousands of code changes every day across a codebase of hundreds of millions of lines. The secret isn't magic tooling — it's a documented, principled approach to code review

Amazon Simple Notification Service (SNS) looks straightforward on the surface — publish a message, deliver it to subscribers — but the gap between a topic that "works in dev" and one that is productio

Amazon Simple Queue Service (SQS) is deceptively simple to get started with, but surprisingly easy to misconfigure in ways that cause silent message loss, runaway costs, duplicate processing storms, o

HTTP API is the right default choice for the majority of .NET serverless workloads. At 70% less cost than REST API, lower latency, and native JWT authorization, it covers most production requirements

AWS Lambda has become a cornerstone of modern serverless architectures, but writing a function that "just works" is very different from writing one that is performant, cost-effective, secure, and read

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.