I have a PaginatedList<T>
class inheriting from List<T>
in an ASP.NET Core app that my controllers and views use to render paginated content:
public class PaginatedList<T> : List<T>
{
public int PageNumber { get; private set; } = 1; // page index
public int PageSize { get; private set; } = 25; // item count in each page
public int TotalCount { get; private set; } // total items in all pages
public int PageCount { get; private set; } // total page count
public bool HasPreviousPage => PageNumber > 1;
public bool HasNextPage => PageNumber < PageCount;
public int From => PageSize * (PageNumber - 1) + 1; // index of first item in page
public int To => From + Count - 1; // index of last item in page
//Method bodies omitted for brevity
public static async Task<PaginatedList<T>> CreateAsync(
IQueryable<T> source, int pageSize = 25, int pageNumber = 1) {..}
private PaginatedList(
List<T> items, int count, int pageSize = 25, int pageNumber = 1) {..}
}
Code based on this with additional validation.
I have noticed that almost all of my views serving paginated lists have the same page navigation UI. (For all entities in the app like Person
, Book
, etc.):
@model PaginatedList<Person>
@*View-specific code here...*@
<nav aria-label="Page Navigation">
@if (Model.Count > 1)
{
@* Display the range of entities displayed *@
<div class="text-center text-muted my-2">
<em>Showing @Model.From to @Model.To out of @Model.TotalCount</em>
</div>
}
<ul class="pagination justify-content-center">
@if (Model.HasPreviousPage || Model.HasNextPage)
{
@if (Model.HasPreviousPage)
{
<li class="page-item">
<a asp-controller="Person"
asp-action="Index"
asp-route-repoId="@Model.RepoId"
asp-route-page="@(Model.PageNumber - 1)"
title="Previous" class="page-link" aria-label="Previous">
<span class="sr-only">Previous</span>«
</a>
</li>
}
else
{
<li class="page-item disabled">
<a class="page-link" tabindex="-1">
<span class="sr-only">Previous</span>«
</a>
</li>
}
<li class="page-item active" aria-current="page">
<a class="page-link" href="#">@Model.PageNumber</a>
</li>
@if (Model.HasNextPage)
{
<li class="page-item">
<a
asp-controller="Person"
asp-action="Index"
asp-route-repoId="@Model.RepoId"
asp-route-page="@(Model.PageNumber + 1)" title="Next" class="page-link" aria-label="Next">
<span class="sr-only">Next</span>»
</a>
</li>
}
else
{
<li class="page-item disabled">
<a class="page-link" tabindex="-1">
<span class="sr-only">Next</span>»
</a>
</li>
}
}
</ul>
</nav>
I would like to extract this to a partial view called _PageNav.cshtml
. But there are two things I’m not quite sure about:
- The
PaginatedList<T>
class is generic and I cannot have a partial view with a model calledPaginatedList<T>
with an open generic parameterT
. One possible solution I can think of is writing a non-generic interface calledIPaginatedList
that has all the properties required for page navigation and then havePaginatedList<T>
implement it. This is because the navigation UI does not need to know anything about the items in the list. Then I can useIPaginatedList
as the model for my partial view:
public interface IPaginatedList
{
public int PageNumber { get; }
public int PageSize { get; }
public int TotalCount { get; }
public int PageCount { get; }
public bool HasPreviousPage { get; }
public bool HasNextPage { get; }
public int From { get; }
public int To { get; }
}
public class PaginatedList<T> : List<T>, IPaginatedList
{
...
}
_PageNav.cshtml
:
@model IPaginatedList
@* Navigation UI *@
...
This would kind of solve the first issue.
- The second issue is that each view with a paginated list has a different previous and next page link. (the area/controller/action name for the link and different route values):
@* Previous page link *@
<a asp-controller="Persons"
asp-action="Index"
asp-route-repoId="@Model.RepoId"
asp-route-page="@(Model.PageNumber - 1)"
title="Previous"
class="page-link"
aria-label="Previous">
<span class="sr-only">Previous</span>«
</a>
@* Next page link *@
<a asp-controller="Persons"
asp-action="Index"
asp-route-repoId="@Model.RepoId"
asp-route-page="@(Model.PageNumber + 1)"
title="Next"
class="page-link"
aria-label="Next">
<span class="sr-only">Next</span>»
</a>
The above would be the previous and next links for the Index
action of PersonsController
(Person
entity). This would change for another entity like Book
.
Should I have a property for each link tag helper argument in my PaginatedList<T>
like ControllerName
, ActionName
, etc.? Is there a better way?
Can this be solved with a custom tag-helper? What am I missing?
2
I use the page model… so @model YourSite.Pages.IndexModel. (razor pages with view models…the partial uses the same model as the “body” partial I use) When you pass your Iqueryable into your Paginatation class, you type it, and then cast it again on return: public PaginatedList<InventoryItem> PaginatedInventory { get; set; } PaginatedInventory = await PaginatedList<InventoryItem>.CreateAsync( Items,CurrentPageIndex, IntegerCurrentLimit); So you really do need separate pagination partials for each different page/view model.
The PaginatedInventory object contains some of the parameters you need… such has HasPrevious, HasNext, etc… for instance in links I use the Page index for next/previous, like this for previous: asp-route-pageIndex=”@(Model.PaginatedInventory.PageIndex – 1)”